HasRequired和WithOptional不会生成一对一的关系

时间:2019-03-14 14:01:44

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

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

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实际上指向了我在数据库中创建的用户,即使我没有将用户分配给GenericObject。

4 个答案:

答案 0 :(得分:1)

您应该阅读: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:无法确定“ ef6tests.GenericObject_UserWhoGeneratedThisObject”关系的主要结尾。添加的多个实体可能具有相同的主键。

在简单情况下,一个新用户和一个新GenericObject EF可以推断处于相同状态的两个实体之间可能的唯一关系。在其他情况下,他会抛出。

答案 1 :(得分:1)

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)

例外

  

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

由于GenericObject是EF唯一执行insert查询的实体,因此失败,因为数据库中没有UserObject等于Id的{​​{1}}。

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

0

已执行查询

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) (Id = 0)和UserObject(Id = 0)。 EF认为GenericObject引用了GenericObject,因为它的外键等于0,而UserObject主键等于0。因此,EF首先插入UserObject作为主体,并且它认为UserObject依赖于该用户,因此它返回了GenericObject并执行了第二次插入操作,并且一切都很好。

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

UserObject.Id

例外

  

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

     

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

EF看到上下文中有2个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(); ,其中UserObject等于Id,而0也等于0,所以框架无法肯定地连接实体,因为那里有多个可能的选择。

一对多

GenericObject.Id

配置

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实体的外键。

测试1.仅创建GenericObject

UserObject中的代码相同。它执行相同的查询并产生相同的异常。原因是相同的。

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

one-to-one中的代码相同。

已执行查询

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的{​​{1}}实体和Id等于0的{​​{1}},因此EF认为它们已连接并执行GenericObject的插入取UserId并执行第二次插入。

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

UserObject中的代码相同。它执行相同的查询并产生相同的异常。原因是相同的。

从这些测试中您可以看到,问题并非特定于UserObject.Id关系。

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

  

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

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

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

null

one-to-one方法告诉EF在modelBuilder .Entity<GenericObject>() .HasRequired(u => u.UserWhoGeneratedThisObject) .WithOptional() .Map(config => { config.MapKey("UserId"); }); 中创建单独的Map外键。进行此更改后,您问题中的代码将引发异常

UserId
  

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

答案 2 :(得分:0)

您需要设置两个导航属性:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
   modelBuilder.Entity<GenericObject>()
      .HasRequired(genericObject => genericObject.UserWhoGeneratedThisObject)
      .WithOptional(user => user.GenericObject);
}

答案 3 :(得分:0)

  

在将myObject添加到数据库中时,我没有遇到异常,因为我没有为其分配User对象? (注释行)

我认为没有使用“ OnModelCreating”函数实现在数据库中创建一对一关系的原因。 应该创建将被“ [ForeignKey]”属性标记为外键的GenericObjectId属性,以在“ User”类中创建一对一关系。应将“ GenericObject”属性添加到“ User”类,并使用虚拟修饰符进行标记。现在,您的“ OnModelCreating”实现将配置这些类之间的关系。 有示例的例子:

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添加到新用户中,以避免发生此异常(如果您向用户放置外键,则可以避免这种情况。)

希望我能清楚地回答你的问题。