我正在使用 SQL Server 更改跟踪,并且正在尝试将本文从 Microsoft Docs 改编为实体框架应用程序:使用更改跟踪。
我想使用实体框架运行此 SQL 查询:
SELECT
P.*, CT.*
FROM
dbo.Product AS P
RIGHT OUTER JOIN
CHANGETABLE(CHANGES dbo.Product, @last_synchronization_version) AS CT
ON
P.ProductID = CT.ProductID
这是我到目前为止所得到的:
public class Product
{
public int ProductID { get; set; }
// omitted dozens of other properties
}
public class ProductChange
{
public int ProductID { get; set; }
public Product? Product { get; set; }
public long SYS_CHANGE_VERSION { get; set; }
public long? SYS_CHANGE_CREATION_VERSION { get; set; }
public char SYS_CHANGE_OPERATION { get; set; }
public byte[]? SYS_CHANGE_COLUMNS { get; set; }
public byte[]? SYS_CHANGE_CONTEXT { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ProductChange>()
.HasNoKey()
.ToView(null);
base.OnModelCreating(modelBuilder);
}
long lastSynchronizationVersion = ...; // obtained as described in "Work with Change Tracking"
const string sql = @"
SELECT
P.*, CT.*
FROM
dbo.Product AS P
RIGHT OUTER JOIN
CHANGETABLE(CHANGES dbo.Product, {0}) AS CT
ON
P.ProductID = CT.ProductID";
var changes = await dbContext.Set<ProductChange>.FromSqlRaw(sql, lastSynchronizationVersion);
它不起作用,因为 EF 不理解
P.*
如何映射到 public Product? Product { get; set; }
。当我删除 Product
属性并从查询中删除 P.*
时,一切都会按预期进行。但是,我需要所有属性,而不仅仅是 ID。
将
Product
的所有属性复制到 ProductChange
并使它们全部可为空,但我真的不想这样做。
在实践中,我不仅将更改跟踪用于产品,还将用于数十种实体类型,这些实体类型都具有许多属性。必须在两个地方指定每个属性只是为了使实体框架与更改跟踪配合良好并不是一个好主意。
有没有办法让无键实体类型做我想做的事?或者我是否必须使用 ADO.NET 的
ExecuteReader
并手动映射结果?
事实证明,您可以在无键实体类型上使用具有导航属性的关系,就像使用实体类型一样。
在
OnModelCreating
中配置关系:
modelBuilder.Entity<ProductChange>()
.HasNoKey()
.HasOne(x => x.Entity).WithMany().HasForeignKey(x => x.ProductID) // I added this line.
.ToView(null);
现在您可以使用
Include
而不是手动连接表:
const string sql = "SELECT * FROM CHANGETABLE(CHANGES dbo.Product, {0}) AS CT";
var changes = await dbContext
.Set<ProductChange>
.FromSqlRaw(sql, lastSynchronizationVersion)
.Include(x => x.Entity)
.DefaultIfEmpty() // https://stackoverflow.com/a/63006304/1185136
.ToArrayAsync();
// it works!
此外(这是可选的),我创建了可以轻松继承的基本类型
Change
和 Change<TEntity>
:
public abstract class Change
{
public long Version { get; set; }
public long? CreationVersion { get; set; }
public char Operation { get; set; }
public byte[]? Columns { get; set; }
public byte[]? Context { get; set; }
}
public abstract class Change<TEntity> : Change
where TEntity : class
{
public TEntity? Entity { get; set; }
}
public ProductChange : Change<Product>
{
public int ProductID { get; set; }
}
public OrderChange : Change<Order>
{
public int OrderID { get; set; }
}
// etc...
您必须在
OnModelCreating
中配置每个派生类型的关系。
modelBuilder.Entity<ProductChange>()
.HasOne(x => x.Entity).WithMany().HasForeignKey(x => x.ProductID);
modelBuilder.Entity<OrderChange>()
.HasOne(x => x.Entity).WithMany().HasForeignKey(x => x.OrderID);
// etc...
您不必为每个实体重复
HasNoKey()
和 ToView(null)
,而是添加此循环:
foreach (var changeType in modelBuilder.Model.FindLeastDerivedEntityTypes(typeof(Change)))
{
var builder = modelBuilder.Entity(changeType.ClrType).HasNoKey().ToView(null);
builder.Property(nameof(Change.Version)).HasColumnName("SYS_CHANGE_VERSION");
builder.Property(nameof(Change.CreationVersion)).HasColumnName("SYS_CHANGE_CREATION_VERSION");
builder.Property(nameof(Change.Operation)).HasColumnName("SYS_CHANGE_OPERATION");
builder.Property(nameof(Change.Columns)).HasColumnName("SYS_CHANGE_COLUMNS");
builder.Property(nameof(Change.Context)).HasColumnName("SYS_CHANGE_CONTEXT");
}
如果需要,您可以将 ID 属性移至
Change<TEntity>
。通过这样做,您可以删除 ProductChange
、OrderChange
等类。但是,您必须指定列名称,以便 Entity Framework Core 理解从 ID
映射到 Change<Product>
的 ProductID
属性,以及从 ID
映射到 Change<Order>
的 OrderID
属性,等等。选择不这样做,因为如果您有复合键,这种方法将不起作用。