
アプリの設計において、特定のデータを永久的に保存する必要があります。
その際にユーザーのスマホ内に作成されるローカルDBを設計します。
先日はIsarをご紹介しましたが、個人的な評価としては、Isarは更新頻度が低く不安です。
そこで今回は、Isarにも負けず高速なRDBのDriftについて紹介します。
そもそもローカルDBってどのような時に必要?
通常、アプリで使用する変数や状態(State)はアプリが終了した際に、
データも一緒に消えてしまいますが、アプリによってはメモやチャット履歴など、
次回起動時も利用したいデータがあります。
そのようなデータを保存するため、ローカルDBを使用します。
Driftとは?
Driftは、FlutterおよびDart向けの強力なローカルデータベースライブラリです。
SQLiteをベースにしており、型安全なクエリビルダー、リアクティブなストリーム、
そして直感的なデータ操作を提供します。旧名:Moorです。
Driftの主な特徴
それらのデータをローカルDBに保存しておくことで、
次回起動時も利用することができます。
Driftには以下の特徴があります。
- 型安全なクエリ
DriftはDartの型システムを活用し、コンパイル時にクエリのエラーを検出します。
コーディング中にエラーが検知できるのはありがたいですね! - リアクティブなストリーム
データベースの変更をリアルタイムで検知し、UIを自動的に更新することが可能です。 - 拡張性とカスタマイズ性
Driftは拡張性が高く、カスタム関数やトリガーを簡単に追加することができます。
複雑なクエリやトランザクションも容易に扱えるので、自由度が高いです! - 豊富なドキュメント
Driftは詳細なドキュメントと活発なコミュニティによって支えられているため、
ドキュメントを見れば大体のことはできるはず!?
公式ドキュメント)https://drift.simonbinder.eu/setup/ - 活発な更新頻度
Driftの更新頻度は高く、直近だと2週間〜1ヶ月ごとにバージョンアップがされています。
Stream社がDriftのスポンサーとなっているから活発なのかもしれません。
Driftの使い方
Driftの使用方法について、ご紹介します。
環境の準備
“pubspec.yaml”に必要なパッケージを追加します。
バージョンについては、ご自身の環境にあったものをご利用ください。
dependencies:
〜他の既存パッケージ〜
drift: ^2.23.1
sqlite3_flutter_libs: ^0.5.28
dev_dependencies:
build_runner: ^2.4.14
drift_dev: ^2.23.1
- drift: driftのコアライブラリ
- sqlite3_flutter_libs: FlutterでSQLite3を使用するための追加ライブラリ
- build_runner: Dartのコード生成を行うためのツール
- drift_dev: Driftのコード自動生成ツール
データベースとテーブルの定義
まず初めに、DriftはRDBのためデータベースとテーブルを定義する必要があります。
今回はファイルを以下のように分けていますが、テーブル定義とデータベース定義を
1ファイルにしても良いです。
- テーブル定義ファイル:todo_tbl.dart
- データベース定義ファイル:app_db.dart
- 自動生成ファイル:app_db.g.dart
テーブル定義
テーブルの定義を行い、管理するデータを設定します。
例として、ToDo情報を管理するテーブル:ToDosを定義してみます。
part of 'app_db.dart';
class Todos extends Table {
IntColumn get id => integer().autoIncrement()(); // autoIncrement()は自動採番
TextColumn get title => text().withLength(min: 1, max: 50)();
BoolColumn get completed => boolean().withDefault(Constant(false))();
}
テーブル名の末尾に”s”をつけると自動的にレコード名は”s”が除いた名前となります。
上記だとテーブル名:ToDos、レコード名:ToDo です。
データベースの定義
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'dart:io';
part 'todo_tbl.dart'; // 上記で定義したテーブルのファイル
part 'app_db.g.dart'; // ジェネレータで自動生成されるファイル
@DriftDatabase(tables: [Todos]) // 複数テーブルがある場合は[ToDos, Users]のようにカンマ区切りで指定
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1; // スキーマバージョン
//処理
Future<List<Todo>> getAllTodos() => select(todos).get(); // 全てのレコードを取得
Future<List<Todo>> fetchTodos(String word) async {
return await (select(todos)..where((tbl) => tbl.title.equals(word))).get();
}
Future insertTodo(TodosCompanion todo) => into(todos).insert(todo); // レコードの挿入
Future updateTodo(Todo todo) => update(todos).replace(todo); // レコードの更新
Future deleteTodo(int id) =>
(delete(todos)..where((tbl) => tbl.id.equals(id))).go(); // レコードの削除
Stream<List<Todo>> watchAllTodos() => select(todos).watch(); // (参考)レコードを監視
}
// ローカルDBの実データの格納場所
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
return NativeDatabase(file);
});
}
インポートしている”app_db.g.dart”は、この時点では存在しないため、エラーが発生します。
以下コードをターミナルで実行することで”app_db.g.dart”が作成されます。
flutter pub run build_runner build --delete-conflicting-outputs
Driftの基本的な使い方
Driftを使用してデータベース操作を行うクライアント処理をご紹介します。
基本的には、上記のデータベースに設定した処理を呼び出すだけです。
データの取得
import 'app_db.dart';
final db = AppDatabase();
Future<List<Todo>> fetchAllTodos() async {
return await db.getAllTodos();
}
データの条件取得
import 'app_db.dart';
final db = AppDatabase();
Future<List<Todo>> fetchTodos(String word) async {
return await db.fetchTodos(word);
}
データの挿入
import 'app_db.dart';
final db = AppDatabase();
Future<void> addTodo(String title) async {
await db.insertTodo(TodosCompanion(
title: Value(title),
));
}
データの更新
import 'app_db.dart';
final db = AppDatabase();
Future<void> toggleTodoStatus(Todo todo) async {
final updatedTodo = todo.copyWith(completed: !todo.completed);
await db.updateTodo(updatedTodo);
}
データの削除
import 'app_db.dart';
final db = AppDatabase();
Future<void> deleteTodoItem(int id) async {
await db.deleteTodo(id);
}
データのドメインモデルとマッピング
通常、アプリ内で扱うデータは単純ではなく、ドメインモデルとして定義されることが一般的です。
その場合、ドメインモデルとデータベースモデル間のデータの変換が必要になります。
ドメインモデルの定義
ドメインモデルのToDo
クラスが以下だったとします。
/// ドメインモデル: ToDo
class ToDo {
final int? id;
final String title;
final bool isCompleted;
ToDo({
this.id,
required this.title,
this.isCompleted = false,
});
}
ドメインモデルとデータベースモデルの変換処理
拡張メソッドを使用し、ドメインモデルとデータベースの変換処理を実装します。
/// ドメインモデル → Driftのデータベースモデルへの変換
extension ToDoToDatabase on ToDo {
TodosCompanion toInsertCompanion() {
return TodosCompanion.insert(
// insert時にidは自動設定される
title: title,
completed: completed,
);
}
TodosCompanion toUpdateCompanion() {
return TodosCompanion(
id: Value(id),
title: Value(title),
completed: Value(isCompleted),
);
}
}
/// Driftのデータベースモデル → ドメインモデルへの変換
extension DatabaseToDo on Todo {
ToDo toDomain() {
return ToDo(
id: id,
title: title,
isCompleted: completed,
);
}
}
マッピングを活用したデータ操作
例として、上記変換処理を使ったデータ操作をご紹介します。
import 'app_db.dart';
final db = AppDatabase();
// ToDo挿入
Future<void> addToDoItem(ToDo toDo) async {
await db.insertTodo(toDo.toInsertCompanion());
}
// ToDo全件取得
Future<List<ToDo>> getToDoItems() async {
final dbTodos = await db.getAllTodos();
return dbTodos.map((dbTodo) => dbTodo.toDomain()).toList();
}
Driftの注意
DriftはRDBのため、テーブル定義やスキーマを変更するときにはバージョン管理が必要です。
また、データ項目を追加する場合は、バージョン毎に移行処理を記述する必要があります。
詳しくは、公式ドキュメントをご参照ください。
公式ドキュメント)https://drift.simonbinder.eu/Migrations/step_by_step/?h=migration
まとめ
Driftの処理スピードは実機での体感ではIsarとあまり変わりませんでした。
パッケージの更新頻度が高いため、他のパッケージとの依存関係も調整しやすく使いやすいです。
Isarの大体としては良い選択肢だと思いました。
皆さんも是非、Driftを使って開発してみてください!