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

问题描述 投票:3回答:4

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

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-framework visual-studio-2017 entity-framework-6 fluent
4个回答
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
投票

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查询的唯一实体,因此在数据库中没有UserObjectId等于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并执行第二次插入它和eveything很好。

测试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等于Id0等于0的情况下有2个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);

在这种情况下,GenericObjectUserId作为引用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等于0GenericObjectUserId等于现在,因此EF认为它们连接并执行插入UserObject然后它需要UserObject.Id并执行第二次插入。

测试3.创建2个UserObject和GenericObject

one-to-one相同的代码。它执行相同的查询并产生相同的异常。理由是一样的。

从这些测试中可以看出,问题并不是one-to-one关系所特有的。

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

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

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

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

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

Map方法告诉EF在UserId中创建单独的GenericObject外键。通过此更改,您的问题中的代码将引发异常

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'是预期的。


0
投票

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

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

0
投票

我将myObject添加到数据库时不应该遇到异常,因为我没有为它分配一个User对象吗? (注释掉的行)

我认为不,因为在您的“OnModelCreating”函数实现的数据库中没有创建一对一的关系。应创建将通过“[ForeignKey]”属性标记为外键的GenericObjectId属性,以在“User”类中创建一对一关系。应将“GenericObject”属性添加到“User”类并使用virtual modifier标记。现在,您的“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添加到新用户以避免此异常(如果您将外键放入用户,则会出现这种情况)。

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

© www.soinside.com 2019 - 2024. All rights reserved.