[Flutter] sqfliteのopenDatabase詳解
sqfliteを利用しようとした際に、基本的なDatabaseインスタンス管理等の実装パターンについて悩んだので、調査結果についてのメモになります。
主にopenDatabaseの処理フローと、それを踏まえマイグレーションなどを考えた実装パターンの個人的なメモを記載します。
sqfliteについて
sqfliteは、FlutterアプリケーションでSQLiteデータベースを利用するための人気のパッケージです。
主な機能
sqfliteパッケージには以下のような主要な機能があります
- データの挿入、取得、更新、削除(CRUD操作)
- クエリの実行
- トランザクション管理
- テーブルの作成と管理
- データベースの自動バージョン管理
- 背景スレッドでのデータベース操作(iOSおよびAndroid)
簡単な使用方法
- インストール
pubspec.yamlファイルに以下を追加します
dependencies:
sqflite: ^2.2.8+4
path: ^1.8.0
その後、flutter pub get
コマンドを実行してパッケージをインストールします。
- データベースの作成
以下のようなコードでデータベースを作成し、テーブルを定義できます
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
Future<Database> openDatabase() async {
String databasesPath = await getDatabasesPath();
String dbPath = join(databasesPath, 'my_database.db');
// 今回、取り扱うのは主にここ
return await openDatabase(
dbPath,
version: 1,
onCreate: (Database db, int version) async {
await db.execute(
"CREATE TABLE Users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)"
);
},
);
}
- データの操作
sqfliteでは、insert(), query(), update(), delete()などのメソッドを使用してデータを操作できます
今回は、2のデータベースの作成時に必要なopenDatabase処理について取り扱います
openDatabaseについて
openDatabase
メソッドは、指定されたパスでデータベースを開き、各DB操作を行う Database
インスタンスを取得します。
まずopenDatabaseの定義は下記になっており、該当DBを指定するpath以外に、DBの設定・作成時に呼び出されるコールバック関数とインスタンスの扱いを決めるプロパティを指定できます。
Future<Database> openDatabase(String path,
{int? version,
OnDatabaseConfigureFn? onConfigure,
OnDatabaseCreateFn? onCreate,
OnDatabaseVersionChangeFn? onUpgrade,
OnDatabaseVersionChangeFn? onDowngrade,
OnDatabaseOpenFn? onOpen,
bool? readOnly = false,
bool? singleInstance = true}) {
...
}
openDatabaseで指定可能なパラメーター
各パラメーターについて、コードコメントからの抜粋です
-
version
: 開かれるデータベースのスキーマバージョンを指定します。これによって、onCreate
、onUpgrade
、およびonDowngrade
のいずれが呼び出されるかが決まります。 -
onConfigure
: データベースを開く際に最初に呼び出されるコールバックです。これを用いて、外部キーの有効化や先行書き込みログの有効化などのデータベース初期化を行うことができます。 -
onCreate
:openDatabase
を呼び出す前にデータベースが存在しなかった場合に呼び出されます。この機会を利用して、スキーマに従って必要なテーブルをデータベース内に作成できます。 -
onUpgrade
:onCreate
が指定されていない。またはデータベースがすでに存在し、version
が最後のデータベースバージョンより高い場合に呼び出されます。- onCreateが指定されていないケースでは、oldVersionパラメータを0として呼び出されます。
-
onDowngrade
:version
が最後のデータベースバージョンより低い場合にのみ呼び出されます。これは稀なケースで、データベースが古いバージョンのコードによって操作されるようなことは避けるべきとしてます。 -
onOpen
: 呼び出される最後のオプションのコールバックです。データベースのバージョンが設定された後、openDatabase
が戻る前に呼び出されます。 -
readOnly
: trueの場合、他のすべてのパラメータは無視され、データベースはそのままで開かれます。(デフォルトはfalse) -
singleInstance
: 指定されたパスに対して単一のデータベースインスタンスが返されます。同じパスでの後続のopenDatabase
呼び出しは同じインスタンスを返し、その呼び出し時のコールバックのような他のすべてのパラメータは無視されます。(デフォルトはtrue)
コールバック処理のフロー
複数のコールバック関数を指定すると、インスタンスの作成時は以下の順序で呼び出されます。
onConfigure
-
onCreate
またはonUpgrade
またはonDowngrade
onOpen
2の onCreate
, onUpgrade
, onDowngrade
は排他的に呼び出され一回のインスタンス作成時にはいずれかのみが呼び出されます。
定義を書き出してみると、マイグレーションを順に行うのであれば onCreate
を定義せずに onUpgrade
のみでも担保可能かもしれません。
また、上記では記載してませんが singleInstance
で2回目にインスタンス取得が起こる場合は、全コールバック関数は無視されます。
コード例
Databaseのインスタンス管理を行うDataBaseConnector
クラスと、マイグレーション処理を行う DatabaseMigration
クラスを下記のように実装しました。
バージョン管理は DatabaseMigration
クラスで行い、Batchなど依存関係のあるまとまった処理は行わないシンプルな想定で書いてます。
データベース作成処理 : DataBaseConnector
クラス
import './database_migration.dart';
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
class DataBaseConnector {
final String dbName;
late Future<Database> database;
DataBaseConnector({
required this.dbName,
}) {
assert(dbName.endsWith('.db'));
debugPrint('DataBaseConnector($dbName)');
init();
}
init() {
debugPrint('DataBaseConnector.init($dbName)');
// getDatabasesPath():デフォルトのデータベース保存用フォルダのパスを取得
var databasesPath = getDatabasesPath();
// 取得したパスから本アプリ用にて生成するDB名を指定
String path = '$databasesPath/$dbName';
database = openDatabase(
path,
// 0より大きい32ビット整数
version: DatabaseMigration.getLatestVersion(),
onConfigure: (Database db) async {
// DBが開かれた際に最初に呼び出される
debugPrint('onConfigureDatabase');
},
// [version] が指定されている場合、[onCreate]、[onUpgrade]、および [onDowngrade] が呼び出される
// これらの関数は相互排他的で、状況に応じて一つの関数だけが呼び出される
onCreate: (Database db, int version) async {
// [openDatabase]を呼び出す前にデータベースが存在しなかった場合に呼び出される
debugPrint('onCreateDatabase($version)');
for (var migration in DatabaseMigration.values) {
for (var sqlCommand in migration.sqlCommands) {
await db.execute(sqlCommand);
}
}
},
onUpgrade: (Database db, int oldVersion, int newVersion) async {
// データベースがすでに存在し、[version]が最後のデータベースバージョンより高い場合に呼び出される
// onCreateが指定されていない場合には、oldVersion = 0として呼び出される
debugPrint('onUpgradeDatabase($oldVersion, $newVersion)');
final List<DatabaseMigration> targetMigrations =
DatabaseMigration.getCompareMigrations(oldVersion, newVersion);
for (var migration in targetMigrations) {
for (var sqlCommand in migration.sqlCommands) {
await db.execute(sqlCommand);
}
}
},
onDowngrade: (Database db, int oldVersion, int newVersion) async {
// データベースがすでに存在し、[version]が最後のデータベースバージョンより低い場合にのみ呼び出されます
debugPrint('onDowngradeDatabase($oldVersion, $newVersion)');
},
onOpen: (Database db) async {
// DBが開かれる直前に呼び出される
debugPrint('onOpenDatabase');
// isDebugModeの場合、データベースの内容を確認する
_debugSchemaPrint(db);
},
// デフォルトで、trueだが後学のために明示的に記載
// 指定されたパスに対して単一のデータベースインスタンスが返される。
// 同じパスでの後続の[openDatabase]呼び出しは同じインスタンスを返し、
// その呼び出し時のコールバックのような他のすべてのパラメータは無視される。
singleInstance: true,
);
}
Future<void> close() async {
database.then((db) => db.close());
}
void _debugSchemaPrint(Database db) async {
if (kDebugMode) {
final List<Map<String, dynamic>> schema =
await db.rawQuery("SELECT * FROM sqlite_master WHERE type='table'");
for (var table in schema) {
print('Table Name: ${table['name']}');
print('SQL: ${table['sql']}');
}
}
}
}
マイグレーション処理管理 : DatabaseMigration
クラス
enum DatabaseMigration {
v1(version: 1, sqlCommands: [
'''
CREATE TABLE Company (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT
);
'''
]),
v2(version: 2, sqlCommands: [
'ALTER TABLE Company ADD description TEXT',
'''
CREATE TABLE Test2 (
id INTEGER PRIMARY KEY,
name TEXT,
value INTEGER,
num REAL
);
''',
]),
;
final int version;
final List<String> sqlCommands;
const DatabaseMigration({
required this.version,
required this.sqlCommands,
});
static int getLatestVersion() {
return values.last.version;
}
static List<DatabaseMigration> getCompareMigrations(
int oldVersion,
int currentVersion,
) {
assert(oldVersion >= 0);
assert(currentVersion >= 1);
return values.where((migration) {
return migration.version > oldVersion &&
migration.version <= currentVersion;
}).toList();
}
}
参考
部分的には、公式に下記のガイドが提供されているので合わせて参考にしてみてください
Discussion