在EF 4.1 Code First中将分离的实体添加到1-many关系中

时间:2011-11-13 23:02:49

标签: c# entity-framework entity-framework-4 ef-code-first

我正在尝试使用EF 4.1 Code First来建模具有单个角色的用户的简单关系。当我尝试将具有角色的现有用户保存到不同的上下文(使用不同的上下文来模拟客户端 - 服务器往返)时,我得到以下异常:

  

System.Data.Entity.Infrastructure.DbUpdateException:保存未公开其关系的外键属性的实体时发生错误。 EntityEntries属性将返回null,因为无法将单个实体标识为异常源。通过在实体类型中公开外键属性,可以更轻松地在保存时处理异常。有关详细信息,请参阅InnerException。 ---> System.Data.UpdateException:来自“User_CurrentRole”AssociationSet的关系处于“已添加”状态。给定多重约束,相应的'User_CurrentRole_Source'也必须处于'已添加'状态。

我期望创建一个新角色并与现有用户相关联 我做错了什么,这有可能首先在EF 4.1代码中实现吗?错误消息似乎表明它需要用户和角色都处于添加状态,但我正在修改一个现有用户,那怎么可能呢?

需要注意的事项:我想避免修改实体的结构(例如,通过引入实体上可见的外键属性),并且在数据库中我希望用户有一个指向Role的外键(而不是相反)。我也不准备转移到自我跟踪实体(除非没有别的办法)。

以下是实体:

public class User
{
    public int UserId { get; set; }
    public string Name { get; set; }
    public Role CurrentRole { get; set; }
}

public class Role
{
    public int RoleId { get; set; }
    public string Description { get; set; }
}

这是我正在使用的映射:

public class UserRolesContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Role> Roles { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>().HasKey(u => u.UserId);
        modelBuilder.Entity<Role>().HasKey(r => r.RoleId);
        modelBuilder.Entity<User>().HasRequired(u => u.CurrentRole);
    }
}

我用这个预先填充数据库:

public class UserInitializer : DropCreateDatabaseAlways<UserRolesContext>
{
        protected override void Seed(UserRolesContext context)
        {
            context.Users.Add(new User() {Name = "Bob", 
                                          CurrentRole = new Role() {Description = "Builder"}});
            context.SaveChanges();
        }
}

最后,这是失败的测试:

    [TestMethod]
    public void CanModifyDetachedUserWithRoleAndReattach()
    {
        Database.SetInitializer<UserRolesContext>(new UserInitializer());
        var context = new UserRolesContext();

        // get the existing user
        var user = context.Users.AsNoTracking().Include(c => c.CurrentRole).First(u => u.UserId == 1);

        //modify user, and attach to a new role
        user.Name = "MODIFIED_USERNAME";
        user.CurrentRole = new Role() {Description = "NEW_ROLE"};

        var newContext = new UserRolesContext();
        newContext.Users.Attach(user);
        // attachment doesn't mark it as modified, so mark it as modified manually
        newContext.Entry(user).State = EntityState.Modified;
        newContext.Entry(user.CurrentRole).State = EntityState.Added;

        newContext.SaveChanges();

        var verificationContext = new UserRolesContext();
        var afterSaveUser = verificationContext.Users.Include(c => c.CurrentRole).First(u => u.UserId == 1);
        Assert.AreEqual("MODIFIED_USERNAME", afterSaveUser.Name, "User should have been modified");
        Assert.IsTrue(afterSaveUser.CurrentRole != null, "User used to have a role, and should have retained it");
        Assert.AreEqual("NEW_ROLE", afterSaveUser.CurrentRole.Description, "User's role's description should  have changed.");
    }
}
}

当然这是一个被覆盖的场景,我猜这是我在定义模型映射的方式中缺少的东西?

1 个答案:

答案 0 :(得分:7)

你已经破坏了EF状态模型。您使用强制CurrentRole映射了您的实体,因此EF知道如果没有User,您就无法拥有现有Role。您还使用了independent associations(在您的实体上没有公开FK属性)。这意味着角色和用户之间的关系是另一个具有其状态的被跟踪条目。将角色分配给现有用户时,关系条目的状态设置为Added,但现有User不可能(因为它必须已分配角色),除非您将旧关系标记为{{ 1}}(或者除非您正在使用新用户)。在分离方案中解决此问题的是very hard,它会导致代码中必须在往返过程中传递有关旧角色的信息,并手动使用状态管理器或实体图本身。类似的东西:

Deleted

最简单的解决方案是从数据库加载旧状态,并将更改从新状态合并到加载(附加)状态。通过暴露FK属性也可以避免这种情况。

顺便说一下。你的模型不是一对一的,而是一对一的角色可以分配给多个用户 - 如果是一对一的话,它会更复杂,因为你必须在创建一个新角色之前删除旧角色