👻

FlutterのSQLiteでインメモリDBを使う

2021/10/12に公開

これはなに?

FlutterでSQLiteを使う際に、パフォーマンス向上を目的にDBデータをすべてインメモリにしてしまおうという試みです。

SQLiteをインメモリDBで使うとは

Flutterで開発するにあたり、SQLiteは非常に使いやすいですよね。
assets以下にdbファイルを配置すればマスターデータをアプリに組み込んだりできますし、気軽にユーザーデータを保存できるのでおすすめです。

SQLiteは標準でもパフォーマンスは高いですが、MBレベルの軽量なマスターデータかつ、頻繁に検索が走りパフォーマンスに影響があるような状況でしたら、DBのデータをすべてメモリ上に乗せた状態でselect等を行うことでさらに高速に処理を行えます。

具体的にどうするか?

やり方は非常にかんたんで、起動時にassets以下のDBを読み込んで、インメモリDBを作成してコピーすればあとは通常のDBと同じように扱えます。

import 'package:sqflite/sqflite.dart';

class InMemoryDBProvider {
  // コンストラクタを呼んでシングルトンで構成
  InMemoryDBProvider._();
  static final InMemoryDBProvider instance = InMemoryDBProvider._();

  Database _database;
  bool initializeStarted = false;
  Future<Database> get database async {
    if (_database != null) return _database;
    if (initializeStarted) {
      while (_database == null) {
        await Future.delayed(Duration(milliseconds: 200));
      }
    } else {
      initializeStarted = true;
      _database = await _initDatabase();
    }
    return _database;
  }

  // 初期化
  _initDatabase() async {
    // assets以下のDBを読み込み
    final String databasesPath = await getDatabasesPath();
    final String path = join(databasesPath, "on_memory_db.sqlite3");
    var exists = await databaseExists(path);
    if (!exists) {
      try {
        await Directory(dirname(path)).create(recursive: true);
      } catch (_) {}
      ByteData data = await rootBundle.load(join("assets", "on_memory_db.sqlite3"));
      List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
      await File(path).writeAsBytes(bytes, flush: true);
    }

    // assets以下のテーブルの内容をインメモリDBに乗せる
    var _db;
    try {
      _db = await openDatabase(inMemoryDatabasePath);
      await _db.rawQuery("ATTACH DATABASE ? as tmpDb", [path]);
      await _db.rawQuery("CREATE TABLE my_items AS SELECT * FROM tmpDb.my_items");
      await _db.rawQuery("DETACH DATABASE tmpDb");
    } catch (e) {
      print("Error creating database");
      _db.close();
      _db = null;
      throw (e);
    }
    return _db;
  }

  Future close() async => (await instance.database).close();
}

// モデル
class MyItem {
  num id;
  String name;

  MyItem({
    this.id,
    this.name,
  });

  Map<String, dynamic> toMap() => {
        "id": id,
        "name": name,
      };

  factory MyItem.fromMap(Map<String, dynamic> json) => MyItem(
        id: json["id"],
        name: json["name"],
      );

  static Future<MyItem?> findById(num id) async {
    final db = await InMemoryDBProvider.instance.database;
    List<Map> maps = await db.query("my_items", where: 'id = ?', whereArgs: [id]);
    if (maps.length > 0) {
      return MyItem.fromMap(maps.first);
    }
    return null;
  }
}

注意点

起動時にassets以下のファイルDBから読み込んで使うようになっているので、create/update/deleteされた内容は永続化されません。
追加の対応を行えばそれらを永続化できますが、マスターデータなど検索をヘビーに行う状況が多いと思うのでそういった場合に活用ください。

自分はこのInMemoryDBProviderと(通常のファイルDBとしての)UserDBProviderをアプリ内で使い分けるようにしています。

誰かのお役に立てば幸いです :bow:

Happy Elements

Discussion