4

我有以下类结构:

在此处输入图像描述

如何配置 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.UpdateException:来自“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 });
    }
}
4

2 回答 2

3

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只有CustomerIdPKFK,而不是两个单独的字段。

尝试这个,我发现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.

于 2015-01-18T22:57:45.070 回答
1

实体框架实际上并不允许这种操作。您不能简单地通过尝试用另一个对象替换它来从数据库中“删除”一个对象。即使使用 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()调用。

于 2015-01-18T04:44:10.937 回答