我可以使用无键实体类型来查询 Entity Framework Core 中的 CHANGETABLE 吗?
Can I use Keyless Entity Types to query CHANGETABLE in Entity Framework Core?
我正在使用 SQL 服务器更改跟踪,我正在尝试将 Microsoft Docs 中的这篇文章改编为 Entity Framework 应用程序:Work with Change Tracking.
我想 运行 这个 SQL 查询使用 Entity Framework:
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
并使它们全部可为空,但我真的不想诉诸于此。
在实践中,我不仅会为产品使用更改跟踪,还会为数十种实体类型使用更改跟踪,它们都有很多属性。必须在两个地方指定每个 属性 只是为了让 Entity Framework 很好地使用更改跟踪不是一个好主意。
有没有办法让无键实体类型做我想做的事?还是我必须使用 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() //
.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
,等等。我选择不这样做,因为如果你有复合键,这种方法将不起作用。
我正在使用 SQL 服务器更改跟踪,我正在尝试将 Microsoft Docs 中的这篇文章改编为 Entity Framework 应用程序:Work with Change Tracking.
我想 运行 这个 SQL 查询使用 Entity Framework:
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
并使它们全部可为空,但我真的不想诉诸于此。
在实践中,我不仅会为产品使用更改跟踪,还会为数十种实体类型使用更改跟踪,它们都有很多属性。必须在两个地方指定每个 属性 只是为了让 Entity Framework 很好地使用更改跟踪不是一个好主意。
有没有办法让无键实体类型做我想做的事?还是我必须使用 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() //
.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
,等等。我选择不这样做,因为如果你有复合键,这种方法将不起作用。