HasRequired и WithOptional не генерируют отношения один-к-одному - PullRequest
3 голосов
/ 14 марта 2019

У меня есть два очень простых объекта:

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

Я вижу, что переменная u, которая является GenericObjects, фактически указывает на пользователя, которого я создал в базе данных,даже если я не назначил пользователя GenericObject.

Ответы [ 4 ]

1 голос
/ 14 марта 2019

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, равным 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 'произошло в EntityFramework.dll

Дополнительная информация: невозможно определить основной конец отношения TestConsole.Data.GenericObject_UserWhoGeneratedThisObject. Несколько добавленных объектов могут иметь один и тот же первичный ключ.

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 test 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, и теперь в базе данных есть внешний ключ, но ему не сопоставлено свойство. В этом случае каждый отдельный тест немедленно выдаст следующее исключение

Объекты в «AppContext.GenericObjects» участвуют в отношении «GenericObject_UserWhoGeneratedThisObject». 0 связанных 'GenericObject_UserWhoGeneratedThisObject_Target' были найдены. 1 'GenericObject_UserWhoGeneratedThisObject_Target' ожидается.

Почему это происходит?Теперь 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 создать отдельный 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' ожидается.

1 голос
/ 14 марта 2019

вы должны прочитать: Разница между .WithMany () и .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 определяют единственное возможное отношение между двумя объектами в одном и том же состоянии.В других случаях он бросает.

0 голосов
/ 14 марта 2019

Разве я не должен нажимать исключение при добавлении myObject в базу данных, так как я не назначил ему объект User?(Закомментированная строка)

Я думаю, что нет, потому что отношение «один к одному» не было создано в базе данных с вашей реализацией функции «OnModelCreating».Необходимо создать свойство GenericObjectId, которое будет помечено как внешний ключ атрибутом «[ForeignKey]» для создания взаимно-однозначного отношения в классе «Пользователь».Свойство «GenericObject» должно быть добавлено в класс «Пользователь» и отмечено виртуальным модификатором.Теперь ваша реализация «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; }
    }

После того, как отношения будут отображены, появится искомое исключение:

Сначала должен быть создан общий объект и идентификатор созданного объектаследует добавить к новому пользователю, чтобы избежать этого исключения (это в случае, если вы поставили внешний ключ для пользователя).

Надеюсь, я четко ответил на ваш вопрос.

0 голосов
/ 14 марта 2019

Вам необходимо настроить оба свойства навигации:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
   modelBuilder.Entity<GenericObject>()
      .HasRequired(genericObject => genericObject.UserWhoGeneratedThisObject)
      .WithOptional(user => user.GenericObject);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...