Dartテーブル
リレーショナルデータベースでは、テーブルを使用して行の構造を記述します。 事前に定義されたスキーマに従うことで、Driftはデータベースに対して型安全なコードを生成できます。 セットアップページですでに示したように、DriftはDartでテーブルを宣言するためのAPIを提供します:
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()();
}
このページでは、テーブルのDSLについて詳しく説明します。
カラム
各テーブルでじゃ、カラムの型、Dartでの名前、SQLにマッピングされた定義で始まるゲッターを宣言することでカラムを定義します。
上の例では、IntColumn get category => integer().nullable()();で、categoryというう名前のnullableな整数値を保持するカラムを定義しています。
このセクションでは、カラムを宣言する際に使用できるすべてのオプションについて説明します。
サポートされているカラムの型
Driftはさまざまなカラムの型をサポートしています。型変換を使用することで、独自の型をカラムに格納することもできます。
| Dartの型 | カラム | 対応するSQLiteの型 |
|---|---|---|
int | integer() | INTEGER |
BigInt | int64() | INTEGER (ウェブ上での大きな値に便利) |
double | real() | REAL |
boolean | boolean() | INTEGER, 0または1のみを許可するCHECKが必要 |
String | text() | TEXT |
DateTime | dateTime() | オプションによってはINTEGER(デフォルト)またはTEXT |
Uint8List | blob() | BLOB |
Enum | intEnum() | INTEGER (詳細はこちら) |
Enum | textEnum() | TEXT (詳細はこちら) |
boolean、dateTime、型変換のマッピングは、レコードをデータベースに格納する際にのみ適用されることに注意してください。
これらはJSON Serializationには全く影響しません。
例えば、boolean値はデータベース内では0または1として保存されますが、fromJsonファクトリではtrueまたはfalseとして期待されます。
JSON用のカスタムリマッピングをしたい場合は、独自のValueSerializerを提供する必要があります。
独自のカラム型
SQLite3がサポートする型には制約がありますが、任意のDart型をSQLに格納するための型変換をサポートしています。
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get preferences =>
text().map(const PreferenceConverter()).nullable()();
}
型変換の詳細については、このドキュメントの型変換のページを参照してください。
BigIntサポート
Driftはint64()カラムをサポートし、カラムが大きな整数を格納し、BigIntとしてDartにマッピングされることを示します。
これは主にDartアプリがJavaScriptにコンパイルされた場合に役立ちます。
ここではintは情報を失うことなく大きな整数を格納できないdoubleです。
この場合、整数をBigIntとして表現し(それを基盤となるデータベース実装にわたすことで)、精度の損失なしに大きな整数を格納することができます。
BigIntはIntよりもオーバーヘッドが高いことに注意してください。
したがって、以下の必要があるカラムにのみint64()の使用を推奨します:
SQLite3では、INTEGERカラムは64ビット整数として格納されます。
ウェブ以外のすべてで動作するDart VMでアプリを実行している場合、Dart内のint型は64bit整数であるため、完璧に一致します。
これらのアプリに対しては、通常のinteger()カラムの使用を推奨します。
基本的に、以下の両方が真である場合にint64()を使用すべきです:
- ウェブ上で機能するアプリを駆逐している場合
- 該当するカラムが より大きな値を格納する可能性がある場合
その他のすべてのケースでは、通常のinteger()カラムを使用するのが効率的です。
BigIntをDriftで使用する際のポイントをいくつか紹介します:
- SQLite3では、
integer()とint64()は同じ列タイプなので、スキーマ移行を書かずに2つの間で切り替えることができます。 - 大きなカラムだけでなく、
BigIntとして表現したほうが良い複雑な式がSelect文に含まれている可能性もあります。 これにはdartCast()を使用できます。(table.columnA * table.columnB).dartCast<BigInt>()に対して、DriftはcolumnAとcolumnBが通常の整数として定義されていたとしても、結果の値をBigIntとして報告します。 BigIntは現在、moor_flutterおよびdrift_sqfliteには対応していません。- WebDatabaseで
BigIntサポートを使用するには、インスタンス化する際にreadIntsAsBigInt: trueフラグを設定します。 NativeDatabaseとWasmDatabaseの両方にはBigIntのサポートが組み込まれています。
DataTimeオプション
DriftはSQLにDataTime値を格納する2つのアプローチをサポートしています:
-
UNIXタイムスタンプ(デフォルト): このモードでは、DriftはUNIXタイムスタンプ(秒単位)を含むSQL
INTEGERとして日付時刻の値を格納します。 SQLからDartに日付時刻がマッピングされると、Driftは常にUTCでない値を返します。そのため、UTCの日付時刻が格納されている場合でも、行を取得する際にこの情報は失われます。 -
ISO 8601文字列: このモードでは、
DataTime.toIso8601String()に基づくテキスト形式で日付時刻の値が格納されます。 UTC値は変更されずに保存され(例えば2022-07-25 09:28:42.015Z)、ローカル値にはUTCオフセットが付加されます(例えば2022-07-25T11:28:42.015 +02:00)。 SQLite3の日付と時刻関数のほとんどはUTC値を操作しますが、SQLで日付時刻を構文解析すると、UTCオフセットが値に追加されることを尊重します。
データベースから値を読み戻すとき、DriftはDateTime.parseを以下のように使用します:- テキスト値の末尾が
Zの場合、DriftはDateTime.parseを直接使用します。接尾辞Zが認識され、UTC値が返されます。 - テキスト値の末尾がUTCオフセット(
+02:00など)である場合、DriftはまずDateTime.parseを使用し、修飾子を尊重しながらUTC日時を返します。その後、Driftはこの中間結果に対してtoLocal()を呼び出し、ローカル値を返します。
この動作は、SQLite3の日付関数とうまく連動し、保存された値の "UTCらしさ"も保つことができます。
- テキスト値の末尾が
このモードはstore_date_time_values_as_textビルドオプションで変更できます。
使用するオプションに関係なく、Driftの組み込みの日付・時刻関数のサポートは同等の値を返します。
Driftは日付関数を動作させるためにUNIXタイムスタンプを使用する場合、内部的にunixepoch修飾子を挿入します。
テキストとして保存された日付を比較する場合、Driftはjuliandayの値を裏で比較します。
2つのモード間のマイグレーション
Dartで日付時刻モードを変更するのは、ビルドオプションを変えるだけで簡単ですが、この挙動を切り替えることは既存のデータベーススキーマと互換性がありません:
-
ビルドオプションによって、Driftは日付時刻値に文字列または整数を期待します。そのため、オプションを変更する際には、保存されている列を新しい形式に移行する必要があります。
-
.driftファイルで定義されたSQL文を使用している場合、ランタイムでカスタムSQLを使用するか、高レベルの日付時刻APIを使用する代わりに、直接FunctionCallExpressionで日付時刻式を手動で呼び出す必要がある場合、これらの使用法を適応させる必要があります。例えば、比較演算子
<はUNIXタイムスタンプに対して機能しますが、テキスト形式の日付時刻値を辞書順に比較します。したがって、使用しているモードに応じて、値をunixepochやjuliandayでラップして比較可能にする必要があります。
2番目のポイントはアプリケーションの使用法に特有のものなので、このドキュメントでは保存された列を形式間で移行する方法のみを説明します:
デフォルトで生成されるJSONシリアライゼーションは選択した日付時刻モードに影響されません。デフォルトでは、DriftはDateTime値をミリ秒単位のUNIXタイムスタンプにシリアライズします。
これを変更するには、ValueSerializer.defaults(serializeDateTimeValueAsString: true)を作成し、それをdriftRuntimeOptiuons.defaultSerializerに割り当てます。
UNIXタイムスタンプからテキストへのマイグレーション
タイムスタンプ(デフォルトオプション)からテキストとして日付時刻を保存する形式に移行するには、以下の手順に従ってください:
store_date_time_values_as_textビルドオプションを有効にします。- 以下のメソッド(またはニーズに合わせたバージョン)をデータベースクラスに追加します。
- データベースクラスで
schemaVersionを増分します。 onUpgrade内でこのスキーマバージョンの増加に対して、migrateFromUnixTimestampsToTextを呼び出すマイグレーションステップを記述します。 トリガー、ビュー、またはデータベース内のその他のカスタムSQLエントリには、このガイドではカバーされていないカスタムマイグレーションが必要です。
Future<void> migrateFromUnixTimestampsToText(Migrator m) async {
for (final table in allTables) {
final dateTimeColumns =
table.$columns.where((c) => c.type == DriftSqlType.dateTime);
if (dateTimeColumns.isNotEmpty) {
// このテーブルには、移行が必要な dateTime カラムがあります
await m.alterTable(TableMigration(
table,
columnTransformer: {
for (final column in dateTimeColumns)
// データベース内のカラムはint型(unixタイムスタンプ)と仮定し、
// `fromUnixEpoch`を使用して日時に変換します。
// データベース内の結果値はUTCであることに注意してください。
column: DateTimeExpressions.fromUnixEpoch(column.dartCast<int>()),
},
));
}
}
}
テキストからUNIXタイムスタンプへのマイグレーション
テキストとして保存された日時からUNIXタイムスタンプへの移行には、以下の手順に従ってください:
store_date_time_values_as_textビルドオプションを無効にします。- 以下のメソッド(あなたのニーズに合わせて適応)をデータベースクラスに追加します。
- データベースクラスの
schemaVersionをインクリメントします onUpgrade内でこのスキーマバージョン増加のためにmigrateFromTextDateTimesToUnixTimestampsを呼び出すマイグレーションステップを記述します。 データベース内のトリガー、ビュー、またはその他のカスタムSQLエントリは、このガイドではカバーされていないカスタムマイグレーションが必要です。
Future<void> migrateFromTextDateTimesToUnixTimestamps(Migrator m) async {
for (final table in allTables) {
final dateTimeColumns =
table.$columns.where((c) => c.type == DriftSqlType.dateTime);
if (dateTimeColumns.isNotEmpty) {
// このテーブルには、移行が必要な dateTime カラムがあります
await m.alterTable(TableMigration(
table,
columnTransformer: {
for (final column in dateTimeColumns)
// データベースのカラムが文字列であると仮定します。
// 私たちはそれをSQLで日付にパースし、そのunixタイムスタンプを取得します。
// これにはsqliteのバージョン3.38以上が必要であることに注意してください。
column: FunctionCallExpression('unixepoch', [column]),
},
));
}
}
}
このスニペットでは、SQLite 3.38で追加されたunixepochというSQLite3関数を使用しています。
古いバージョンのSQLite3をサポートするには、代わりにstrftimeを使用し、整数にキャストすることができます。
columnTransformer: {
for (final column in dateTimeColumns)
// `unixepoch`の代わりに使用:
column: FunctionCallExpression(
'strftime', [const Constant('%s'), column]).cast<int>(),
},
splite3_flutter_libsパッケージに最近依存したNativeDatabaseを使用している場合、unixepochをサポートした最新のSQLite3バージョンを使用していると考えて問題ありません。
Nullable
Driftは、デフォルトでnon-nullableのDartの慣習に従います。
これは、Dartで定義されたテーブル上で宣言されたカラムは、デフォルトではnull値を格納することができず、SQLではNOT NULL制約として生成されることを意味します。
挿入時に値を設定し忘れると例外が投げられます。
SQLを使用する場合、Driftはコンパイル時にそれについても警告します。
カラムをnullableにしたい場合は、単にnullable()を使用します:
class Items extends Table {
IntColumn get category => integer().nullable()();
// ...
}
参照
外部キー参照は、Dartテーブルでカラムを構築する際に、refarences()メソッドで表すことができます:
class TodoItems extends Table {
// ...
IntColumn get category =>
integer().nullable().references(TodoCategories, #id)();
}
("Category")
class TodoCategories extends Table {
IntColumn get id => integer().autoIncrement()();
// and more columns...
}
referencesの最初のパラメータは、参照を作成するテーブルを指します。
2番目のパラメータは、参照に使用するカラムのシンボルです。
オプションで、onUpdateとonDeleteパラメータを使用すると、対象の行が更新または削除されたときの動作を記述できます。
SQLite3では、外部キー参照はデフォルトでは有効になっていないことに注意してください。
PRAGMA foreign_keys = ONで有効にする必要があります。
この設定をDriftで発行するのに適した場所は、マイグレーション後のコールバックです。
デフォルト値
カラムにデフォルト値を設定することができます。
明示的に設定しない場合は、新しい行を挿入する際にデフォルト値が使用されます。
一定のデフォルト値を設定するには、withDefaultを使用します:
class Preferences extends Table {
TextColumn get name => text()();
BoolColumn get enabled => boolean().withDefault(const Constant(false))();
}
後でinto(preferences).insert(PreferencesCompanion.forInsert(name:'foo'));を使用すると、新しい行のenabledカラムはfalseに設定されます(通常はnullではありません)。
デフォルト値があるカラム(autoIncrementを使うか、デフォルト値を使用する)は、生成されたデータクラスでも@requiredとしてマークされていることに注意してください。
これは、完全な行を表すためであり、すべての行には値が存在するためです。挿入や更新のような部分的な行を表す場合にはコンパニオンを使用してください。
もちろん、定数は静的な値にのみ使用できます。
しかし、各カラムに対して動的なデフォルト値を生成したい場合はどうでしょうか?
そのためには、clientDefaultを使用できます。
これは、望ましいデフォルト値を返す関数を取ります。
この関数は各挿入時に呼び出されます。
例えば、uuidパッケージを使用してランダムなUuidを生成する例をこちらに示します:
final _uuid = Uuid();
class Users extends Table {
TextColumn get id => text().clientDefault(() => _uuid.v4())();
// ...
}
どれをいつ使えばいいのかわからない?
デフォルト値が定数である場合や、currentDateのような単純な値の場合はwithDefaultを使うことをお勧めします。
ランダムに生成されたIdのような複雑な値の場合はclientDefaultを使う必要があります。内部的には、withDefaultはデフォルト値をCREATE TABLE文に書き込みます。
これはより効率的ですが、動的な値をサポートしていません。
チェック
ある列(または行)が特定の値しか含まないことが分かっている場合、SQLのCHECK制約を使用してデータにカスタム制約を強制することができます。
Dartでは、カラムビルダーのcheckメソッドは生成されたカラムにチェック制約を追加します:
// sqlite3は、このカラムに1950年(の始まり)以降のタイムスタンプのみが含まれるように強制します
DateTimeColumn get creationTime => dateTime()
.check(creationTime.isBiggerThan(Constant(DateTime(1950))))
.withDefault(currentDateAndTime)();
これらのチェック制約はCREATE TABLE文の一部であることに注意してください。
チェック制約を変更または削除したい場合は、スキーママイグレーションを記述し、制約なしでテーブルを再作成してください。
ユニークカラム
個々のカラムがテーブルのすべての行に対して一意でなければならない場合、そのカラムの定義でunique()と宣言することができます:
class TableWithUniqueColumn extends Table {
IntColumn get unique => integer().unique()();
}
複数のカラムの組み合わせがテーブル内で一意でなければならない場合、テーブルにカスタムテーブル制約を追加することができます。
カスタム制約
カラムやテーブルの制約の中には、DriftのDart APIではサポートされていないものがあります。
これにはカラムの照合順序が含まれ、customConstraintを使用して適用できます:
class Groups extends Table {
TextColumn get name => integer().customConstraint('COLLATE BINARY')();
}
customConstraintを適用すると、デフォルトで含まれる他のすべての制約が上書きされます。
特に、NOT NULL制約を再度含める必要があることを意味します。
テーブルクラスのcustomConstraintsゲッターをオーバーライドすることで、テーブル全体の制約を追加することもできます。
名前
デフォルトでは、DriftはデータベースのDartゲッターのsnake_case名を使用します。たとえば
import 'package:drift/drift.dart';
class EnabledCategories extends Table {
IntColumn get parentCategory => integer()();
}
CREATE TABLE enabled_categories (parent_category INTEGER NOT NULL)として生成されます。
テーブル名をオーバーライドするには、単純にtableNameゲッターをオーバーライドします。
カラムの名前を明示的に指定するには、namedメソッドを使用します:
('EnabledCategory')
class EnabledCategories extends Table {
String get tableName => 'categories';
('parent_id')
IntColumn get parentCategory => integer().named('parent')();
}
更新されたクラスは、CREATE TABLE categories (parent INTEGER NOT NULL)として生成されます。
データをjsonにシリアライズする際にカラム名を更新するには、ゲッターに@JsonKeyをアノテーションします。
生成されるデータクラスの名前も変更できます。デフォルトでは、driftはデーブル名から末尾のsを削除します(したがって、UsersテーブルはUserデータクラスを持つことになります)。
しかし、これはすべてのケースで機能するわけではありません。上記のEnabledCategoriesクラスでは、EnabledCategorieデータクラスが生成されます。
このような場合は、@DataClassNameアノテーションを使用して、必要な名前を設定できます。
既存の行クラス
デフォルトでは、Driftは各テーブルに行クラスを生成します。この行クラスはすべてのカラムにアクセスでき、hasCodeやoperator==などの便利な演算子も実装しています。
独自の型階層を使用したい場合や、生成されるクラスよりも細かく制御したい場合は、Driftに独自のクラスや型を指定することもできます:
typedef Category = ({int id, String name});
(Category)
class Categories extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
Driftは、その型がそのテーブルの行を格納するのに適しているかどうかを検証します。 この機能の詳細については、こちらを参照してください。
テーブルオプション
個々のカラムに追加されるオプションに加えて、いくつかの制約はテーブル全体に適用されます。
プライマリーキー
テーブルがautoIncrement()制約を持つIntColumnを持っている場合、Driftはそれをデフォルトのプライマリーキーとして認識します。
カスタムプライマリーキーを指定したい場合は、テーブルのprimaryKeyゲッターをオーバーライドします。
class GroupMemberships extends Table {
IntColumn get group => integer()();
IntColumn get user => integer()();
Set<Column> get primaryKey => {group, user};
}
ジェネレータが認識できるように、プライマリーキーは基本的に一定でなければならないことに注意してください。つまり
=>構文で定義されなければならず、関数の本体はサポートされていません。if、for、スプレッド演算子のようなコレクション要素を含まないセットリテラルを返さなければなりません。
テーブル内のユニークコラム
1つのカラムの値がテーブル内で一意でなければならない場合、そのカラムをユニークにすることができます。
複数のカラムを組み合わせた値が一意でなければならない場合は、uniqueKeysゲッターをオーバーライドしてテーブル上で宣言する必要があります:
class IngredientInRecipes extends Table {
List<Set<Column>> get uniqueKeys => [
{recipe, ingredient},
{recipe, amountInGrams}
];
IntColumn get recipe => integer()();
IntColumn get ingredient => integer()();
IntColumn get amountInGrams => integer().named('amount')();
}
テーブル内のカスタムテーブル制約
テーブル制約の中には、Driftではまだ直接サポートされていないものがあります。
カラムのカスタム制約と同様に、customConstraintsをオーバーライドすることで追加できます。
class TableWithCustomConstraints extends Table {
IntColumn get foo => integer()();
IntColumn get bar => integer()();
List<String> get customConstraints => [
'FOREIGN KEY (foo, bar) REFERENCES group_memberships ("group", user)',
];
}
インデックス
テーブルのカラムにインデックスを設定すると、そのカラムで特定される行をより簡単に特定できるようになります。
Dirftでは、@TableIndexアノテーションを使用してテーブルにインデックスを適用できます。
アノテーションを繰り返すことで、同じテーブルに複数のインデックスを適用できます。
@TableIndex(name: 'user_name', columns: {#name})
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
各インデックスには固有の名前が必要です。通常、一意な名前を保証するために、テーブル名はインデックス名の一部となります。