serverpod-database について
serverpod-databaseは、Serverpodアプリを開発する人向けにORMとデータベース操作を解説するスキルです。モデルのCRUD、クエリ、リレーション、トランザクション、クライアント側SQLiteを扱います。
主な機能
- 01モデルに基づくORM生成
- 02CRUDと条件付きクエリ
- 03リレーションの読み込みと関連付け
- 04トランザクションと行ロック
- 05生SQLクエリ
- 06クライアント側SQLiteデータベース
活用シーン
- 01モデルを定義してCRUDを実行
- 02条件やリレーションによる検索と並べ替え
- 03トランザクション内で複数のDB操作を実行
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ serverpod/ serverpod --skill serverpod-databaseインストール後、Claude Code で/serverpod-databaseを入力して読み込まれたか確認します。
詳細
Serverpod データベース
Serverpod は、.spy.yaml で table が指定されたモデルの ORM コードを生成します。PostgreSQL は本番環境のデフォルトのデータベースです。また、データベースを config/<runMode>.yaml で database.filePath: <path> と設定するプロジェクトやテストでは、SQLite もサポートされています。
CRUD
class: Company
table: company
fields:
name: String
foundedDate: DateTime
モデルとリレーションの定義について詳しくは、Serverpod Models を参照してください。
var company = Company(name: 'Serverpod Inc.', foundedDate: DateTime.now());
company = await Company.db.insertRow(session, company);
var stored = await Company.db.findById(session, company.id);
company = company.copyWith(name: 'New Name');
await Company.db.updateRow(session, company);
await Company.db.deleteRow(session, company);
フィルター
テーブル記述子 t を使い、where コールバックで記述する流れるようなフィルター API です。
var activeCompanies = await Company.db.find(session,
where: (t) => t.name.ilike('a%') & (t.foundedDate > DateTime(2020)));
- 等値:
t.column.equals(value)、t.column.notEquals(value) - 比較(int/double/Duration/DateTime):
>、>=、<、<= - 範囲:
t.column.between(a, b)、notBetween - 集合:
t.column.inSet(set)、notInSet - 文字列:
t.column.like('A%')(大文字・小文字を区別)、ilike(大文字・小文字を区別しない);%= 任意の文字列、_= 1 文字 - 結合:
&(AND)、|(OR)、~(NOT);優先順位を指定するには括弧を使います - 関連(1 対 1):
t.address.street.like('%road%') - 関連(1 対多):
t.orders.count() > 3、t.orders.count((o) => o.itemType.equals('book')) > 3、t.orders.none()、t.orders.any()、t.orders.any((o) => ...)、t.orders.every((o) => ...) - ベクトル:類似検索用の距離演算子(
distanceCosine、distanceL2)を使用します(PostgreSQL のみ)。
並べ替えとページネーション
- 並べ替え:
orderBy: (t) => t.column(デフォルトは昇順);降順にするにはorderBy: (t) => t.column.desc() - 複数の項目:
orderByList: (t) => [t.name.desc(), t.id.asc()] - リレーションで並べ替え:
orderBy: (t) => t.ceo.name;件数で並べ替え:orderBy: (t) => t.employees.count() - ページネーション:オフセット方式には
limitとoffsetを使用します。カーソル方式ではwhere: (t) => t.id > lastIdとorderBy: (t) => t.id、limitを組み合わせます
リレーション(include、attach、detach)
include を使って関連オブジェクトを取得します。
var employee = await Employee.db.findById(session, id,
include: Employee.include(address: Address.include()));
var company = await Company.db.findById(session, id,
include: Company.include(
employees: Employee.includeList(
where: (t) => t.name.ilike('a%'),
orderBy: (t) => t.name,
limit: 10,
includes: Employee.include(address: Address.include()),
),
));
オブジェクトリレーションを持つモデルには、attach/detach 用の専用メソッドもあります。
await Company.db.attachRow.employees(session, company, employee);
await Company.db.attach.employees(session, company, [e1, e2]);
await Company.db.detachRow.employees(session, employee);
attach/detach するオブジェクトには id が設定されている必要があります(通常は事前にデータベースから取得します)。
トランザクション
await session.db.transaction((tx) async {
await Company.db.insertRow(session, company, transaction: tx);
await OtherModel.db.updateRow(session, other, transaction: tx);
});
トランザクション内のすべての DB 呼び出しには tx を使用してください。
行ロック
トランザクションが必要です。find/findFirstRow/findById に lockMode と transaction を渡します。
LockMode.forUpdate(排他)、forNoKeyUpdate、forShare、forKeyShareLockBehavior.wait(デフォルト)、noWait(例外をスロー)、skipLocked(スキップ。ジョブキューに適しています)- 読み込みを伴わないロック:
Company.db.lockRows(session, where: ..., lockMode: ..., transaction: tx)
SQLite は一度に 1 つの書き込みトランザクションしかサポートしないため、SQLite で行をロックしようとしても何も行われません。
実行時パラメーター
Postgres のパラメーターをグローバルに設定するには、Serverpod の初期化時に runtimeParametersBuilder: (params) => [params.searchPaths(['my_schema', 'public'])] を指定します。トランザクション単位では await tx.setRuntimeParameters(...) を使用します。検索パス、ベクトルインデックスのオプション、カスタムの MapRuntimeParameters に使用できます。コネクションプーリングのため、セッション単位では設定できません。
生 SQL
生 SQL クエリを実行するには、以下のいずれかのメソッドを使用します。
late DatabaseResult result;
result = await session.db.unsafeQuery(query, parameters: parameters);
result = await session.db.unsafeSimpleQuery(query);
late int rowsAffected;
rowsAffected = await session.db.unsafeExecute(query, parameters: parameters);
rowsAffected = await session.db.unsafeSimpleExecute(query);
標準的な CRUD には ORM を優先し、SQL の文字列連結ではなく、ユーザー入力をパラメーター化してください。
クライアント側データベース
少なくとも 1 つのモデルで database: client または database: all が設定されている場合、生成された Client クラスには、SQLite データベースファイル用の ClientDatabaseSession を返す createSession メソッドが追加されます。Flutter では、次のようにデータベースを開きます。
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:my_project_client/my_project_client.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Set the client URL to the server URL
final client = Client(clientUrl);
// Resolve the database path
final path = await resolveDatabasePath('app.db');
// Store the session in your state manager to later use on database operations.
final session = await client.createSession(path, isDebugMode: kDebugMode);
}
Future<String> resolveDatabasePath(String fileName) async {
if (kIsWeb) return fileName;
final dir = await getApplicationSupportDirectory();
return p.join(dir.path, fileName);
}
クライアントパッケージの依存関係に serverpod_database パッケージを追加する必要があることに注意してください。