SQL Serverを利用していると特に工夫することなく利用できるエンティティのバージョン管理(Concurrency Tokens - EF Core | Microsoft Docs)ですが、PostgreSQLでは少し工夫が必要です。
PostgreSQLではMisc | Npgsql Documentationに記載のある通り、内部的に管理されているシステム列のうちxmin
列の値をバージョン管理に使用します。
このため、コンソールアプリケーションなどのようにefのChangeTrackerが状態をすべて管理できる場合にはバージョンを気にする必要はないのですが、ASP.NETのような状態管理がefから離れるようなシステムの場合には少し面倒になります。
バージョン管理を利用するための設定
バージョン管理を使用するための設定はContextクラスに以下の設定を追加すればOKです。
public class HogeContext : DbContext
{
public DbSet<PostalAddress> PostalAddresses { get; set; }
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// このようにForNpgsqlUseXminAsConcurrencyTokenを追加すればバージョン管理対象となる
modelBuilder.Entity<PostalAddress.PostalAddress>().ForNpgsqlUseXminAsConcurrencyToken();
....
}
}
エンティティの取得時にバージョン番号を設定する
ASP.NETなどのウェブアプリケーションではエンティの取得時にバージョン番号を取得してクライアントへバージョン番号を送信し、データの更新時に取得時のバージョン番号をクライアントからサーバーへ送信して、更新時にバージョン番号が一致することを確認します。そのためにはエンティティの取得時にバージョン番号を予め取得する必要があります。
ところが、Npgsqlのバージョン番号はShadow Properties
に定義されているため普通に取得することはできません。このShadow Properties
についてはこちらに説明がありますが、efのChangeTrackerにより値が管理されています。
Shadow Properties - EF Core | Microsoft Docs
値の取得方法は以下の通りとなります。
(uint) context.Entry(entity).Property("xmin").CurrentValue;
このようにすることで、Shadow Property
に定義されているxmin
列から値を取得することができます。
ただ、Context
クラスから取得する必要があるため通常のLinqで取得した処理の後に取得した対象エンティティの全てに上記の値を設定する必要があります。
取得した値は、各エンティティにはCurrentVersion
というフィールドを定義し[NotMapped]
を設定しています。
エンティティ
namespace Hoge
{
public class PostalAddress
{
[Required]
[MaxLength(36)]
[MinLength(36)]
public string Id { get; set; }
...
// NotMappedでCurrentVersionを定義している
[NotMapped]
public uint CurrentVersion { get; set; }
}
}
このエンティティにバージョン番号を設定するためDbContextの拡張メソッドを定義しています。
バージョン番号の取得
public static class DbContextExtensions
{
public static void CurrentVersion<T>(this DbContext context, T entity)
where T:class, IVersionable
{
entity.CurrentVersion = (uint) context.Entry(entity).Property("xmin").CurrentValue;
}
}
UpdateやDeleteのときにバージョン番号を設定する
次に実際の更新処理についてです。NpgsqlではUpdateやDeleteなどをefから発行すれば自動的にバージョン番号を付加したSQLを発行してくれます。
[15:24:16 INF] Executed DbCommand (0ms) [Parameters=[@p0='7f396576-9ddb-4b5f-9ce4-3a2dcf641c27' (Nullable = false), @p1='3347' (DbType = Object)], CommandType='Text', CommandTimeout='30']
DELETE FROM "customers"
WHERE "id" = @p0 AND "xmin" = @p1;
ここに記載されている条件のxmin=@p1
がバージョン番号の比較になります。特に設定しないときには更新対象のデータ取得時一緒に取得され(、ShadowPropertiesに設定され)るバージョン番号が設定されます。
ASP.NETなどのウェブアプリケーションでは、ここにクライアントから送信されてきたバージョン番号を設定する必要があります。設定は例によってShadow Properties
へ設定することになります。
この処理についてもDbContextに拡張メソッドを定義して設定できるようにしています。
public static void SetCurrentVersion<T>(this DbContext context, T entity, ILogger logger = null)
where T : class, IVersionable
{
logger?.LogDebug(
$"{entity}.CurrentVersion = {entity.CurrentVersion} / xmin = {context.Entry(entity).Property("xmin").CurrentValue}");
logger?.LogDebug(
$"{entity} xmin.OriginalValue = {context.Entry(entity).Property("xmin").OriginalValue}");
// ここでOriginalValueに値を設定する
context.Entry(entity).Property("xmin").OriginalValue = entity.CurrentVersion;
logger?.LogDebug(
$"--> {entity} xmin.OriginalValue = {context.Entry(entity).Property("xmin").OriginalValue}");
}
Shadow Properties
にはOriginalValue
とCurrentValue
がありますが、OrigginalValue
が比較対象のバージョン番号として利用されているようです。(ef coreのソースを読めていないので挙動としてそのようになっているという認識です。)
最後に
SQL Serverだとスムーズに対応できるバージョン番号ですが、PostgreSQLを利用する場合には少し手間が必要です。どうやるんだろうかと思って色々と調べてみたところこんな感じでうまく行っていますという内容ですので、間違いやもっといい方法があれば教えていただけるとうれしいです。
追記
aspnet/EntityFrameworkCore: Entity Framework Core is a lightweight and extensible version of the popular Entity Framework data access technologyでCurrentVersion
のコメントを読んでみたところ以下の通りデータベースに処理を行うときに使用する値と記載がありました。
github.com
/// <summary>
/// Gets the original property values for this entity. The original values are the property
/// values as they were when the entity was retrieved from the database.
/// </summary>
/// <value> The original values. </value>
public virtual PropertyValues OriginalValues
{
[DebuggerStepThrough] get => new OriginalPropertyValues(InternalEntry);
}