首页 > 解决方案 > HasRequired 和 WithOptional 不生成一对一关系

问题描述

我有两个非常简单的对象:

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。

标签: c#entity-frameworkvisual-studio-2017entity-framework-6fluent

解决方案


TL;博士

此行为并非特定于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)

例外

INSERT 语句与 FOREIGN KEY 约束“FK_dbo.GenericObjects_dbo.UserObjects_Id”冲突。冲突发生在数据库“TestProject”、表“dbo.UserObjects”、列“Id”中。该语句已终止。

因为GenericObject是唯一实体 EF 执行查询并且它失败,因为数据库中insert没有等于。UserObjectId0

测试 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。所以首先 EFUserObject作为主体插入,并且因为它考虑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();

例外

EntityFramework.dll 中发生“System.Data.Entity.Infrastructure.DbUpdateException”

附加信息:无法确定“TestConsole.Data.GenericObject_UserWhoGeneratedThisObject”关系的主体端。多个添加的实体可能具有相同的主键。

EF 看到UserObject上下文中有 2 与Idequals0GenericObject.Idequals 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)

执行的查询与测试 2 非常相似one-to-one。唯一的区别是现在GenericObject有单独的UserId外键。推理几乎是一样的。上下文包含具有等于和等于现在的UserObject实体,因此 EF 认为它们已连接并执行插入,然后它需要并执行第二次插入。Id0GenericObjectUserIdUserObjectUserObject.Id

测试 3. 创建 2 个 UserObject 和一个 GenericObject

与中相同的代码one-to-one。它执行相同的查询并产生相同的异常。原因是一样的。

正如您从这些测试中看到的那样,问题并非特定于one-to-one关系。

但是如果我们不添加怎么UserIdGenericObject?在这种情况下,EF 将为UserWhoGeneratedThisObject_Id我们生成外键,现在数据库中有外键但没有映射到它的属性。在这种情况下,每个测试都会立即抛出以下异常

“AppContext.GenericObjects”中的实体参与“GenericObject_UserWhoGeneratedThisObject”关系。找到 0 个相关的“GenericObject_UserWhoGeneratedThisObject_Target”。1 'GenericObject_UserWhoGeneratedThisObject_Target' 是预期的。

为什么会发生这种情况?现在 EF 无法确定GenericObject和是否UserObject已连接,因为 上没有外键属性GenericObject。在这种情况下,EF 只能依赖导航属性UserWhoGeneratedThisObjectnull因此会引发异常。

one-to-one这意味着如果您可以在数据库中有外键但没有属性映射到它的情况下实现这种情况, EF 将为您的问题中的代码抛出相同的异常。通过更新配置很容易完成

modelBuilder
    .Entity<GenericObject>()
    .HasRequired(u => u.UserWhoGeneratedThisObject)
    .WithOptional()
    .Map(config =>
    {
        config.MapKey("UserId");
    });

Map方法告诉 EFUserIdGenericObject. 通过此更改,您问题中的代码将引发异常

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

“AppContext.GenericObjects”中的实体参与“GenericObject_UserWhoGeneratedThisObject”关系。找到 0 个相关的“GenericObject_UserWhoGeneratedThisObject_Target”。1 'GenericObject_UserWhoGeneratedThisObject_Target' 是预期的。


推荐阅读