Skip to content

Core の DB の接続・暗号化・起動時の排他 ​

第1版作成 最終更新 (日本時間)

このページは、VehicleVision.PleasanterTools.Core.Data が決めている DB まわりの規約と挙動を説明します。対象は 0.1.0-preview.9(最新のプレビュー版)です。バージョン別の違いは 概要 の対応表と 変更履歴 を参照してください。

プレビュー版・未確認の範囲

Core は 0.1.0-preview.N で、API が変わり得ます(概要)。このページは、リリースタグのソース(src/VehicleVision.PleasanterTools.Core.Data/)を読んで書いています。4 つの RDBMS の実機で動かして確かめた内容ではありません。 コード例は、ソースの署名から組み立てたもので、実行して確かめていません。

設定名の規約(AppDatabase) preview.5 以降 ​

ツールごとに DB の設定名・ファイル名・ロック名を手で書くと、書き写しの違いが増え、取り違えの原因になります。AppDatabase は、アプリ ID とマイグレーションを持つアセンブリを受け取り、残りの名前を規約で導きます。

アプリ ID は一意にします。小文字の英字で始まり、小文字の英数字とハイフン(連続しない)だけで書けます。形が違うと、作る時に例外で止まります。

値規約(<id> はアプリ ID、<ID> はその大文字でハイフンを下線にしたもの)sample-app の場合
RDBMS を選ぶ設定名<ID>_DB_PROVIDERSAMPLE_APP_DB_PROVIDER
接続文字列を与える設定名<ID>_DB_CONNECTIONSTRINGSAMPLE_APP_DB_CONNECTIONSTRING
暗号化の検査を緩める設定名<ID>_DB_ALLOW_INSECURESAMPLE_APP_DB_ALLOW_INSECURE
SQLite の既定ファイル名<id>.dbsample-app.db
マイグレーションの排他のロック名<id>-startup-migrationssample-app-startup-migrations

メールの環境変数の接頭辞(<ID>_MAIL_)は、Core.Mail と Core.Data が互いを参照しないため、AppDatabase には含まれません。メール側は メール送信 を参照してください。

ロック名はアプリごとに別になります。同じ DB サーバを複数のアプリが使うとき、ロック名が同じだと別のアプリのマイグレーションを待たせてしまうためです。アプリ ID が一意なら、ロック名も衝突しません。

組み込み方の例 ​

アプリのデータ層に AppDatabase を 1 つだけ持ちます。次はソースの XML ドキュメントにある書き方に沿った例です。

csharp
public static class SampleAppDatabase
{
    public static AppDatabase Instance { get; } =
        new("sample-app", typeof(SampleAppDatabase).Assembly);

    // アセンブリの読み込み時に作る(SQLite のモデル型の登録が、必ず先に済む)
    [ModuleInitializer]
    internal static void Initialize() => _ = Instance;
}

起動時の流れは次のとおりです。メソッドの名前と引数はソースの署名に合わせていますが、この組み立て自体は実行して確かめていません。

csharp
var db = SampleAppDatabase.Instance;

// 未設定なら既定(ここでは SQL Server)。読めない値は止める
var provider = db.ParseProvider(configuration[db.ProviderSetting], DatabaseProvider.SqlServer);

// 設定があればそれを使う。SQLite だけは App_Data 配下の <id>.db が既定
var connectionString = db.ResolveConnectionString(
    provider, configuration[db.ConnectionStringSetting], environment.ContentRootPath);

// 暗号化を切っていないか(切っていれば例外。緩める設定が true のときだけ通す)
var allowInsecure = bool.TryParse(configuration[db.AllowInsecureSetting], out var allow) && allow;
db.EnsureSecure(provider, connectionString, allowInsecure);

// DB が繋がるまで待ってから、排他を取ってマイグレーションを当てる
var failure = await db.Migrator.WaitForDatabaseAsync(provider, connectionString, TimeSpan.FromSeconds(60));
if (failure is not null)
{
    throw failure;
}

await db.Migrator.MigrateUpWithLockAsync(
    provider, connectionString, AppDatabase.DefaultMigrationLockTimeout);

// 以後の読み書きは、このファクトリから接続を作る
var connections = new DbConnectionFactory(provider, connectionString);

RDBMS の選択 ​

ParseProvider は、設定値を DatabaseProvider の名前(SqlServer / PostgreSql / MySql / Sqlite)として読みます。大文字小文字は区別しません。未設定(空白だけを含む)なら、呼び出し側が渡した既定値になります。読めない値は黙って既定へ落とさず、例外で止まります。 例外の文には、設定名と指定できる値が出ます。

接続文字列の解決 ​

ResolveConnectionString(内部では DatabaseConnectionString.Resolve)の規則です。

条件結果
設定値があるその値を使う
設定値が無く、RDBMS が SQLite<コンテンツのルート>/App_Data/<id>.db を、無ければ作成する接続文字列で返す(App_Data フォルダーも作る)
設定値が無く、SQLite 以外<ID>_DB_CONNECTIONSTRING が設定されていない の例外で止まる

接続文字列は、設定ファイルへ直書きせず、環境変数か Key Vault から読む前提です(DbConnectionFactory のコメント)。

接続の作り方と暗号化 ​

DbConnectionFactory は、RDBMS に合わせた接続を作ります。

  • SQL Server: 接続文字列に Encrypt=False と書かれていても、必ず Encrypt=True にして接続します。 暗号化しないという逃げ道を残さないための動作です。
  • SQLite: 待機時間(busy timeout)を 30 秒に設定し、接続を開くたびに PRAGMA journal_mode = WAL(有効にできなければ例外)と PRAGMA foreign_keys = ON を適用します。
  • PostgreSQL・MySQL: 接続文字列をそのまま使います。

起動時の暗号化の検査(ConnectionSecurity) ​

EnsureSecure は、接続文字列が暗号化を切っていないか、相手の証明書を確かめているかを見て、問題があれば InvalidOperationException で止めます。例外の文には問題の理由が出ます(接続文字列そのものは出しません)。

RDBMS問題として扱う指定直し方
SQL ServerEncrypt=False(実際には暗号化されるが、読み替えを黙ってやらないために咎める)、TrustServerCertificate=True指定を外す
PostgreSQLSslMode が Disable、Prefer、Allow、Require(Require は証明書を確かめない)VerifyCA か VerifyFull にする
MySQLSslMode が None(Disabled)、Preferred、Required(Required は証明書を確かめない)VerifyCA か VerifyFull にする
SQLite検査しない(同一プロセスがローカルファイルを直接読むため)-

検証環境が自己署名の証明書を使うときのために、<ID>_DB_ALLOW_INSECURE=true を設定すると、検査の結果を無視して起動できます。緩むのは検査だけです。 SQL Server の接続は、それでも必ず暗号化されます。本番では true にしないでください。

運用での確認

起動時に「DB への接続が暗号化されていない、または相手を確かめていない」という例外で止まったら、例外の文に出る理由(上の表の指定)を接続文字列から直します。検証環境で意図して緩めるときだけ、例外の文が示す設定名(<ID>_DB_ALLOW_INSECURE)を true にします。

起動時マイグレーションと排他(DatabaseMigrator) ​

MigrateUpWithLockAsync は、排他を取ってから未適用のマイグレーションをすべて適用します。複数台が同時に起動しても、マイグレーションを当てるのは 1 台だけになります。待機時間の既定値として、AppDatabase.DefaultMigrationLockTimeout(5 分)が用意されています。取れなかったときは TimeoutException(Migration lock was not acquired within N seconds.)で止まります。

排他の仕組みは RDBMS ごとに違います。

RDBMS排他の方法
SQL Serversp_getapplock(排他、セッション所有)
PostgreSQLpg_try_advisory_lock
MySQLGET_LOCK
SQLiteDB ファイルと同じ場所の <DB ファイル>.migration-lock.sqlite に書き込み予約(BEGIN IMMEDIATE)を取る。ファイルを持たない DB(:memory:)では使えない
  • ロックの名前には DB の名前から作った値を付け足します(SQL Server・MySQL)。同じロック名のアプリが同じサーバの別々の DB を使っても、互いに待たせません。
  • 100 ミリ秒ごとに取り直し、期限まで待ちます。ロックを保持する接続を閉じれば、DB 側でも解放されます。
  • 前方のみです。ロールバック用のスクリプトには頼りません。

この排他を直接使いたい場合のために、DatabaseMigrationLock が公開されています preview.3 以降。

待機・状況の確認 ​

メソッド役割
WaitForDatabaseAsyncDB が接続を受けるまで、1 秒おきに試して待つ。繋がれば null、期限までに繋がらなければ最後の例外を返す。コンテナで DB と同時に立ち上げると、DB より先にアプリが動き出すことへの備え。待つのはここだけで、アプリ本体は待たない
HasPendingMigrations未適用のマイグレーションがあるか
PendingMigrations未適用の名前の一覧(20 M0020_Xxx のように、番号と型の名前)。DB は読むだけで、版の表も作らない。説明文を出さず型名を出すのは、日本語の説明文が Azure の Kudu の Debug console で文字化けするため
GetStatus適用済みの最大の版・最新の版・未適用の件数・最後に適用した日時(UTC)。管理画面の表示用

DatabaseStartupState は、起動時マイグレーションの完了まで受付や常駐処理を止めておくための状態です。MarkReady を呼ぶと IsReady が真になり、WaitUntilReadyAsync で待っていた処理が進みます。

時刻の持ち方(DbTime) ​

DB に入れる時刻は、必ず DbTime を通します。DateTime.UtcNow をそのまま渡さないでください。

メソッド動作
DbTime.ForDb / UtcNowTruncated秒未満を切り捨て、DateTimeKind を外して(Unspecified)DB へ渡す形にする
DbTime.AsUtcDB から読んだ値に UTC の印を付け直す(値は動かさない)。DateTime? 版もある
DbTime.Truncate秒未満の切り捨てだけ(Kind は保つ)

理由は、ソースのコメントによると次の 2 つです。

  • MySQL の datetime は秒未満を保持せず四捨五入するため、そのまま渡すと保存した時刻が現在より未来に丸められ、「今より前か」の判定が通らなくなる。
  • Npgsql は DateTimeKind.Utc を見て timestamptz と判断するが、列は時間帯を持たない timestamp のため、PostgreSQL がセッションの時間帯へ変換してから格納する。サーバの時間帯が Asia/Tokyo だと保存値が 9 時間ずれる(ソースのコメントは「実際に踏んだ」と記しています)。

列に入っているのは常に UTC です。読み出した値は Unspecified になるので、JSON などへ出す前に AsUtc で印を付けます。

SQL の方言差(SqlDialect) ​

RDBMS ごとに書き方が違う部分は SqlDialect に閉じ込めています。

機能内容
FormatSQL 中の [識別子] を、RDBMS の引用符へ書き換える(SQL Server は [x]、PostgreSQL と SQLite は "x"、MySQL は `x`)。角括弧で囲んだ所だけが書き換わる。文字列リテラルを含む SQL には使わない(リテラル内の [ まで書き換えるため)
Page件数を絞る句(SQL Server は OFFSET/FETCH、それ以外は LIMIT/OFFSET)。@Limit と @Offset を使い、呼ぶ側で ORDER BY が要る
EscapeLike / LikeEscapeLIKE の逃がし文字に ! を使う。%・_・[ と ! 自身を逃がす
SupportsReturningRETURNING 相当が使えるか(SQL Server・PostgreSQL・SQLite)

SQLite のモデル型の登録 preview.4 以降 ​

SQLite は、列挙型や record のような型を Dapper が組み立てるために、型の対応表の登録が要ります。登録の対象は、走査したアセンブリの中にある列挙型と、名前つきコンストラクタを持つ型です。AppDatabase を作ると、マイグレーションを持つアセンブリ(と additionalModelAssemblies に渡したアセンブリ)が対象に加わります。実際の登録は SQLite の接続を作るときだけで、ほかの RDBMS の挙動は変わりません。直接使うなら SqliteTypeHandlers.AddModelAssemblies です(何度呼んでもよい)。

preview.4 より前は、Core.Data のアセンブリだけが対象でした。アプリのモデルを SQLite で読むと、Dapper が A parameterless default constructor or one matching signature … is needed で止まる原因になります。

関連ページ ​

変更履歴

第1版共通ライブラリ Core のマニュアルを追加する (#35)