Select
このページでは、DriftのDart APIを使用したSELECT
文の記述方法について説明します。
例をわかりやすくするために、Todoリストアプリのベースとなる2つの一般的なテーブルを参照しています:
class TodoItems 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().references(Categories, #id)();
}
('Category')
class Categories extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
データベースクラスの@DriftDatabase
アノテーションで指定した各テーブルに対応するゲッターが生成されます。
このゲッターを使用してステートメントを実行できます。
(tables: [TodoItems, Categories])
class MyDatabase extends _$MyDatabase {
// 前のページのスキーマバージョンのゲッターとコンストラクタは省略
// すべてのTodoエントリを取得する
Future<List<TodoItem>> get allTodoItems => select(todoItems).get();
// 指定されたカテゴリのすべてのTodoエントリを監視する
// Streamはデータが変更されるたびに、自動的に新しいアイテムを発行する
Stream<List<TodoItem>> watchEntriesInCategory(Category c) {
return (select(todos)..where((t) => t.category.equals(c.id))).watch();
}
}
Driftはクエリを簡単かつ安全に書くことができます。
このページでは、基本的なselectクエリの書き方だけでなく、join
やsubquery
を使った高度なクエリの書き方についても説明します。
シンプルなSelect
select
文を作成するにはselect(tableName)
を使用します。
データベースで使用される各テーブルには、クエリを実行するための一致するフィールドがあります。
どのクエリもget()
で一度だけ実行することもできますし、watch()
で自動更新Streamにすることもできます。
Where
Where()
をコールすることで、クエリにフィルタを適用できます。where
メソッドは、与えられたテーブルをboolean
の式にマップする関数を受け取ります。
このような式を作成する一般的な方法は、式のequals
を使用することです。
整数カラムは、isBiggerThan
やisSmallerThan
で比較することもできます。
a & b
、a | b
、a.not()
を使って式を組み合わせることができます。
式の詳細については、こちらを参照してください。
リミット
クエリに対してlimit
をコールすると、返される結果の量を制限することができます。
このメソッドには、返す行数とオプションのオフセットを指定します。
Future<List<TodoItem>> limitTodos(int limit, {int? offset}) {
return (select(todoItems)..limit(limit, offset: offset)).get();
}
順序付け
select
ステートメントにorderBy
メソッドを使用することができます。
これは、テーブルから個々の順序付けを抽出する関数のリストを期待しています。
順序付けとして任意の式を使用することができます。詳細については、こちらをご覧ください。
Future<List<TodoItem>> sortEntriesAlphabetically() {
return (select(todoItems)
..orderBy([(t) => OrderingTerm(expression: t.title)]))
.get();
}
OrderingTerm
プロパティをOrderingMode.desc
に設定することで、順序を逆にすることもできます。
単一の値
クエリが複数の行を返さないことが分かっている場合、結果をList
ラップするのは面倒です。
Driftでは、getSingle
とwatchSingle
でそれを回避することができます:
Stream<TodoItem> entryById(int id) {
return (select(todoItems)..where((t) => t.id.equals(id))).watchSingle();
}
指定されたId
を持つエントリが存在する場合、そのエントリはStreamに送られます。
そうしない場合は、Streamにnullが追加されます。
watchSingle
を使用したクエリが複数のエントリを返した場合(この場合はありえません)、代わりにエラーが追加されます。
マッピング
watch
やget
(あるいはその単一型)を呼び出す前に、mapを使って結果を変換することができます。
Stream<List<String>> contentWithLongTitles() {
final query = select(todoItems)
..where((t) => t.title.length.isBiggerOrEqualValue(16));
return query.map((row) => row.content).watch();
}
Deferring getとwatchの比較
クエリをFuture
またはStream
として処理したい場合は、Selectable
抽象クラスを使用して、戻り値の型を絞り込むことができます:
// get` と `watch` を公開
MultiSelectable<TodoItem> pageOfTodos(int page, {int pageSize = 10}) {
return select(todoItems)..limit(pageSize, offset: page);
}
// `getSingle` と `watchSingle` を公開
SingleSelectable<TodoItem> selectableEntryById(int id) {
return select(todoItems)..where((t) => t.id.equals(id));
}
// `getSingleOrNull` と `watchSingleOrNull` を公開
SingleOrNullSelectable<TodoItem> entryFromExternalLink(int id) {
return select(todoItems)..where((t) => t.id.equals(id));
}
これらの基本クラスは、クエリ構築メソッドやmap
メソッドを持ちません。
結合
Driftは、複数のテーブルを操作するクエリを記述するためのSQL結合をサポートしています。
この機能を使用するには、select(table)
で通常のselect文を開始し、.join()
で結合のリストを追加します。
内側joinと左外側joinは、ON
式を指定する必要があります。
// Todoエントリーと関連するかとカテゴリの両方を含むデータクラスを定義する
class EntryWithCategory {
EntryWithCategory(this.entry, this.category);
final TodoItem entry;
final Category? category;
}
// データベースクラスで、各エントリのカテゴリをロードすることができます
Stream<List<EntryWithCategory>> entriesWithCategory() {
final query = select(todoItems).join([
leftOuterJoin(categories, categories.id.equalsExp(todoItems.category)),
]);
// 結果の解析方法については、次のセクションを参照
}
もちろん、複数のテーブルを結合することもできます:
/// `titleQuery`をタイトルに含むTodo項目と同じカテゴリにあるTodoエントリーを検索します。
Future<List<TodoItem>> otherTodosInSameCategory(String titleQuery) async {
// タイトルでフィルタリングするために、同じカテゴリ内の他のtodosを見つけるためにも
// 同じテーブルを2回追加しているため、2つのテーブルを区別する方法が必要です。
// そのため、一方には特別な名前を付けています:
final otherTodos = alias(todoItems, 'inCategory');
final query = select(otherTodos).join([
// joinでは、`useColumns: false`はドリフトに結合されたテーブルのカラムを
// 結果セットに追加しないよう指示します。ここでは、where句で参照できるように
// テーブルを結合するだけなので、これが役立ちます。
innerJoin(categories, categories.id.equalsExp(otherTodos.category),
useColumns: false),
innerJoin(todoItems, todoItems.category.equalsExp(categories.id),
useColumns: false),
])
..where(todoItems.title.contains(titleQuery));
return query.map((row) => row.readTable(otherTodos)).get();
}
解析結果
joinを持つselect文でget()
またはwatch
を呼び出すと、それぞれList<TypedResult>
のFuture
またはStream
が返されます。
各TypedResult
はデータを読み込めることができる行を表します。これは生の列を得るためのrawData
ゲッターを含んでいます。
しかし、より重要なことは、readTable
メソッドを使用してテーブルからデータクラスを読み取ることができるということです。
上記のクエリ例では、各行からTodoエントリとカテゴリを次のように読み取ることができます:
return query.watch().map((rows) {
return rows.map((row) {
return EntryWithCategory(
row.readTable(todoItems),
row.readTableOrNull(categories),
);
}).toList();
});
テーブルが行に存在しない場合、readTable
はArgumentError
をスローします。例えば、Todoエントリはどのカテゴリにも存在しないかもしれません。
それを考慮し、row.readTableOrNull
を使用してカテゴリを読み込みます。
カスタムコラム
slect文はテーブルのカラムに限定されません。より複雑な式をクエリに含めることもできます。 結果の各行に対して、これらの式がデータベースエンジンによって評価されます。
Future<List<(TodoItem, bool)>> loadEntries() {
// エントリーの内容のどこかに "important "という文字列があれば、そのエントリーは重要であるとみなす
final isImportant = todoItems.content.like('%important%');
return select(todoItems).addColumns([isImportant]).map((row) {
final entry = row.readTable(todoItems);
final entryIsImportant = row.read(isImportant)!;
return (entry, entryIsImportant);
}).get();
}
like
チェックはDartでは実行されず、すべての行について効率的に計算できる基礎となるデータベースエンジンに送られることに注意してください。
エイリアス
クエリがテーブルを複数回参照することがあります。次の例では、ナビゲーション・システム用に保存されたルートを保存しています:
class GeoPoints extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get latitude => text()();
TextColumn get longitude => text()();
}
class Routes extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
// contains the id for the start and destination geopoint.
IntColumn get start => integer()();
IntColumn get destination => integer()();
}
ここで、各ルートの出発地と目的地のジオポイントオブジェクトもロードしたいとします。
この場合、geo-points
テーブルのjoinを2回使う必要があります。これをクエリで表現するにはエイリアスを使います:
class RouteWithPoints {
final Route route;
final GeoPoint start;
final GeoPoint destination;
RouteWithPoints({this.route, this.start, this.destination});
}
// データベースクラスの中:
Future<List<RouteWithPoints>> loadRoutes() async {
final start = alias(geoPoints, 's');
final destination = alias(geoPoints, 'd');
final rows = await select(routes).join([
innerJoin(start, start.id.equalsExp(routes.start)),
innerJoin(destination, destination.id.equalsExp(routes.destination)),
]).get();
return rows.map((resultRow) {
return RouteWithPoints(
route: resultRow.readTable(routes),
start: resultRow.readTable(start),
destination: resultRow.readTable(destination),
);
}).toList();
}
生成されたステートメントは次のようになります:
SELECT
routes.id, routes.name, routes.start, routes.destination,
s.id, s.name, s.latitude, s.longitude,
d.id, d.name, d.latitude, d.longitude
FROM routes
INNER JOIN geo_points s ON s.id = routes.start
INNER JOIN geo_points d ON d.id = routes.destination