こんにちは。pregum_foxです。
今回はdriftの使い方についていくつか自分が調べて情報がなかった箇所についてtipsとして書いていこうと思います。
以下目次です。
- 検証環境
- サンプルで作成したリポジトリ
- set up
- primaryKeyをuuidにする方法
- 外部キーの有効化
- 外部キー制約の定義方法(CASCADE / RESTRICT 等)
- 中間テーブルを含むDB操作
- マイグレーション方法
- serializer(DataTimeをISO8601形式にする)
- toJsonで出力されるMapのkeyをキャメルケースからスネークケースに変える
- drift_db_viewerの使い方
- シンプルなCRUD操作で使用できるmanagers
- postgreSQL のクライアントとしてDriftを使用する
- まとめ
検証環境
fvm flutter doctor -v [✓] Flutter (Channel stable, 3.22.1, on macOS 14.3.1 23D60 darwin-arm64, locale ja-JP) • Flutter version 3.22.1 on channel stable at /Users/pregum/fvm/versions/3.22.1 • Upstream repository https://github.com/flutter/flutter.git • Framework revision a14f74ff3a (4 weeks ago), 2024-05-22 11:08:21 -0500 • Engine revision 55eae6864b • Dart version 3.4.1 • DevTools version 2.34.3
サンプルで作成したリポジトリ
今回の検証は以下のリポジトリにて試したものがあるので、よければ参考にしてください
https://github.com/Pregum/drift_practice
set up
driftのセットアップは、下記のページからテーブルの定義方法から実際にdbへCRUD処理を行うところまで書かれております。
まずはこちらのページを読んでイメージを掴んでみてください
https://drift.simonbinder.eu/docs/getting-started/
上記をベースに、もう少し込み入ったことをするときのtipsを書いていきます。
primaryKeyをuuidにする方法
drift(sqlite) では uuidカラムは存在しないのですが、clientDefault
を使うことで、生成時に関数を実行してuuidを生成できるようになります
const _uuid = Uuid(); class TodoCategories extends Table { TextColumn get id => text().clientDefault(() => _uuid.v4())(); // これで、レコード追加時にuuidが入るようになる TextColumn get description => text()(); @override Set<Column<Object>>? get primaryKey => {id}; }
似たようなメソッドでwithDefault()
がありますが、こちらは生成時に関数は実行されず、同じ値が入る為、デフォルト値として空文字を入れたりする際に使用します。
withDefault についての説明は下記をご覧ください
外部キーの有効化
drift(sqlite) では、外部キー設定はデフォルトでOFFになっている為、まずはdbアクセス前に外部キー設定をONにします。
/// 重要なお知らせ: sqlite3 では、外部キーはデフォルトで有効になっていません。外部キーを使用する場合は、[MigrationStrategy.beforeOpen] コールバックでオプションを有効にすることを忘れないでください: /// 外部キーを有効にする為にPRAGMA foreign_keys = ONを実行 @override MigrationStrategy get migration => MigrationStrategy( onCreate: (detail) { return detail.createAll(); }, //毎回アプリを明けデータベースにアクセスするタイミングで呼ばれる。 beforeOpen: (details) async { await customStatement('PRAGMA foreign_keys = ON;'); }, );
外部キー制約の定義方法(CASCADE / RESTRICT 等)
references
を使うことで外部キーの設定ができます。
第1引数に参照先のテーブル、第2引数に一致させるカラムに#
をつけたシンボル型で定義します。
class TodoItemHashTag extends Table { IntColumn get todoItemId => integer().references(TodoItems, #id, onDelete: KeyAction.cascade)(); TextColumn get hashTagId => text().references(HashTags, #id, onDelete: KeyAction.cascade)(); // こんなかんじで外部キーを指定することもできる // TextColumn get hashTagId => // text().customConstraint('REFERENCES hash_tags(id)')(); @override Set<Column<Object>>? get primaryKey => {todoItemId, hashTagId}; }
上記のコードに書いている通り、onDeleteの箇所で外部キー制約が設定できます。
更新時は、onUpdateで定義できます
上記の例だと、todoItemId
もしくは hashTagId
が一致するレコードが削除された時、同じidが含まれる TodoItemHashTag
のテーブル
中間テーブルを含むDB操作
シンプルなDB操作の場合、後述するmanagersを使用することでIterableな配列と似たインターフェースでDB操作が可能です ですが、中間テーブルが必要な3つ以上のテーブルを操作する場合、DAO(Data Access Object)を使って操作を行います。 おそらくみなさん〜〜Serviceや〜〜Repositoryといった名前で普段定義されているクラスのことだと思ってもらえれば良いです
下記に HashTag
に紐づく TodoItem
のリストを返すメソッドです。
少し長いですが、以下の中央付近のコードで紐付けを行っています。
Future<(HashTag, List<TodoItem>)> getHashTagWithItems({ required String hashTagId, }) async { // 対象のハッシュタグを取得 final hashTag = await (select(hashTags) ..where((c) => c.id.equals(hashTagId))) .getSingle(); // 紐づくTodoItemsを中間テーブル経由で取得 final relatedTodoItemsQuery = select(hashTags).join([ innerJoin( todoItemHashTag, todoItemHashTag.hashTagId.equalsExp(hashTags.id)), innerJoin(todoItems, todoItems.id.equalsExp(todoItemHashTag.todoItemId)), ]) ..where(hashTags.id.equals(hashTagId)); final result = await relatedTodoItemsQuery.get(); final todoList = result.map((row) { final item = row.readTable(todoItems); return item; }).toList(); return (hashTag, todoList); }
マイグレーション方法
まだそこまで試せれてはいないのですが、schemaVersion
バージョンを上げていき、その際のマイグレーションをmigration
getterで記述していけば良さそうです。
カラム名の変更などもできるのでうまくマイグレーション毎に処理を切り出すのが大切になりそう
https://drift.simonbinder.eu/docs/migrations/
serializer(DataTimeをISO8601形式にする)
今回サンプルレポジトリ内には入れていないのですが、toJsonを叩くとデフォルトだとunix timestampが設定されています。
ビルドオプション設定を変えることで、ISO8601 に変えることもできるらしいのですが、今回はコードで変更する方法を選択しました。
下記のようにtoJsonのSerializerをdefaultファクトリメソッドで呼び出して引数にiso8601の使用フラグがあるのでそれをtrueにすればOKです。
final exhibitionData = await query.getSingle(); return Exhibition.fromJson( exhibitionData.toJson( serializer: const ValueSerializer.defaults( serializeDateTimeValuesAsString: true), ), );
オプションの詳細な説明は下記をご覧ください。
https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/#datetime-options
toJsonで出力されるMapのkeyをキャメルケースからスネークケースに変える
JsonSerializableと同様な形で @JsonKey を使用することができます。
ただ、 @JsonSerializable(fieldRename: FieldRename.snake)
は適用できる場所がなかった為、1つずつJsonKeyを定義する力技でとりあえず対応しました...
class UserTable extends Table { TextColumn get id => text().clientDefault(() => _uuid.v4())(); TextColumn get username => text().nullable()(); @JsonKey('created_at') DateTimeColumn get createdAt => dateTime().clientDefault(() => DateTime.now())(); @JsonKey('updated_at') DateTimeColumn get updatedAt => dateTime().nullable()(); @override Set<Column<Object>>? get primaryKey => {id}; }
試せてはいないのですが、ビルドオプションでsqlのテーブルのカラムを使用するオプション(use_sql_column_name_as_json_key
) をtrueにすることで、デフォルトだとスネークケース(case_from_dart_to_sql
オプション のデフォルトがsnake_case
)で生成される為
おそらくuse_sql_column_name_as_json_key
をtrueにすることで、toJsonのkeyをsnake_caseにすることができそうです。
※ マイグレーションなどでカラム名を変えた場合、不一致になる可能性があるので変更するときは慎重に試すのが良さそうです。
drift_db_viewerの使い方
現在のDBの中身をbreak pointなどで毎回確認するのは大変なので、drift_db_viewerを試してみました。
driftで自動生成されたテーブルが確認できる為、導入するための敷居はすこく低くてよかったです。
今回作成したサンプルだと以下のように使用しました。
void _handleOnPressedFAB() { final db = AppDatabase.singleton(); Navigator.of(context).push<void>( MaterialPageRoute<void>( builder: (context) { return DriftDbViewer(db); }, ), ); }
シンプルなCRUD操作で使用できるmanagers
最初はChatGPTに聞いていたのですが、ドキュメントを見るとmanagersクラスがあって、そこで簡単なDB操作ができましたのでここに記載します。
CREATE
Future<void> createTodoItem() async { // Create a new item await managers.todoItems .create((o) => o(title: 'Title', content: 'Content')); // We can also use `mode` and `onConflict` parameters, just // like in the `[InsertStatement.insert]` method on the table await managers.todoItems.create( (o) => o(title: 'Title', content: 'New Content'), mode: InsertMode.replace); // We can also create multiple items at once await managers.todoItems.bulkCreate( (o) => [ o(title: 'Title 1', content: 'Content 1'), o(title: 'Title 2', content: 'Content 2'), ], ); }
READ
通常の選択
Future<void> selectTodoItems() async { // Get all items managers.todoItems.get(); // A stream of all the todo items, updated in real-time managers.todoItems.watch(); // To get a single item, apply a filter and call `getSingle` await managers.todoItems.filter((f) => f.id(1)).getSingle(); }
フィルタする場合
Future<void> filterTodoItems() async { // All items with a title of "Title" managers.todoItems.filter((f) => f.title("Title")); // All items with a title of "Title" and content of "Content" managers.todoItems.filter((f) => f.title("Title") & f.content("Content")); // All items with a title of "Title" or content that is not null managers.todoItems.filter((f) => f.title("Title") | f.content.not.isNull()); }
UPDATE
Future<void> updateTodoItems() async { // Update all items await managers.todoItems.update((o) => o(content: Value('New Content'))); // Update multiple items await managers.todoItems .filter((f) => f.id.isIn([1, 2, 3])) .update((o) => o(content: Value('New Content'))); }
DELETE
Future<void> deleteTodoItems() async { // Delete all items await managers.todoItems.delete(); // Delete a single item await managers.todoItems.filter((f) => f.id(5)).delete(); }
他にも親テーブルのレコードの値によってフィルタ、子テーブルのレコードの値によってフィルタすることもできます。
詳細は下記URLをご覧ください
https://drift.simonbinder.eu/docs/manager/
postgreSQL のクライアントとしてDriftを使用する
実際には試していませんが、postgreSQL のDB操作もサポートされているようなので、試したい場合は下記ページが参考になるかと思います。
https://drift.simonbinder.eu/docs/platforms/postgres/
まとめ
driftという単語は知っていましたが、sqfliteしか使ったことがなかったので、コードが補完されつつ、簡易的なviewerも手軽に試せて良いなと思いました。
トランザクションも含まれて実装はできたのですが、まだ動作確認はできていないので、確認できたら追記しようと思います。
ここまで読んでいただきありがとうございました。