我正在尝试使用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.");
}
}
}
当然这是一个被覆盖的场景,我猜这是我在定义模型映射的方式中缺少的东西?
答案 0 :(得分:7)
你已经破坏了EF状态模型。您使用强制CurrentRole
映射了您的实体,因此EF知道如果没有User
,您就无法拥有现有Role
。您还使用了independent associations(在您的实体上没有公开FK属性)。这意味着角色和用户之间的关系是另一个具有其状态的被跟踪条目。将角色分配给现有用户时,关系条目的状态设置为Added
,但现有User
不可能(因为它必须已分配角色),除非您将旧关系标记为{{ 1}}(或者除非您正在使用新用户)。在分离方案中解决此问题的是very hard,它会导致代码中必须在往返过程中传递有关旧角色的信息,并手动使用状态管理器或实体图本身。类似的东西:
Deleted
最简单的解决方案是从数据库加载旧状态,并将更改从新状态合并到加载(附加)状态。通过暴露FK属性也可以避免这种情况。
顺便说一下。你的模型不是一对一的,而是一对一的角色可以分配给多个用户 - 如果是一对一的话,它会更复杂,因为你必须在创建一个新角色之前删除旧角色