HasRequired 和 WithOptional 不生成一对一关系
HasRequired and WithOptional not generating one-to-one relationship
我有两个非常简单的对象:
public class GenericObject
{
public int Id { get; set; }
public string description { get; set; }
public User UserWhoGeneratedThisObject { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Lastname { get; set; }
}
我正在覆盖 OnModelCreating 函数来声明对象的关系:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<GenericObject>().HasRequired(u => u.UserWhoGeneratedThisObject)
.WithOptional();
}
然后,在我的应用程序中,我执行以下操作:
using (var ctx = new TestContext())
{
GenericObject myObject = new GenericObject();
myObject.description = "testobjectdescription";
User usr = new User() { Name = "Test"};
ctx.Users.Add(usr);
//myObject.UserWhoGeneratedThisObject = usr;
ctx.GenericObjects.Add(myObject);
ctx.SaveChanges();
}
当我将 myObject 添加到数据库时,我不应该遇到异常吗,因为我没有为它分配一个 User 对象? (注释掉的行)我希望在这种情况下看到异常,但代码工作正常。更糟糕的是,如果我在 ctx.SaveChanges() 之后添加以下两行:
var u = ctx.GenericObjects.Find(1);
System.Console.WriteLine(u.ToString());
我看到作为 GenericObjects 的变量 u 实际上指向我在数据库中创建的用户,即使我没有将用户分配给 GenericObject。
您需要设置两个导航属性:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<GenericObject>()
.HasRequired(genericObject => genericObject.UserWhoGeneratedThisObject)
.WithOptional(user => user.GenericObject);
}
你应该阅读:Difference between .WithMany() and .WithOptional()?
代码:
using (var ctx = new Tests6Context()) {
GenericObject myObject = new GenericObject();
myObject.description = "testobjectdescription";
User usr = new User() { Name = "Test" };
ctx.Users.Add(usr);
//myObject.UserWhoGeneratedThisObject = usr;
ctx.GenericObjects.Add(myObject);
myObject = new GenericObject();
myObject.description = "testobjectdescription 2";
usr = new User() { Name = "Test 2" };
ctx.Users.Add(usr);
//myObject.UserWhoGeneratedThisObject = usr;
ctx.GenericObjects.Add(myObject);
ctx.SaveChanges();
}
抛出:
System.Data.Entity.Infrastructure.DbUpdateException: Unable to determine the principal end of the 'ef6tests.GenericObject_UserWhoGeneratedThisObject' relationship. Multiple added entities may have the same primary key.
在简单情况下,一个新用户和一个新 GenericObject EF 推断出处于同一状态的两个实体之间唯一可能的关系。在其他情况下,他会抛出。
Shouldn't I be hitting an exception when I am adding the myObject to the database, since I haven't assigned it a User object? (The commented out line)
我认为没有,因为没有在数据库中使用您的 "OnModelCreating" 函数实现创建一对一关系。
GenericObjectId 属性 将被“[ForeignKey]”属性标记为外键,应创建以在 "User" class 中创建一对一关系。 "GenericObject" 属性 应添加到 "User" class 并用 virtual 修饰符标记。现在,您的 "OnModelCreating" 实现将配置这些 class 之间的关系。
有机型示例:
public class User
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public string Lastname { get; set; }
[ForeignKey("GenericObject")]
public int GenericObjectId { get; set; }
public virtual GenericObject GenericObject { get; set; }
}
public class GenericObject
{
[Key]
public int Id { get; set; }
public string Description { get; set; }
public virtual User UserWhoGeneratedThisObject { get; set; }
}
After relations will be mapped, searched exception will be appeared:
应首先创建通用对象,并应将创建的对象 ID 添加到新用户以避免此异常(如果您将外键放入用户)。
希望我清楚地回答了你的问题。
TL;DR
此行为并非特定于 one-to-one
关系,one-to-many
的行为方式完全相同。
如果您想让 EF 使用当前代码抛出异常,请为 GenericObject
映射单独的外键
modelBuilder
.Entity<GenericObject>()
.HasRequired(u => u.UserWhoGeneratedThisObject)
.WithOptional()
.Map(config =>
{
config.MapKey("UserId");
});
实际回答
实际上 one-to-many
关系也遇到了完全相同的问题。让我们做一些测试。
一对一
public class GenericObject
{
public int Id { get; set; }
public string Description { get; set; }
public UserObject UserWhoGeneratedThisObject { get; set; }
}
public class UserObject
{
public int Id { get; set; }
public string Name { get; set; }
}
配置
modelBuilder
.Entity<GenericObject>()
.HasRequired(u => u.UserWhoGeneratedThisObject)
.WithOptional();
在这种情况下 GenericObject.Id
是主键和外键同时引用 UserObject
实体。
测试1.只创建GenericObject
var context = new AppContext();
GenericObject myObject = new GenericObject
{
Description = "some description"
};
context.GenericObjects.Add(myObject);
context.SaveChanges();
执行查询
INSERT [dbo].[GenericObjects]([Id], [Description])
VALUES (@0, @1)
-- @0: '0' (Type = Int32)
-- @1: 'some description' (Type = String, Size = -1)
异常
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_dbo.GenericObjects_dbo.UserObjects_Id". The conflict occurred in database "TestProject", table "dbo.UserObjects", column 'Id'.
The statement has been terminated.
因为 GenericObject
是唯一的实体 EF 执行 insert
查询并且它失败了,因为在数据库中没有 UserObject
with Id
等于 0
。
测试 2. 创建 1 个 UserObject 和一个 GenericObject
var context = new AppContext();
GenericObject myObject = new GenericObject
{
Description = "some description"
};
UserObject user = new UserObject
{
Name = "user"
};
context.UserObjects.Add(user);
context.GenericObjects.Add(myObject);
context.SaveChanges();
执行的查询
INSERT [dbo].[UserObjects]([Name])
VALUES (@0)
SELECT [Id]
FROM [dbo].[UserObjects]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'user' (Type = String, Size = -1)
INSERT [dbo].[GenericObjects]([Id], [Description])
VALUES (@0, @1)
-- @0: '10' (Type = Int32)
-- @1: 'some description' (Type = String, Size = -1)
现在上下文包含 UserObject
(Id = 0) 和 GenericObject
(Id = 0)。 EF 认为 GenericObject
引用 UserObject
因为它的外键等于 0 以及 UserObject
主键等于 0。所以首先 EF 插入 UserObject
作为主体,因为它认为 GenericObject
取决于它返回 UserObject.Id
的那个用户,并用它执行第二次插入,一切都很好。
测试 3. 创建 2 个 UserObject 和一个 GenericObject
var context = new AppContext();
GenericObject myObject = new GenericObject
{
Description = "some description"
};
UserObject user = new UserObject
{
Name = "user"
};
UserObject user2 = new UserObject
{
Name = "user"
};
context.UserObjects.Add(user);
context.UserObjects.Add(user2);
context.GenericObjects.Add(myObject);
context.SaveChanges();
异常
'System.Data.Entity.Infrastructure.DbUpdateException' occurred in EntityFramework.dll
Additional information: Unable to determine the principal end of the 'TestConsole.Data.GenericObject_UserWhoGeneratedThisObject' relationship. Multiple added entities may have the same primary key.
EF 看到上下文中有 2 个 UserObject
,Id
等于 0
,GenericObject.Id
也等于 0,因此框架无法明确连接实体因为有多种可能的选择。
一对多
public class GenericObject
{
public int Id { get; set; }
public string Description { get; set; }
public int UserId { get; set; } //this property is essential to illistrate the problem
public UserObject UserWhoGeneratedThisObject { get; set; }
}
public class UserObject
{
public int Id { get; set; }
public string Name { get; set; }
}
配置
modelBuilder
.Entity<GenericObject>()
.HasRequired(o => o.UserWhoGeneratedThisObject)
.WithMany()
.HasForeignKey(o => o.UserId);
在这种情况下 GenericObject
有单独的 UserId
作为引用 UserObject
实体的外键。
测试1.只创建GenericObject
与 one-to-one
中的代码相同。它执行相同的查询并产生相同的异常。道理是一样的。
测试 2. 创建 1 个 UserObject 和一个 GenericObject
与 one-to-one
中的代码相同。
执行的查询
INSERT [dbo].[UserObjects]([Name])
VALUES (@0)
SELECT [Id]
FROM [dbo].[UserObjects]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'user' (Type = String, Size = -1)
-- Executing at 14.03.2019 18:52:35 +02:00
INSERT [dbo].[GenericObjects]([Description], [UserId])
VALUES (@0, @1)
SELECT [Id]
FROM [dbo].[GenericObjects]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'some description' (Type = String, Size = -1)
-- @1: '3' (Type = Int32)
执行的查询与 one-to-one
测试 2 非常相似。唯一的区别是现在 GenericObject
有单独的 UserId
外键。推理是完全一样的。上下文包含 UserObject
实体 Id
等于 0
和 GenericObject
与 UserId
现在相等,因此 EF 认为它们已连接并执行插入 UserObject
然后它接受 UserObject.Id
并用它执行第二次插入。
测试 3. 创建 2 个 UserObject 和一个 GenericObject
与 one-to-one
中的代码相同。它执行相同的查询并产生相同的异常。道理是一样的。
从这些测试中可以看出,问题并非特定于 one-to-one
关系。
但是如果我们不把 UserId
添加到 GenericObject
呢?在这种情况下,EF 将为我们生成 UserWhoGeneratedThisObject_Id
外键,现在数据库中有外键但没有 属性 映射到它。在这种情况下,每个单独的测试都会立即抛出以下异常
Entities in 'AppContext.GenericObjects' participate in the 'GenericObject_UserWhoGeneratedThisObject' relationship. 0 related 'GenericObject_UserWhoGeneratedThisObject_Target' were found. 1 'GenericObject_UserWhoGeneratedThisObject_Target' is expected.
为什么会这样?现在 EF 无法确定 GenericObject
和 UserObject
是否连接,因为 GenericObject
上没有外键 属性。在这种情况下,EF 只能依赖导航 属性 UserWhoGeneratedThisObject
,即 null
,因此会引发异常。
这意味着当数据库中有外键但没有 属性 映射到它时,如果您可以实现 one-to-one
的这种情况,EF 将对您问题中的代码抛出相同的异常.更新配置即可轻松完成
modelBuilder
.Entity<GenericObject>()
.HasRequired(u => u.UserWhoGeneratedThisObject)
.WithOptional()
.Map(config =>
{
config.MapKey("UserId");
});
Map
方法告诉 EF 在 GenericObject
中创建单独的 UserId
外键。通过此更改,您问题中的代码将引发异常
using (var ctx = new TestContext())
{
GenericObject myObject = new GenericObject();
myObject.description = "testobjectdescription";
User usr = new User() { Name = "Test"};
ctx.Users.Add(usr);
//myObject.UserWhoGeneratedThisObject = usr;
ctx.GenericObjects.Add(myObject);
ctx.SaveChanges();
}
Entities in 'AppContext.GenericObjects' participate in the 'GenericObject_UserWhoGeneratedThisObject' relationship. 0 related 'GenericObject_UserWhoGeneratedThisObject_Target' were found. 1 'GenericObject_UserWhoGeneratedThisObject_Target' is expected.
我有两个非常简单的对象:
public class GenericObject
{
public int Id { get; set; }
public string description { get; set; }
public User UserWhoGeneratedThisObject { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Lastname { get; set; }
}
我正在覆盖 OnModelCreating 函数来声明对象的关系:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<GenericObject>().HasRequired(u => u.UserWhoGeneratedThisObject)
.WithOptional();
}
然后,在我的应用程序中,我执行以下操作:
using (var ctx = new TestContext())
{
GenericObject myObject = new GenericObject();
myObject.description = "testobjectdescription";
User usr = new User() { Name = "Test"};
ctx.Users.Add(usr);
//myObject.UserWhoGeneratedThisObject = usr;
ctx.GenericObjects.Add(myObject);
ctx.SaveChanges();
}
当我将 myObject 添加到数据库时,我不应该遇到异常吗,因为我没有为它分配一个 User 对象? (注释掉的行)我希望在这种情况下看到异常,但代码工作正常。更糟糕的是,如果我在 ctx.SaveChanges() 之后添加以下两行:
var u = ctx.GenericObjects.Find(1);
System.Console.WriteLine(u.ToString());
我看到作为 GenericObjects 的变量 u 实际上指向我在数据库中创建的用户,即使我没有将用户分配给 GenericObject。
您需要设置两个导航属性:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<GenericObject>()
.HasRequired(genericObject => genericObject.UserWhoGeneratedThisObject)
.WithOptional(user => user.GenericObject);
}
你应该阅读:Difference between .WithMany() and .WithOptional()?
代码:
using (var ctx = new Tests6Context()) {
GenericObject myObject = new GenericObject();
myObject.description = "testobjectdescription";
User usr = new User() { Name = "Test" };
ctx.Users.Add(usr);
//myObject.UserWhoGeneratedThisObject = usr;
ctx.GenericObjects.Add(myObject);
myObject = new GenericObject();
myObject.description = "testobjectdescription 2";
usr = new User() { Name = "Test 2" };
ctx.Users.Add(usr);
//myObject.UserWhoGeneratedThisObject = usr;
ctx.GenericObjects.Add(myObject);
ctx.SaveChanges();
}
抛出:
System.Data.Entity.Infrastructure.DbUpdateException: Unable to determine the principal end of the 'ef6tests.GenericObject_UserWhoGeneratedThisObject' relationship. Multiple added entities may have the same primary key.
在简单情况下,一个新用户和一个新 GenericObject EF 推断出处于同一状态的两个实体之间唯一可能的关系。在其他情况下,他会抛出。
Shouldn't I be hitting an exception when I am adding the myObject to the database, since I haven't assigned it a User object? (The commented out line)
我认为没有,因为没有在数据库中使用您的 "OnModelCreating" 函数实现创建一对一关系。 GenericObjectId 属性 将被“[ForeignKey]”属性标记为外键,应创建以在 "User" class 中创建一对一关系。 "GenericObject" 属性 应添加到 "User" class 并用 virtual 修饰符标记。现在,您的 "OnModelCreating" 实现将配置这些 class 之间的关系。 有机型示例:
public class User
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public string Lastname { get; set; }
[ForeignKey("GenericObject")]
public int GenericObjectId { get; set; }
public virtual GenericObject GenericObject { get; set; }
}
public class GenericObject
{
[Key]
public int Id { get; set; }
public string Description { get; set; }
public virtual User UserWhoGeneratedThisObject { get; set; }
}
After relations will be mapped, searched exception will be appeared:
应首先创建通用对象,并应将创建的对象 ID 添加到新用户以避免此异常(如果您将外键放入用户)。
希望我清楚地回答了你的问题。
TL;DR
此行为并非特定于 one-to-one
关系,one-to-many
的行为方式完全相同。
如果您想让 EF 使用当前代码抛出异常,请为 GenericObject
modelBuilder
.Entity<GenericObject>()
.HasRequired(u => u.UserWhoGeneratedThisObject)
.WithOptional()
.Map(config =>
{
config.MapKey("UserId");
});
实际回答
实际上 one-to-many
关系也遇到了完全相同的问题。让我们做一些测试。
一对一
public class GenericObject
{
public int Id { get; set; }
public string Description { get; set; }
public UserObject UserWhoGeneratedThisObject { get; set; }
}
public class UserObject
{
public int Id { get; set; }
public string Name { get; set; }
}
配置
modelBuilder
.Entity<GenericObject>()
.HasRequired(u => u.UserWhoGeneratedThisObject)
.WithOptional();
在这种情况下 GenericObject.Id
是主键和外键同时引用 UserObject
实体。
测试1.只创建GenericObject
var context = new AppContext();
GenericObject myObject = new GenericObject
{
Description = "some description"
};
context.GenericObjects.Add(myObject);
context.SaveChanges();
执行查询
INSERT [dbo].[GenericObjects]([Id], [Description])
VALUES (@0, @1)
-- @0: '0' (Type = Int32)
-- @1: 'some description' (Type = String, Size = -1)
异常
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_dbo.GenericObjects_dbo.UserObjects_Id". The conflict occurred in database "TestProject", table "dbo.UserObjects", column 'Id'. The statement has been terminated.
因为 GenericObject
是唯一的实体 EF 执行 insert
查询并且它失败了,因为在数据库中没有 UserObject
with Id
等于 0
。
测试 2. 创建 1 个 UserObject 和一个 GenericObject
var context = new AppContext();
GenericObject myObject = new GenericObject
{
Description = "some description"
};
UserObject user = new UserObject
{
Name = "user"
};
context.UserObjects.Add(user);
context.GenericObjects.Add(myObject);
context.SaveChanges();
执行的查询
INSERT [dbo].[UserObjects]([Name])
VALUES (@0)
SELECT [Id]
FROM [dbo].[UserObjects]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'user' (Type = String, Size = -1)
INSERT [dbo].[GenericObjects]([Id], [Description])
VALUES (@0, @1)
-- @0: '10' (Type = Int32)
-- @1: 'some description' (Type = String, Size = -1)
现在上下文包含 UserObject
(Id = 0) 和 GenericObject
(Id = 0)。 EF 认为 GenericObject
引用 UserObject
因为它的外键等于 0 以及 UserObject
主键等于 0。所以首先 EF 插入 UserObject
作为主体,因为它认为 GenericObject
取决于它返回 UserObject.Id
的那个用户,并用它执行第二次插入,一切都很好。
测试 3. 创建 2 个 UserObject 和一个 GenericObject
var context = new AppContext();
GenericObject myObject = new GenericObject
{
Description = "some description"
};
UserObject user = new UserObject
{
Name = "user"
};
UserObject user2 = new UserObject
{
Name = "user"
};
context.UserObjects.Add(user);
context.UserObjects.Add(user2);
context.GenericObjects.Add(myObject);
context.SaveChanges();
异常
'System.Data.Entity.Infrastructure.DbUpdateException' occurred in EntityFramework.dll
Additional information: Unable to determine the principal end of the 'TestConsole.Data.GenericObject_UserWhoGeneratedThisObject' relationship. Multiple added entities may have the same primary key.
EF 看到上下文中有 2 个 UserObject
,Id
等于 0
,GenericObject.Id
也等于 0,因此框架无法明确连接实体因为有多种可能的选择。
一对多
public class GenericObject
{
public int Id { get; set; }
public string Description { get; set; }
public int UserId { get; set; } //this property is essential to illistrate the problem
public UserObject UserWhoGeneratedThisObject { get; set; }
}
public class UserObject
{
public int Id { get; set; }
public string Name { get; set; }
}
配置
modelBuilder
.Entity<GenericObject>()
.HasRequired(o => o.UserWhoGeneratedThisObject)
.WithMany()
.HasForeignKey(o => o.UserId);
在这种情况下 GenericObject
有单独的 UserId
作为引用 UserObject
实体的外键。
测试1.只创建GenericObject
与 one-to-one
中的代码相同。它执行相同的查询并产生相同的异常。道理是一样的。
测试 2. 创建 1 个 UserObject 和一个 GenericObject
与 one-to-one
中的代码相同。
执行的查询
INSERT [dbo].[UserObjects]([Name])
VALUES (@0)
SELECT [Id]
FROM [dbo].[UserObjects]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'user' (Type = String, Size = -1)
-- Executing at 14.03.2019 18:52:35 +02:00
INSERT [dbo].[GenericObjects]([Description], [UserId])
VALUES (@0, @1)
SELECT [Id]
FROM [dbo].[GenericObjects]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'some description' (Type = String, Size = -1)
-- @1: '3' (Type = Int32)
执行的查询与 one-to-one
测试 2 非常相似。唯一的区别是现在 GenericObject
有单独的 UserId
外键。推理是完全一样的。上下文包含 UserObject
实体 Id
等于 0
和 GenericObject
与 UserId
现在相等,因此 EF 认为它们已连接并执行插入 UserObject
然后它接受 UserObject.Id
并用它执行第二次插入。
测试 3. 创建 2 个 UserObject 和一个 GenericObject
与 one-to-one
中的代码相同。它执行相同的查询并产生相同的异常。道理是一样的。
从这些测试中可以看出,问题并非特定于 one-to-one
关系。
但是如果我们不把 UserId
添加到 GenericObject
呢?在这种情况下,EF 将为我们生成 UserWhoGeneratedThisObject_Id
外键,现在数据库中有外键但没有 属性 映射到它。在这种情况下,每个单独的测试都会立即抛出以下异常
Entities in 'AppContext.GenericObjects' participate in the 'GenericObject_UserWhoGeneratedThisObject' relationship. 0 related 'GenericObject_UserWhoGeneratedThisObject_Target' were found. 1 'GenericObject_UserWhoGeneratedThisObject_Target' is expected.
为什么会这样?现在 EF 无法确定 GenericObject
和 UserObject
是否连接,因为 GenericObject
上没有外键 属性。在这种情况下,EF 只能依赖导航 属性 UserWhoGeneratedThisObject
,即 null
,因此会引发异常。
这意味着当数据库中有外键但没有 属性 映射到它时,如果您可以实现 one-to-one
的这种情况,EF 将对您问题中的代码抛出相同的异常.更新配置即可轻松完成
modelBuilder
.Entity<GenericObject>()
.HasRequired(u => u.UserWhoGeneratedThisObject)
.WithOptional()
.Map(config =>
{
config.MapKey("UserId");
});
Map
方法告诉 EF 在 GenericObject
中创建单独的 UserId
外键。通过此更改,您问题中的代码将引发异常
using (var ctx = new TestContext())
{
GenericObject myObject = new GenericObject();
myObject.description = "testobjectdescription";
User usr = new User() { Name = "Test"};
ctx.Users.Add(usr);
//myObject.UserWhoGeneratedThisObject = usr;
ctx.GenericObjects.Add(myObject);
ctx.SaveChanges();
}
Entities in 'AppContext.GenericObjects' participate in the 'GenericObject_UserWhoGeneratedThisObject' relationship. 0 related 'GenericObject_UserWhoGeneratedThisObject_Target' were found. 1 'GenericObject_UserWhoGeneratedThisObject_Target' is expected.