Entity Framework Code First Fluent API 一对一识别关系的配置

Entity Framework Code First Fluent API configuration for one to one identifying relationship

我有以下 class 结构:

如何配置 Fluent API 将识别关系放入 Cards table?

我是说

我想在给 Customer.Card 属性.

分配新卡时删除之前的卡

所以我这样定义 classes:

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());
    }
}

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

Unhandled Exception: System.Data.Entity.Infrastructure.DbUpdateException: An err or occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a singl e entity cannot be identified as the source of the exception. Handling of except ions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details. ---> System.Data.Entity.Core.U pdateException: A relationship from the 'Customer_Card' AssociationSet is in the 'Deleted' state. Given multiplicity constraints, a corresponding 'Customer_Card _Target' must also in the 'Deleted' state.

UPDATE 使其适用于 one-to-many 关系很容易。您可以在下面找到完整的示例:

[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 });
    }
}

Entity Framework 不允许这种操作。您不能 "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() 调用。

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.