Moorの使い方

Flutterで使えるデータベースライブラリの[Moor](https://moor.simonbinder.eu/)が便利すぎてやばかったので使い方を調べてみました。

MoorはSQLiteを使用するデータ永続化用のライブラリで、AndroidのRoomのような型安全でシンプルなAPIで高機能なORMマッピング機能を提供します。Mapのような汎用型ではなくテーブルをモデル化したデータクラスを使ってデータベースの読み書きができるので、カラム名のスペルミスなどをコンパイル時に検出できるためミスが少なくなります。
そして`Stream`や`Future`といった標準的な非同期処理APIを使ってデータの取得や変更の監視が行えるので、`FutureBuilder`や`StreamBuilder`を使うことでデータベースの値を表示するUIを簡単に構築することができます。

## 前提

Flutterでアプリケーションを開発したことがある人。

## インストール

pubspec.yamlに以下のように依存関係を宣言します。（2020年1月2日時点の最新バージョンです）

```
dependencies:
  moor: ^2.2.0
  moor_ffi: ^0.3.1
  path_provider: ^1.5.1
  path: ^1.6.4

dev_dependencies:
  moor_generator: ^2.2.0
  build_runner: ^1.7.2
```

## テーブル定義

Todos、Categoriesというテーブルを持つデータベースを定義するファイルは以下のようになります。これを`db.dart`という名前で保存します。

```dart:%PROJECT_ROOT%/lib/db.dart
import 'package:moor/moor.dart';

part 'db.g.dart';

class Todos extends Table {
  IntColumn get id => integer().autoIncrement()();              // 自動採番id
  TextColumn get title => text().withLength(min: 6, max: 32)(); // 制約をつける
  TextColumn get content => text().named('body')();             // カラムに別名をつける
  IntColumn get category => integer().nullable()();             // nullableにする（デフォルトではnull不可）
}

@DataClassName("Category")
class Categories extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get description => text()();
}

@UseMoor(tables: [Todos, Categories])
class MyDatabase {
  
}
```

ここまで書けたら一旦次のコマンドを実行します。

```
flutter packages pub run build_runner build
```

すると、`db.g.dart`というファイルが生成されます。この中に`Todo`,`Category`といったレコード単位のデータを表すクラスや、データベース操作のためのヘルパークラスなどが含まれています。手で書くと数百行にもなるプログラムを自動で生成してくれるわけですから大したものです。

`db.g.dart`ファイルができたら、先程の`db.dart`をもう少し書き足します。

```dart:%PROJECT_ROOT%/lib/db.dart
import 'dart:io';

import 'package:moor/moor.dart';
import 'package:moor_ffi/moor_ffi.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;

// --- ここから
part 'db.g.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()();
}

@DataClassName("Category")
class Categories extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get description => text()();
}
// --- ここまでは一緒

LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'db.sqlite'));
    return VmDatabase(file);
  });
}

@UseMoor(tables: [Todos, Categories])
class MyDatabase extends _$MyDatabase {
  MyDatabase() : super(_openConnection());

  @override
  int get schemaVersion => 1;
}
```

`MyDatabase`を`_$MyDatabase`というクラスを継承するように書き換えました。このクラスは`db.g.dart`の中で定義されており、データベース関連の面倒なあらゆる処理を行ってくれるやつです。`_openConnection()`ではデータベースファイルのパスを指定してSqliteデータベースをどこに作成するのかを決めています。
ファイルに永続化せずインメモリで処理するデータベースを使いたい場合は`return VmDatabase.memory()`とすることも可能です。その場合は`getApplicationDocumentsDirectory`とかを呼ぶ必要はないので`path_provider`や`path`の依存関係は必要なくなります。

## データベースを使用する

ここまでできたら`MyDatabase`を適当なタイミングでインスタンス化して`select`や`update`などのメソッドを呼ぶことでデータベース操作を行うことが可能ですが、これらのメソッドは`@protected`アノテーションが付いているため継承したクラスからのみ使用することを意図して作られています。なので、ラッパーメソッドを用意します。

たとえば以下のような感じで

```dart:%PROJECT_ROOT%/lib/db.dart
@UseMoor(tables: [Todos, Categories])
class MyDatabase extends _$MyDatabase {  
  // schemaVersion とコンストラクタは省略
  
  /// 全てのTodoを取得する
  Future<List<Todo>> get allTodoEntries => select(todos).get();

  /// Categoryに紐付く全てのTodoを取得する。Streamはデータベースの変更を監視し、変更が行われたら新しい結果を返す。
  Stream<List<Todo>> watchEntriesInCategory(Category c) {
    return (select(todos)..where((t) => t.category.equals(c.id))).watch();
  }

  /// [id]のTodoを取得する。Streamはデータベースの変更を監視し、変更が行われたら新しい結果を返す。
  Stream<Todo> todoById(int id) {
    return (select(todos)..where((t) => t.id.equals(id))).watchSingle();
  }
}
```

上の中に現れる`todos`というプロパティは親クラス`_$MyDatabase`にあるもので、Moorが自動生成します。`select(todos)`という記述は`SELECT * FROM todos`というSQLに対応します。
`select()`メソッドは`Query`クラスを返します。この`Query`の`get()`を呼ぶと結果が`Future`で返ります。`Query`の`watch()`を呼ぶと`Stream`を取得することができます。この`Stream`は、最初に一度結果を返した後、データベースに変更が行われたタイミングで更新されたデータを送ってきます。`StreamBuilder`と組み合わせると便利です。

### Create

テーブルにデータを追加する（INSERT文）には、以下のようなメソッドを用意します。

```dart
/// 生成されたidを返す
Future<int> addTodoEntry(TodosCompanion entry) {
  return into(todos).insert(entry);
}
```

`TodosCompanion`というクラスが登場しました。これは`db.g.dart`の中に生成されたクラスです。
`Todo`クラスも生成されているのに、なぜ`TodosCompanion`が必要なのでしょう？それは`Todo`は実際のテーブル内の1行を表すクラスであるからです。

テーブルにデータを追加するときは、今回の例のように自動採番(autoincrement)なidを使ったり、テーブルで指定したデフォルト値を使用したりすることがあるため、必ずしも全てのカラムを指定するわけではありません。`Todo`クラスを使うと、**そのカラムが省略されているのかnullが指定されているのか**を判別することができません。「カラムを省略して追加する」という概念を表すために`TodosCompanion`が必要なのです。

上のメソッドは、以下のようにして使います。

```dart
addTodoEntry(
  TodosCompanion(
    title: Value('Important task'),
    content: Value('Refactor persistence code'),
  ),
);
```

### Read

データを読み取るには、先に紹介した例のように`select()`メソッドを使います。

```dart
Future<List<Todo>> getAllTodoEntries() {
  return select(todos).get();
}
```

### Update

insertのときと同じように、update時にも`TodosCompanion`を使用します。理由は同じくupdate時にも一部のカラムだけを更新することがあるためです。

```dart
/// 変更された行数を返す
Future<int> updateTodo(int id, TodosCompanion todo) {
  return (update(todos)..where((it) => it.id.equals(id))).write(todo);
}
```

### Delete

deleteする時は`delete`メソッドを使用します。

```dart
/// 変更された行数を返す
Future<int> deleteTodo(int id) {
  return (delete(todos)..where((it) => it.id.equals(id))).go();
}
```

---

ここまでできれば、基本的なデータ操作はバッチリですね！

## その他いろいろ

[公式のドキュメント](https://moor.simonbinder.eu/docs/)には入門方法しか記載されておらず、ちょっと凝ったことをやろうとするとどうすればいいの？って感じでした。GitHubのissueとか見ながらあれこれ調べたものです。

### プライマリーキーを設定する

チュートリアルにあるような`IntColumn get id => integer().autoIncrement()();`は自動採番のプライマリーキーを作りますが、明示的にプライマリーキーを指定したい場合や複合プライマリーキーを使用したい場合は、テーブルクラスで`primaryKey`をオーバーライドして、プライマリーキーとなるカラムのセットを返すようにします。

```dart:nameをプライマリーキーにしたい
class MyData extends Table {
  TextColumn get name => text()();
  TextColumn get description => text()();

  @override
  Set<Column> get primaryKey => {name};
}
```

### countクエリを書く

`@UseMoor`アノテーションに`queries`を指定すると、データベースクラスにカスタムクエリの結果を返すメソッドを自動生成することができます。

**2020/03/23追記: `queries` の指定はlegacy扱いの機能らしいので、.moorファイルを使う方法に移行した方が良さそうです。使い方はまだ調べてないのでそのうち…**

```dart
@UseMoor(
  tables: [MyData],
  queries: {
    '_countMyData': 'SELECT COUNT(*) FROM myData',
  },
)
class MyDatabase extends _$MyDatabase {
  Future<int> get count => _countMyData().then((result) => result.first);
}
```

上の例では`_countMyData`というメソッドを生成しています。生成されるメソッドは`Future<List<int>>`を返すのですが、集計クエリは常に一件の結果を返すのでラッパーメソッドを使って先頭の一件だけを返すようにしています。

より複雑な処理をするために`_countMyDataQuery`というメソッドも同時に生成されます。これを使うと`Future`だけでなく`Stream`も返すことができます。上のコードは下のように書き換えることも可能です。

```dart
@UseMoor(
  tables: [MyData],
  queries: {
    '_countMyData': 'SELECT COUNT(*) FROM myData',
  },
)
class MyDatabase extends _$MyDatabase {
  Future<int> get count => _countMyDataQuery().getSingle();
  Stream<int> get watchCount => _countMyDataQuery().watchSingle();
}
```

### コード生成に失敗した時

`flutter packages pub run build_runner build`を実行したことがある（.g.dartファイルがすでに存在している）状態でもう一度これを実行すると、エラーになることがあります。そういう時は`--delete-conflicting-outputs`オプションを付けて実行すると直ることがあります。

```
flutter packages pub run build_runner build --delete-conflicting-outputs
```

pubspec.yamlを編集した後に`flutter packages pub run build_runner build`が失敗することもあります。そういうときは

```
flutter pub get
```

を実行します。