FlutterでDriftを使用してみた～初期構築編～

# はじめに
FlutterにDriftを導入したときの手順を残しておきます。

本記事は以下の公式ドキュメントを参考にしています。
詳しいことが知りたい方はこちらを参照してください。

https://drift.simonbinder.eu/docs/getting-started/

Flutterのインストール方法は説明しません。

# 環境
![Dart-2.19.6](https://img.shields.io/badge/Dart-2.19.6-blue)![Flutter-3.7.12](https://img.shields.io/badge/Flutter-3.7.12-blue)![Drift-2.7.0](https://img.shields.io/badge/Drift-2.7.0-blue)

# 1.インストール
Driftと依存関係のあるパッケージを```pubspec.yaml```に定義します。

## pubspec.yamlでバージョンを指定してインストールする場合
``` diff_yaml:pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
+  drift: ^2.7.0
+  sqlite3_flutter_libs: ^0.5.0
+  path_provider: ^2.0.0
+  path: ^1.8.2

dev_dependencies:
  flutter_test:
    sdk: flutter
+  drift_dev: ^2.7.0
+  build_runner: ^2.3.3
```

<details><summary>各パッケージの用途</summary>

公式ドキュメントをそのまま翻訳しただけのものですが、載せておきます。

| パッケージ名 | 用途 |
|:-|:-|
|drift|ほとんどのAPIを定義するコアパッケージ|
|sqlite3_flutter_libs|AndroidまたはiOSアプリに最新のsqlite3バージョンを含めるためのパッケージ。Flutterを使用していない場合は必要ありませんが、その場合は自分でsqlite3を含める必要があります。他のプラットフォームの概要については、プラットフォームを参照してください。|
|path_providerとpath|データベースを格納するのに適した場所を見つけるためのパッケージ。FlutterとDartチームによって維持されています。|
|drift_dev|テーブルに基づいたクエリコードを生成するための開発時の依存関係。最終的なアプリには含まれません。|
|build_runner|コード生成のための一般的なツールで、Dartチームによって維持されています。|

</details>

以下のコマンドを実行してパッケージをインストールします。
``` 
flutter pub get
```

## コマンドから最新のパッケージをインストールする場合
以下のコマンドを実行してください。
```
dart pub add drift sqlite3_flutter_libs path_provider path dev:drift_dev dev:build_runner
```

# 2.テーブル定義を作成
## 2.1 データベース関連の定義を置くためのフォルダ／ファイルを作成する
```:フォルダ構成
lib
│  main.dart
└─database <- ここに作成
   └─  database.dart
```
## 2.2 定義ファイルを作成
``` dart:database.dart
import 'package:drift/drift.dart';

// このファイルの名前が"database.dart"の場合は以下の名前で定義しておく。
// このタイミングだとファイルが存在しないためエラーが出るが、後の工程で自動作成される。
part 'database.g.dart';

// 以下の定義の場合だと"todos"というテーブルが作成される。
// 行単位のデータは"Todoクラス"になる。
// 　※Todoクラスはこの後の工程で自動で作成される。
class Todos extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text().withLength(min: 6, max: 32)();
  TextColumn get content => text().named('body')();
  IntColumn get category => integer().nullable()();
}

// Driftでは行単位のデータを扱うクラスを作成するときはテーブルクラスから"s"を除いたものを作成するため、
// 単数形と複数形で形が異なる場合は"DataClassName"で単数形の名称を定義する必要がある。
// DataNameClassを定義しない場合は"Categorie"になってしまう。
@DataClassName('Category')
class Categories extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get description => text()();
}

// DriftDatabaseに上記の2テーブルを指定して、Driftで扱えるよう設定する。
// 今のタイミングではMyDatabeseクラスは空で良い。
// "_$MyDatabase"が存在しないと怒られるが気にしない。
@DriftDatabase(tables: [Todos, Categories])
class MyDatabase extends _$MyDatabase {
}
```

# 3.ビルドする
## 3.1 ビルド
以下のコマンドを実行すると2で書いたテーブル定義をもとに"database.g.dart"を作成してくれます。
テーブル定義に手を加えたときは、毎回このコマンドを実行して"database.g.dart"を更新する必要があります。

<font color="#DDDDDD">筆者は公式ドキュメントをよく読んでなくてここでちょっとつまづきました...。反省...。</font>
``` ターミナル
flutter pub run build_runner build
```

テーブル定義を何回か修正する場合は、以下のコマンドを使用することで変更を追跡して勝手にビルドしてくれます
```
flutter pub run build_runner watch
```

## 3.2 ビルド後の初期設定
"database.dart"内で定義した"MyDatabase"を以下のように変更します。

``` dart:database.dart
// 初期設定を定義するために以下のインポートを追加
import 'dart:io';

import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;

@DriftDatabase(tables: [Todos, Categories])
class MyDatabase extends _$MyDatabase {
  // コンストラクタでDBの保存先を定義する
  MyDatabase() : super(_openConnection());

  // マイグレーションを行う場合に使用ため、テーブルを追加／変更／削除する場合はこの値をインクリメントする
  @override
  int get schemaVersion => 1;
}

LazyDatabase _openConnection() {
  // the LazyDatabase util lets us find the right location for the file async.
  return LazyDatabase(() async {
    // PCの場合はドキュメントフォルダに"db.sqlite"が作成される
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'db.sqlite'));
    return NativeDatabase.createInBackground(file);
  });
}
```

# 4.実際に使用する
お試しでDBにデータを登録して取得する処理を作りたいので、main.dartのincrementボタンを流用します。
main.dart を以下のように変更します。
アプリ立ち上げ後に、incrementボタンを押したらコンソール上に
```Categories in database: [Category(id: 1, description: my first category)]```
みたいな文章がprintされればOKです。

``` diff_Dart:main.dart
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

+  Future<void> _testDrift() async {
+    final database = MyDatabase();
+
+    // Simple insert:
+    await database
+        .into(database.categories)
+        .insert(CategoriesCompanion.insert(description: 'my first category'));
+
+    // Simple select:
+    final allCategories = await database.select(database.categories).get();
+    print('Categories in database: $allCategories');
+  }

//  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

      floatingActionButton: FloatingActionButton(
-        onPressed: _incrementCounter,
+        onPressed: _testDrift,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
}
```

# おまけ（TodosとCategoriesをForeignKeyでつなぐ）
今のままだとTodosとCategoriesの関係がわかりにくいのと、SQLiteのForeignKey機能を利用したい場合は以下のコードのように変更すればよいです。

https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/#references
```diff_dart:database.dart
class Todos extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text().withLength(min: 6, max: 32)();
  TextColumn get content => text().named('body')();
- IntColumn get category => integer().nullable()();
+ // categoyにForeignKeyを設定
+ IntColumn get category => integer().nullable().references(Categories, #id)();
}

@DriftDatabase(tables: [Movies, WatchLists], daos: [MoviesDao, WatchListsDao])
class MyDatabase extends _$MyDatabase {

+@override
+  MigrationStrategy get migration {
+    return MigrationStrategy(
+        onCreate: (Migrator m) async {
+          await m.createAll();
+        },
+        onUpgrade: (Migrator m, int from, int to) async {},
+        beforeOpen: (details) async {
+          if (details.wasCreated) {
+            // ...
+          }
+          // デフォルトではForeignKeyの設定がOFFになっているためONにする
+          await customStatement('PRAGMA foreign_keys = ON');
+        });
+  }
}
```

# Driftを使って見たメモ
Driftを使ってみたときのメモです。よかったらこちらも参考にしてください。
## テスト編
https://qiita.com/team7_403_poppin/items/8877cb75c320c793bc8f