57
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Moorの使い方

Last updated at Posted at 2020-01-02

Flutterで使えるデータベースライブラリのMoorが便利すぎてやばかったので使い方を調べてみました。

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

%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をもう少し書き足します。

%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_providerpathの依存関係は必要なくなります。

データベースを使用する

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

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

%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クラスを返します。このQueryget()を呼ぶと結果がFutureで返ります。Querywatch()を呼ぶとStreamを取得することができます。このStreamは、最初に一度結果を返した後、データベースに変更が行われたタイミングで更新されたデータを送ってきます。StreamBuilderと組み合わせると便利です。

Create

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

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

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

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

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

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

Read

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

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

Update

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

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

Delete

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

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

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

その他いろいろ

公式のドキュメントには入門方法しか記載されておらず、ちょっと凝ったことをやろうとするとどうすればいいの?って感じでした。GitHubのissueとか見ながらあれこれ調べたものです。

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

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

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ファイルを使う方法に移行した方が良さそうです。使い方はまだ調べてないのでそのうち…

@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も返すことができます。上のコードは下のように書き換えることも可能です。

@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

を実行します。

57
45
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?