Сложные отношения между сущностями с Entity Framework

Возможно, это дубликат, но я не смог найти подобной темы.

Я использую Entity Framework и имею две таблицы в своей базе данных:

public class A
{
    public virtual B B1 { get; set; }
    public virtual B B2 { get; set; }
}

public class B
{
    public virtual A A1 { get; set; }
}

И нет никакой связи между B1 и A1 или B2 и A1. Это как 3 односторонние отношения. Как вы можете сделать это в Entity Framework?

Я получаю эту ошибку:

An error occurred while saving entities that do not expose foreign key properties for their relationships

Кто-нибудь знает, как с этим справиться?

заранее спасибо

Итак, в традиционных терминах SQL (для которых предназначен EF) это отношение «многие ко многим» между B1 и B2, где A1 является таблицей соединений? Таким образом, вам понадобятся ключи для B1, B2 и A1, которые должны содержать эти ключи.

cjb110 26.06.2019 16:27

Каждому свойству навигации требуется соответствующий столбец или столбцы, которые составляют PK таблицы, на которую ссылаются.

juharr 26.06.2019 16:29

Может быть, я не ясно выразился, но A1, B1 и B2 не связаны. Если я устанавливаю B1, он не должен пытаться установить A1 в базе данных, но я думаю, что это то, что пытается сделать Entity Framework.

lema 26.06.2019 16:34

То есть вы имеете в виду, что A ссылается на две строки в B, а затем B ссылается на некоторую строку в A, которая не обязательно является строкой, которая ссылается на нее через B1 или B2? Как у вас нет навигационного свойства, идущего в обоих направлениях?

juharr 26.06.2019 16:39

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

juharr 26.06.2019 16:46

Это также похоже на проблему XY. Какие данные вы моделируете, что приводит к этому конкретному набору отношений?

juharr 26.06.2019 16:47

Да, это именно так, как вы сказали. Нет, у меня нет свойства навигации в обоих направлениях, если я хорошо понимаю, что это такое.

lema 26.06.2019 16:48

Да, поэтому EF ожидает свойства навигации в обоих направлениях. Похоже, у вас должно быть 3 свойства A в B (одно, на которое он ссылается, и два других, которые ссылаются на него), а затем 3 свойства B в A (два, на которые оно ссылается, и другое, которое ссылается на него).

juharr 26.06.2019 16:51

Ну, я думал об этом, но, возможно, было другое решение с Fluent API... Большое спасибо за вашу помощь!

lema 26.06.2019 16:53
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
9
299
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Если таблица A1 содержит B1_Id и B2_Id, указывающие на одну и ту же таблицу B, но вы ожидаете, что запись B будет связана с A только один раз, то, насколько я знаю о том, как происходит сопоставление, это невозможно. Ничто не помешает вам связать одну и ту же запись B с B1 или B2 с разными записями A, так как же будет юридически разрешаться ссылка B на A? Сущности отражают состояние данных, поэтому, если это допустимо/незаконно из схемы данных, это то же самое для сущности. Наличие идентификаторов B на A формирует множество к 1 или может формировать 1 к одному, но для совместного использования 2x FK с B на A вам потребуется, чтобы БД поддерживала альтернативные FK для B, чего нет.

Вы можете заставить A хранить идентификаторы для 2 записей в B, но B не может сопоставить одну ссылку с A, он должен ее подделать.

public class A
{
   public int AId { get; set; }

   public virtual B B1 { get; set; }
   public virtual B B2 { get; set; }
}

public class B
{
   public int BId { get; set; }

   public virtual ICollection<A> RawA1 { get; private set; } = new List<A>();
   public virtual ICollection<A> RawA2 { get; private set; } = new List<A>();

   [NotMapped]
   public A A
   {
      get { return RawA1.SingleOrDefault() ?? RawA2.SingleOrDefault(); }
   }
}

Предостережение заключается в том, что вы не можете использовать B.A в любом выражении Linq, переходящем в EF, потому что, что касается EF, он не знает об этом свойстве.

Альтернативой является использование 1-ко-многим, когда AId находится на B, а затем ограничение допустимых операций над коллекцией на стороне A. Проблема заключалась бы в том, что порядок B1 и B2 не был бы надежным, если бы он явно не определялся свойствами записи B. A может предоставить неотображенное свойство B1 и B2 из коллекции, но ему нужно будет определить, какой из двух элементов учитывать для каждого, или просто предоставить коллекцию. В конечном счете, именно ваша бизнес-логика должна будет контролировать тот факт, что A должен когда-либо иметь только 2 ссылки B, поскольку база данных не может обеспечить это, а также отношение 2 к 1, когда B может вернуться к A.

Ответ принят как подходящий

Поскольку вы не указываете, какую версию EF вы используете, давайте рассмотрим две текущие версии: EF 6.2.0 и EF-core 2.2.4.

С EF6 это просто. Отображение...

modelBuilder.Entity<A>().HasRequired(a => a.B1).WithMany();
modelBuilder.Entity<A>().HasRequired(a => a.B2).WithMany().WillCascadeOnDelete(false);
modelBuilder.Entity<B>().HasRequired(b => b.A1).WithMany().WillCascadeOnDelete(false);

... создает следующую модель базы данных (без учета индексов):

CREATE TABLE [dbo].[A] (
    [ID] [int] NOT NULL IDENTITY,
    [B1_ID] [int] NOT NULL,
    [B2_ID] [int] NOT NULL,
    CONSTRAINT [PK_dbo.A] PRIMARY KEY ([ID])
)
CREATE TABLE [dbo].[B] (
    [ID] [int] NOT NULL IDENTITY,
    [A1_ID] [int] NOT NULL,
    CONSTRAINT [PK_dbo.B] PRIMARY KEY ([ID])
)

.... в котором поля с _ являются внешними ключами, один из которых может иметь каскадное удаление.

С ef-core все не так однозначно, даже глючно вроде бы. Первый импульс эквивалентен EF6:

modelBuilder.Entity<A>().HasOne(a => a.B1).WithMany();
modelBuilder.Entity<A>().HasOne(a => a.B2).WithMany();
modelBuilder.Entity<B>().HasOne(b => b.A1).WithMany();

Но сгенерированная модель не такая, как можно было бы ожидать:

  CREATE TABLE [B] (
      [ID] int NOT NULL IDENTITY,
      [A1ID] int NULL,
      CONSTRAINT [PK_B] PRIMARY KEY ([ID])
  );
  CREATE TABLE [A] (
      [ID] int NOT NULL,
      [B1ID] int NULL,
      CONSTRAINT [PK_A] PRIMARY KEY ([ID]),
      CONSTRAINT [FK_A_B_B1ID] FOREIGN KEY ([B1ID]) REFERENCES [B] ([ID]) ON DELETE NO ACTION,
      CONSTRAINT [FK_A_B_ID] FOREIGN KEY ([ID]) REFERENCES [B] ([ID]) ON DELETE CASCADE
  );
  ALTER TABLE [B] ADD CONSTRAINT [FK_B_A_A1ID] FOREIGN KEY ([A1ID]) REFERENCES [A] ([ID]) ON DELETE NO ACTION;

Одна из ассоциаций A-B интерпретируется как 1:1. По моему это баг. Инструкция WithMany не должна оставлять места для интерпретации. Два, казалось бы, идентичных сопоставления создают совершенно разные отношения в базе данных. Этого не может быть.

Тем не менее, легко (но не обязательно) направить EF на правильный путь, назвав столбцы FK:

modelBuilder.Entity<A>().HasOne(a => a.B1).WithMany().HasForeignKey("B1_ID")
    .IsRequired();
modelBuilder.Entity<A>().HasOne(a => a.B2).WithMany().HasForeignKey("B2_ID")
    .IsRequired().OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<B>().HasOne(b => b.A1).WithMany().HasForeignKey("A1_ID")
    .IsRequired().OnDelete(DeleteBehavior.Restrict);

Производство (игнорируя индексы):

  CREATE TABLE [B] (
      [ID] int NOT NULL IDENTITY,
      [A1_ID] int NOT NULL,
      CONSTRAINT [PK_B] PRIMARY KEY ([ID])
  );
  CREATE TABLE [A] (
      [ID] int NOT NULL IDENTITY,
      [B1_ID] int NOT NULL,
      [B2_ID] int NOT NULL,
      CONSTRAINT [PK_A] PRIMARY KEY ([ID]),
      CONSTRAINT [FK_A_B_B1_ID] FOREIGN KEY ([B1_ID]) REFERENCES [B] ([ID]) ON DELETE CASCADE,
      CONSTRAINT [FK_A_B_B2_ID] FOREIGN KEY ([B2_ID]) REFERENCES [B] ([ID]) ON DELETE NO ACTION
  );
  ALTER TABLE [B] ADD CONSTRAINT [FK_B_A_A1_ID] FOREIGN KEY ([A1_ID]) REFERENCES [A] ([ID]) ON DELETE NO ACTION;

Обратите внимание, что поля внешнего ключа должны быть явно установлены обязательными (если они есть). Ну, это просто деталь реализации.

Другие вопросы по теме