FlutterでSQLite接続

Gitを翻訳したものです。
引用: https://github.com/tekartik/sqflite/blob/master/sqflite/doc/opening_db.md

データベースのロケーションパスを探す

Sqfliteは、Androidではデータベースのパス、iOSではドキュメントフォルダを使用した基本的なロケーション戦略を提供しています。
両プラットフォームで推奨されています。この場所は getDatabasesPath を使って取得することができる。

var databasesPath = await getDatabasesPath();
var path = join(databasesPath, dbName);

// ディレクトリが存在することを確認する
try {
  await Directory(databasesPath).create(recursive: true);
} catch (_) {}

DB接続

SQLiteデータベースは、パスで記載できる形のファイルです。相対パスの場合、このパスは
getDatabasesPath()によって得られる、Androidではデフォルトのデータベースディレクトリ、iOSではdocumentsディレクトリが使われます。

var db = await openDatabase('my_db.db');

読み取り/書き込み

データベースを読み書きモードで開くのはデフォルトです。バージョンを指定して実行することができます。
マイグレーション戦略、データベースとそのバージョンを設定することができます。

構成

onConfigure は、最初に呼び出されるオプションのコールバックです。このコールバックは、データベースの初期化を行います。
カスケード削除のサポートなど

_onConfigure(Database db) async {
  // カスケード削除のサポート追加
  await db.execute("PRAGMA foreign_keys = ON");
}

var db = await openDatabase(path, onConfigure: _onConfigure);

プリロードデータについて

初回起動時にデータベースをプリロードしておくとよいでしょう。次のいずれかの方法があります。

_onCreate(Database db, int version) async {
  // データベースが作成されたら、テーブルを作成する
  await db.execute(
    "CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)");
  // データを入力する
  await db.insert(...);
}

// データベースを開き、バージョンとonCreateコールバックを指定します。
var db = await openDatabase(path,
    version: 1,
    onCreate: _onCreate);

マイグレーション

データベースのアップグレード(スキーマの変更)を処理するために、Android APIと同様の基本的なバージョン管理機構があります。getVersionsetVersionが公開されていますが、これは使用せず、データベースを開くときに移行を実行する必要があります。

onCreate, onUpgrade, onDowngradeは、バージョンが指定されたときに呼び出される。データベースが存在しない場合、onCreate が呼び出される。onCreateが定義されていない場合、oldVersionの値が0であるonUpgradeが代わりに呼び出される。データベースが存在し、新しいバージョンが現在のバージョンより高い場合、onUpgrade が呼び出される。逆に、新しいバージョンが現在のバージョンより低い場合、onDowngradeが呼び出されます。データベースのバージョンを常にインクリメントすることで、これを回避するようにしてください。ダウングレードの場合、特別な onDatabaseDowngradeDelete コールバックが存在し、単にデータベースを削除して onCreate を呼び出し、データベースを作成します。

これら3つのコールバックは、データベースのバージョンが設定される直前に、トランザクション内で呼び出されます。

_onCreate(Database db, int version) async {
  // データベースが作成されたら、テーブルを作成する
  await db.execute(
    "CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)");
}

_onUpgrade(Database db, int oldVersion, int newVersion) async {
  // データベースのバージョンが更新されたら、テーブルを変更する
  await db.execute("ALTER TABLE Test ADD name TEXT");
}

// onDowngradeに使用される特別なコールバックは、データベースを再作成するために使用されます。
var db = await openDatabase(path,
  version: 1,
  onCreate: _onCreate,
  onUpgrade: _onUpgrade,
  onDowngrade: onDatabaseDowngradeDelete);

完全移行例をご覧ください。

接続後のコールバック

便宜上、onOpen はデータベースのバージョンを設定した後、 openDatabase が戻る前に呼び出されます。

_onOpen(Database db) async {
  // データベースが開かれている場合、そのバージョンを表示する
  print('db version ${await db.getVersion()}');
}

var db = await openDatabase(
  path,
  onOpen: _onOpen,
);

読み取り専用

// データベースを読み取り専用で開く
var db = await openReadOnlyDatabase(path);

破損ファイルの処理

AndroidとiOSでは、破損の処理方法が異なります。

  • iOSでは、データベースへの最初のアクセスで失敗します。
  • Androidの場合、既存のファイルは削除されます。

既存の挙動を崩さずに一貫性を持たせる方法はまだわかりません。

あるファイルが有効なデータベースファイルであるかどうかを確認する1つの方法は、そのファイルを読み取り専用で開くことだと思われます。
とそのバージョンをチェックします(つまり、sqlite/iOSは非sqliteデータベースの最初のアクセスで非一貫して失敗します)。
これをトップレベルの関数にする前に、動作を検証するためにもっと多くのテストが必要でしょう。

/// ファイルが有効なデータベースファイルであるかどうかをチェックする
///
/// 空のファイルは、有効な空のsqliteファイルです
Future<bool> isDatabase(String path) async {
  Database db;
  bool isDatabase = false;
  try {
    db = await openReadOnlyDatabase(path);
    int version = await db.getVersion();
    if (version != null) {
      isDatabase = true;
    }
  } catch (_) {} finally {
    await db?.close();
  }
  return isDatabase;
}

データベースがロックされる問題の防止

データベースを開くのは一度だけにすることを強くお勧めします。デフォルトでは、データベースはシングルインスタンスとして開かれます。(singleInstance: true

singleInstance: false を使って同じデータベースを何度も開くと、(少なくともAndroidでは)以下のような現象が発生する可能性があります。

android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)

それでは、次のようなヘルパークラスについて考えてみましょう。

class Helper {
  final String path;
  Helper(this.path);
  Database _db;

  Future<Database> getDb() async {
    if (_db == null) {
      _db = await openDatabase(path);
    }
    return _db;
  }
}

openDatabaseは非同期なので、openDatabaseが2回呼び出される可能性があり、レースコンディションのリスクがあります。これを修正するには、以下のようにすればよい。

class Helper {
  final String path;
  Helper(this.path);
  Future<Database> _db;

  Future<Database> getDb() {
    if (_db == null) {
      _db = openDatabase(path);
    }
    return _db;
  }
}

もし、openDatabase の後に、ユーザーが使用できるようにするまでに長い操作がある場合は、次のようにします。
この場合、コードを保護する必要があります (ここでは _initDb() メソッドをプライベートにして、同時アクセスから保護します)。

class Helper {
  final String path;
  Helper(this.path);
  Future<Database> _db;

  Future<Database> getDb() {
    _db ??= _initDb();
    return _db;
  }

  // Guaranteed to be called only once.
  Future<Database> _initDb() async {
    final db = await openDatabase(this.path);
    // do "tons of stuff in async mode"
    return db;
  }
}

例外を解決する

データベースを開くときに例外が発生する場合。

  • トラブルシューティングの項を参照してください。
  • データベースを作成するディレクトリが存在することを確認する。
  • データベースのパスが、既存のデータベース(または何もない)を指していることを確認してください。
    sqliteデータベースでないファイルでないこと。
  • オープンコールバック(onCreate/onUpgrade/onConfigure/onOpen)で予想される例外を処理する。

Flutter開発で知らないと損すること Flutter開発で知らないと損すること

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です