用于一对一识别关系的实体框架代码优先流利 API 配置

Posted

技术标签:

【中文标题】用于一对一识别关系的实体框架代码优先流利 API 配置【英文标题】:Entity Framework Code First Fluent API configuration for one to one identifying relationship 【发布时间】:2015-03-16 09:48:22 【问题描述】:

我有以下类结构:

如何配置 Fluent API 以将识别关系放入 Cards 表中?

我是说

卡片表 PK:Id、CustomerId 卡片表 FK:CustomerId

我希望在将新卡分配给 Customer.Card 属性时删除之前的卡。

所以我这样定义我的类:

public class Customer

    public int Id  get; private set; 
    public virtual Card Card  get; set; 


public abstract class Card

    public int Id  get; private set; 


public class Visa : Card



public class Amex : Card


DbContext 看起来像这样:

public class Context : DbContext

    public DbSet<Customer> Customers  get; set; 
    public DbSet<Card> Cards  get; set; 

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Customer>()
            .HasRequired(c => c.Card)
            .WithRequiredPrincipal()
            .Map(a => a.MapKey("CustomerId"))
            .WillCascadeOnDelete();

        modelBuilder.Entity<Card>();
    

这是测试:

[TestClass]
public class UnitTest1

    [TestMethod]
    public void TestMethod1()
    
        var context = new Context();
        var customer = new Customer();
        context.Customers.Add(customer);
        customer.Card = new Visa();
        context.SaveChanges();

        customer.Card = new Amex();
        context.SaveChanges();

        Assert.AreEqual(1, context.Customers.Count());
        Assert.AreEqual(1, context.Cards.Count());
    

它根本不起作用。我在第二次保存时有这个,我不知道如何在这里指定识别关系:

未处理的异常: System.Data.Entity.Infrastructure.DbUpdateException:一个错误或 保存不公开外键的实体时发生 他们关系的属性。 EntityEntries 属性将 返回 null 因为无法将单个实体标识为 异常的来源。可以在保存的同时处理异常离子 通过在实体类型中公开外键属性变得更容易。 有关详细信息,请参阅 InnerException。 ---> System.Data.Entity.Core.U pdateException:来自“Customer_Card”关联集的关系 处于“已删除”状态。给定多重约束,一个 对应的'Customer_Card _Target' 也必须处于“已删除”状态。

更新很容易让它适用于一对多的关系。您可以在下面找到完整的示例:

[TestClass]
public class UnitTest1

    [TestMethod]
    public void TestMethod1()
    
        var context = new Context();
        var customer = new Customer();
        context.Customers.Add(customer);
        customer.Cards.Add(new Visa());
        context.SaveChanges();

        customer.Cards[0] = new Amex();
        context.SaveChanges();

        Assert.AreEqual(1, context.Cards.Count());
    


public class Customer

    public Customer()
    
        Cards = new List<Card>();
    

    public int Id  get; private set; 
    public virtual List<Card> Cards  get; set; 


public abstract class Card

    public int Id  get; private set; 
    public int CustomerId  get; private set; 


public class Visa : Card



public class Amex : Card



public class Context : DbContext

    static Context()
    
        Database.SetInitializer(new DropCreateDatabaseAlways<Context>());
    

    public DbSet<Customer> Customers  get; set; 
    public DbSet<Card> Cards  get; set; 

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Customer>()
            .HasMany(c => c.Cards)
            .WithRequired()
            .HasForeignKey(c => c.CustomerId)
            .WillCascadeOnDelete();

        modelBuilder.Entity<Card>()
            .HasKey(c => new  c.Id, c.CustomerId );
    

【问题讨论】:

问题可能出在你的单元测试上:context.Customers.Add(new Customer()); 没有添加与var customer = new Customer();相同的对象 谢谢,我更新了问题。我仍然不知道如何在这里指定识别关系...... 我猜你想念 Card 类中的 CustomerId 能否请您展示一下您是如何看待它的?我已经试过了…… public abstract class Card public int Id get; private set; 看起来不见了CustomerId 【参考方案1】:

EF 实现一对一的方式是使依赖实体具有一个主键,该主键也是主实体的外键。所以依赖的PK自然会受限于现有的原则PK值。

所以使用你的类,稍微修改一下:

public class Customer

    public int CustomerId  get; private set; 
    public virtual Card Card  get; set; 


public abstract class Card

    public int CustomerId  get; private set; 


public class Visa : Card  

public class Amex : Card  

还有映射:

protected override void OnModelCreating(DbModelBuilder modelBuilder)

    modelBuilder.Entity<Customer>().HasRequired(c => c.Card)
                                   .WithRequiredPrincipal();
    modelBuilder.Entity<Card>().HasKey(c => c.CustomerId);

所以Card 只有CustomerId 作为PK FK,而不是两个单独的字段。

但是

尝试这个,我发现 EF (6.1.2) 中有一个错误。这就是我所做的:

using (var db = new TempModelsContext())

    var cst = new Customer  Name = "Customer1", 
                             Card = new Amex  Number = "Amex"  ;
    db.Customers.Add(cst);
    db.SaveChanges();


using (var db = new TempModelsContext())

    var cst = db.Customers.Include(c => c.Card).Single(c => c.CustomerId == 1);
    cst.Card = new Visa  Number = "Visa" ;
    db.SaveChanges();

(为方便起见,添加了NameNumber)。

通常这样就可以了。 EF 足够聪明,可以看到 1:1 依赖实体被替换,它只是更新Number 字段(有效地删除旧卡)。

但是EF忽略了继承(我使用了默认的TPH)。当然它也应该更新鉴别器字段,但它没有。如果您从数据库中重新获取项目,您最终会得到一张Amex 卡,其编号为“Visa”。

因此,遗憾的是,即使使用此模型,您也必须先移除旧卡,然后添加新卡:

var cst = db.Customers.Include(c => c.Card).Single(c => c.CustomerId == 1);
db.Cards.Remove(cst.Card);
db.SaveChanges();

cst.Card = new Visa  Number = "Visa" ;
db.SaveChanges();

这已经够笨拙了,更不用说你还想把它包装在 TransactionScope 中。

【讨论】:

不,当您使用相同的 DbContext 实例时,TPT 有效。另一个实例在保存时抛出重复的主键异常... 您可以考虑不使用继承。还有其他模式可以对行为差异进行编程,例如战略。与 ORM 的继承通常会导致比其(缺乏)必要性所证明的更多的问题。性能可能会受到严重影响,更何况。【参考方案2】:

实体框架实际上并不允许这种操作。您不能简单地通过尝试用另一个对象替换它来从数据库中“删除”一个对象。即使使用 Cascade Delete,您仍然必须在 Entity Framework 中发出删除命令,否则您最终会在上下文中得到一个孤立的项目。您可以尝试覆盖SaveChanges() 方法来捕获此行为,但这并不容易。

最好的办法是检查卡片是否存在,如果存在,则在添加新卡片之前将其删除。这可以很容易地封装成一个可重复的函数调用,如下所示:

public void AddCard(Customer customer, Card card, Context context)

    if (customer.Card != null)
    
        context.Cards.Remove(customer.Card);
    
    customer.Card = card;

编辑

更清楚地说,Entity Framework 不能批量删除关系对象并将替换对象添加到同一个 SaveChanges() 调用中。

这很好用:

Customer.Card = null;
SaveChanges();
Customer.Card = new Amex();
SaveChanges();

注意多次调用SaveChanges()。前面提供的函数更多的是一个包装函数,以避免额外的SaveChanges()调用。

【讨论】:

msdn.microsoft.com/en-us/library/vstudio/… 删除关系会删除依赖对象。对 EntityCollection 调用 Remove 方法将关系和依赖对象都标记为删除。 是的,但您没有在代码示例中调用 Remove() 方法。如果您将对象分配给null,它将在下一个SaveChanges() 事件期间被删除。但是,将对象分配给另一个对象是错误的,因为在SaveChanges() 同步对象之前,它仍处于已删除状态。您的错误甚至证实了这一点,说 >A 关系处于“已删除”状态。 1) 它在空赋值后抛出,除了我。 2)我实际上在这里没有识别关系,因为我不知道在这种情况下如何配置它,所以无论如何也没什么可期待的。

以上是关于用于一对一识别关系的实体框架代码优先流利 API 配置的主要内容,如果未能解决你的问题,请参考以下文章

实体框架代码优先关系 - 如何定义两个对象之间的关系:两个实体之间的可选一对一

使用实体框架 4.1 代码优先方法将一对一的表关系映射到单个实体

一对一或零对一实体框架代码优先 Fluent Api

实体框架4.1代码优先中的一对多关系

用于一对零或一关系的实体框架 (EF) 代码优先级联删除

AspNetUser 上的实体框架流利的 api 1:1 属性无法更改 Account_Id 属性