Возможно, это дубликат, но я не смог найти подобной темы.
Я использую 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
Кто-нибудь знает, как с этим справиться?
заранее спасибо
Каждому свойству навигации требуется соответствующий столбец или столбцы, которые составляют PK таблицы, на которую ссылаются.
Может быть, я не ясно выразился, но A1, B1 и B2 не связаны. Если я устанавливаю B1, он не должен пытаться установить A1 в базе данных, но я думаю, что это то, что пытается сделать Entity Framework.
То есть вы имеете в виду, что A ссылается на две строки в B, а затем B ссылается на некоторую строку в A, которая не обязательно является строкой, которая ссылается на нее через B1 или B2? Как у вас нет навигационного свойства, идущего в обоих направлениях?
Это могут быть отношения один к одному, но я предполагаю, что вам понадобятся таблицы сопоставления между ними, чтобы все было правильно.
Это также похоже на проблему XY. Какие данные вы моделируете, что приводит к этому конкретному набору отношений?
Да, это именно так, как вы сказали. Нет, у меня нет свойства навигации в обоих направлениях, если я хорошо понимаю, что это такое.
Да, поэтому EF ожидает свойства навигации в обоих направлениях. Похоже, у вас должно быть 3 свойства A в B (одно, на которое он ссылается, и два других, которые ссылаются на него), а затем 3 свойства B в A (два, на которые оно ссылается, и другое, которое ссылается на него).
Ну, я думал об этом, но, возможно, было другое решение с Fluent API... Большое спасибо за вашу помощь!





Если таблица 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;
Обратите внимание, что поля внешнего ключа должны быть явно установлены обязательными (если они есть). Ну, это просто деталь реализации.
Итак, в традиционных терминах SQL (для которых предназначен EF) это отношение «многие ко многим» между B1 и B2, где A1 является таблицей соединений? Таким образом, вам понадобятся ключи для B1, B2 и A1, которые должны содержать эти ключи.