【问题标题】:Can I use Keyless Entity Types to query CHANGETABLE in Entity Framework Core?我可以使用无键实体类型在 Entity Framework Core 中查询 CHANGETABLE 吗?
【发布时间】:2020-08-13 08:57:28
【问题描述】:

我正在使用 SQL Server 更改跟踪,并且正在尝试将 Microsoft Docs 中的这篇文章改编为实体框架应用程序:Work with Change Tracking。

我想使用实体框架运行这个 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 并使其都可以为空,但我真的不想这样做。

在实践中,我将不仅对产品使用更改跟踪,还对具有许多属性的数十种实体类型使用更改跟踪。必须在两个地方指定每个属性只是为了让 Entity Framework 与更改跟踪配合得很好。

有没有办法让无钥匙实体类型做我想做的事?还是我必须使用 ADO.NET 的 ExecuteReader 并手动映射结果?

【问题讨论】:

    标签: c# sql-server entity-framework entity-framework-core change-tracking


    【解决方案1】:

    事实证明,您可以在无键实体类型上使用与导航属性的关系,就像使用实体类型一样。

    在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&lt;TEntity&gt;:

    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&lt;TEntity&gt;。通过这样做,您可以删除ProductChange、OrderChange 等类。但是,您必须指定列名,以便 Entity Framework Core 理解来自Change&lt;Product&gt; 的ID 属性映射到ProductID,以及来自Change&lt;Order&gt; 的ID 属性映射到OrderID,等等。选择不这样做是因为如果您有复合键,这种方法将不起作用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-07
      • 1970-01-01
      • 2017-11-23
      • 1970-01-01
      相关资源
      最近更新 更多