Skip to main content

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クエリの書き方だけでなく、joinsubqueryを使った高度なクエリの書き方についても説明します。

シンプルなSelect

select文を作成するにはselect(tableName)を使用します。 データベースで使用される各テーブルには、クエリを実行するための一致するフィールドがあります。 どのクエリもget()で一度だけ実行することもできますし、watch()で自動更新Streamにすることもできます。

Where

Where()をコールすることで、クエリにフィルタを適用できます。whereメソッドは、与えられたテーブルをbooleanの式にマップする関数を受け取ります。 このような式を作成する一般的な方法は、式のequalsを使用することです。 整数カラムは、isBiggerThanisSmallerThanで比較することもできます。 a & ba | ba.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では、getSinglewatchSingleでそれを回避することができます:

Stream<TodoItem> entryById(int id) {
return (select(todoItems)..where((t) => t.id.equals(id))).watchSingle();
}

指定されたIdを持つエントリが存在する場合、そのエントリはStreamに送られます。 そうしない場合は、Streamにnullが追加されます。 watchSingleを使用したクエリが複数のエントリを返した場合(この場合はありえません)、代わりにエラーが追加されます。

マッピング

watchget(あるいはその単一型)を呼び出す前に、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();
});
note

テーブルが行に存在しない場合、readTableArgumentErrorをスローします。例えば、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();
}
note

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

結合のORDER BYWHERE