简体   繁体   中英

HasRequired and WithOptional not generating one-to-one relationship

I have, two very simple objects:

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

I am overriding the OnModelCreating function to declare the objects' relationships:

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

Then, in my application, I do the following:

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

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) I expected to see an exception in this case, but the code works OK. To make matters worse, if I add the following two lines after ctx.SaveChanges():

var u = ctx.GenericObjects.Find(1);
System.Console.WriteLine(u.ToString());

I see that the variable u which is a GenericObjects actually points to the user that I created in the database, even though I didn't assign the user to the GenericObject.

you should read: Difference between .WithMany() and .WithOptional()?

the code:

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

throws:

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.

In simple case one new User and one new GenericObject EF infers the only relation possible between two entities in the same state. In other cases he throws.

TL;DR

This behavior is not specific for one-to-one relationship, one-to-many behaves exactly the same way.

If you want to make EF throw an exception with your current code then map separate foreign key for GenericObject

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

Actual answer

Actually one-to-many relationship suffers from exactly the same problem. Let's do some tests.

One-to-one

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

Configuration

modelBuilder
    .Entity<GenericObject>()
    .HasRequired(u => u.UserWhoGeneratedThisObject)
    .WithOptional();

In this case GenericObject.Id is a primary key and a foreign key references UserObject entity at the same time.

Test 1. Create only GenericObject

var context = new AppContext();

GenericObject myObject = new GenericObject
{
    Description = "some description"
};

context.GenericObjects.Add(myObject);
context.SaveChanges();

Executed query

INSERT [dbo].[GenericObjects]([Id], [Description])
VALUES (@0, @1)

-- @0: '0' (Type = Int32)

-- @1: 'some description' (Type = String, Size = -1)

Exception

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.

Since GenericObject is the only entity EF executes insert query and it fails because there is no UserObject with Id equals 0 in database.

Test 2. Create 1 UserObject and a 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();

Executed queries

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)

Now context contains UserObject (Id = 0) and GenericObject (Id = 0). EF considers that GenericObject references to the UserObject because its foreign key equals 0 as well as UserObject primary key equals 0. So at first EF inserts UserObject as a principal and because it consider GenericObject dependent on that user it takes returned UserObject.Id and performs second insert with it and eveything is fine.

Test 3. Create 2 UserObject and a 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();

Exception

'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 sees that there are 2 UserObject in context with Id equals 0 and GenericObject.Id equals 0 as well, so the framework just unable to definitely connect entities because there are multiple possible options.

One-to-many

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

Configuration

modelBuilder
    .Entity<GenericObject>()
    .HasRequired(o => o.UserWhoGeneratedThisObject)
    .WithMany()
    .HasForeignKey(o => o.UserId);

In this case GenericObject has separate UserId as a foreign key referencing UserObject entity.

Test 1. Create only GenericObject

Same code as in one-to-one . It executes the same query and yields the same exception. Reason is the same.

Test 2. Create 1 UserObject and a GenericObject

Same code as in one-to-one .

Executed queries

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)

Executed queries are pretty similar to one-to-one test 2. The only difference is that now GenericObject has separate UserId foreign key. Reasoning is pretty the same. Context contains UserObject entity with Id equal 0 and GenericObject with UserId equals now so EF considers them connected and performs insert of UserObject then it takes UserObject.Id and performs second insert with it.

Test 3. Create 2 UserObject and a GenericObject

Same code as in one-to-one . It executes the same query and yields the same exception. Reason is the same.

As you can see from these tests the problem is not specific to one-to-one relationship.

But what if we don't add UserId to GenericObject ? In this case EF will generate UserWhoGeneratedThisObject_Id foreign key for us and now there is foreign key in database but no property mapped to it. In this case every single test will immediately throw the following exception

Entities in 'AppContext.GenericObjects' participate in the 'GenericObject_UserWhoGeneratedThisObject' relationship. 0 related 'GenericObject_UserWhoGeneratedThisObject_Target' were found. 1 'GenericObject_UserWhoGeneratedThisObject_Target' is expected.

Why this happens? Now EF unable to determine whether GenericObject and UserObject are connected because there is no foreign key property on GenericObject . In this case EF can rely only on navigation property UserWhoGeneratedThisObject which is null and therefore an exception is raised.

It means if you can achieve this situation for one-to-one when there is a foreign key in database but no property is mapped to it EF will throw the same exception for the code in your question. It's easy to accomplish by updating configuration

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

The Map method tells EF to create separate UserId foreign key in GenericObject . With this change the code in your question will throw an exception

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.

You need to set up both navigation properties:

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

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)

I think no because of one-to-one relation was not created in data base with your "OnModelCreating" function implementation. GenericObjectId property which will be marked as foreign key by "[ForeignKey]" attribute should be created to create one-to-one relation in "User" class. "GenericObject" property should be added to "User" class and marked with virtual modifier. Now your implementation of "OnModelCreating" will configure relationships between these classes. There are examples of models:

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:

Generic object should be created at first and created object id should be added to new user to avoid this exception(This is in case if you put foreign key to user).

Hopefully I answered for your question clearly.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM