Поддерживает ли платформа Entity Framework циклические ссылки?
У меня есть два объекта в отношениях родителя/ребенка. Кроме того, родительский элемент содержит ссылку на "основной" дочерний элемент, поэтому упрощенная модель выглядит следующим образом:
class Parent
{
int ParentId;
int? MainChildId;
}
class Child
{
int ChildId;
int ParentId;
}
Проблема, которую я сейчас испытываю, заключается в том, что EF, похоже, не может обрабатывать создание как родительского, так и дочернего в одной операции. Я получаю сообщение об ошибке "System.Data.UpdateException: не удается определить допустимый порядок для зависимых операций. Зависимости могут существовать из-за ограничений внешнего ключа, требований к модели или значений, созданных магазином".
MainChildId имеет значение NULL, поэтому должно быть возможно создать родительский элемент, дочерний элемент, а затем обновить родителя с вновь созданным ChildId. Это то, что EF не поддерживает?
Ответы
Ответ 1
Нет, он поддерживается. Попробуйте его с помощью GUID-ключа или назначаемой последовательности. Ошибка означает, что именно это говорит: EF не может понять, как это сделать за один шаг. Вы можете сделать это в два шага, хотя (два вызова SaveChanges()
).
Ответ 2
У меня была эта точная проблема. Очевидная "Циркулярная ссылка" - это просто хороший дизайн базы данных. Наличие флага на дочерней таблице, как "IsMainChild", является плохим дизайном, атрибут "MainChild" является свойством родителя, а не дочернего, поэтому FK в родительском объекте является подходящим.
EF4.1 должен найти способ справиться с этими типами отношений изначально, а не заставлять нас редизайн наших баз данных для устранения недостатков в рамках.
Во всяком случае, обходной способ заключается в том, чтобы сделать несколько шагов (например, при написании хранимой процедуры, чтобы сделать то же самое) единственная морщина - это обойти отслеживание изменений в контексте.
Using context As New <<My DB Context>>
' assuming the parent and child are already attached to the context but not added to the database yet
' get a reference to the MainChild but remove the FK to the parent
Dim child As Child = parent.MainChild
child.ParentID = Nothing
' key bit detach the child from the tracking context so we are free to update the parent
' we have to drop down to the ObjectContext API for that
CType(context, IObjectContextAdapter).ObjectContext.Detach(child)
' clear the reference on the parent to the child
parent.MainChildID = Nothing
' save the parent
context.Parents.Add(parent)
context.SaveChanges()
' assign the newly added parent id to the child
child.ParentID = parent.ParentID
' save the new child
context.Children.Add(child)
context.SaveChanges()
' wire up the Fk on the parent and save again
parent.MainChildID = child.ChildID
context.SaveChanges()
' we're done wasn't that easier with EF?
End Using
Ответ 3
Как для EF, так и для LINQ to SQL эта проблема заключается в невозможности сохранения циклических ссылок, хотя они могут быть намного более полезными, просто инкапсулируя 2 или более SQL-запросов в транзакции за кулисами для вас, вместо того, чтобы бросать Исключение.
Я написал исправление для этого в LINQ to SQL, но пока еще не сделал этого в EF, потому что я только что избегал круговых ссылок в моем проекте db.
Что вы можете сделать, так это создать вспомогательный метод, который выделяет круговые ссылки, запускать их перед вызовом SaveChanges(), запускать другой метод, который возвращает циклические ссылки на место и снова вызывает SaveChanges(). Вы можете инкапсулировать все это одним способом, может быть SaveChangesWithCircularReferences()
.
Чтобы вернуть круговые ссылки, вам нужно отследить, что вы удалили и вернуть этот журнал.
public class RemovedReference() . . .
public List<RemovedReference> SetAsideReferences()
{
. . .
}
Таким образом, в основном код в SetAsideReferences - это поиск круговых ссылок, отбрасывание одной половины в каждом случае и запись их в список.
В моем случае я создал класс, который сохранил объект, имя свойства и значение (другой объект), который был удален, и просто сохранил их в списке, например:
public class RemovedReference
{
public object Object;
public string PropertyName;
public object Value;
}
Вероятно, более разумная структура для этого; вы могли бы использовать объект PropertyInfo, например, вместо строки, и вы можете кэшировать этот тип, чтобы удешевить второй раунд отражения.
Ответ 4
Это старый вопрос, который все еще имеет отношение к Entity Framework 6.2.0. Мое решение три раза:
- НЕ установите столбец
MainChildId
как HasDatabaseGeneratedOption(Computed)
(это блокирует вас от его обновления позже)
- Использовать триггер для обновления родителя, когда я вставляю обе записи одновременно (это не проблема, если родитель уже существует, и я просто добавляю новый ребенок, так что будьте уверены, что Trigger это так или иначе объясняет - было легко в моем случае)
- После вызова
ctx.SaveChanges()
также обязательно вызовите ctx.Entry(myParentEntity).Reload()
, чтобы получать обновления из столбца MainChildId
из триггера (EF не будет автоматически выбирать их).
В моем коде ниже Thing
является родителем, а ThingInstance
является дочерним и имеет следующие требования:
- Каждый раз, когда вставлен
Thing
(родительский элемент), необходимо также вставить ThingInstance
(child) и установить в качестве Thing
CurrentInstance
(основной ребенок).
- Другие
ThingInstances
(дети) могут быть добавлены в Thing
(родительский) с или без использования CurrentInstance
(основного ребенка)
Это привело к следующему дизайну:
* EF Consumer должен вставить обе записи, но оставить CurrentInstanceId
равным нулю, но обязательно установите ThingInstance.Thing
родительскому элементу.
* Триггер обнаружит, имеет ли значение ThingInstance.Thing.CurrentInstanceId
значение null. Если это так, то он обновит его до ThingInstance.Id
.
* EF Consumer должен перезагрузить/восстановить данные для просмотра любых обновлений с помощью триггера.
* Два раундных поездки по-прежнему необходимы, но необходим только один атомный вызов ctx.SaveChanges
, и мне не нужно иметь дело с ручными откатами.
* У меня есть дополнительный триггер для управления, и может быть более эффективный способ сделать это, чем то, что я сделал здесь с помощью курсора, но я никогда не буду делать этого в том, где производительность будет иметь значение.
База данных:
(Извините, не протестировал этот script - просто сгенерировал его из моей БД и поместил его сюда из-за того, что он спешил. Вы наверняка сможете получить важные бит отсюда.)
CREATE TABLE [dbo].[Thing](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[Something] [nvarchar](255) NOT NULL,
[CurrentInstanceId] [bigint] NULL,
CONSTRAINT [PK_Thing] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[ThingInstance](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[ThingId] [bigint] NOT NULL,
[SomethingElse] [nvarchar](255) NOT NULL,
CONSTRAINT [PK_ThingInstance] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Thing] WITH CHECK ADD CONSTRAINT [FK_Thing_ThingInstance] FOREIGN KEY([CurrentInstanceId])
REFERENCES [dbo].[ThingInstance] ([Id])
GO
ALTER TABLE [dbo].[Thing] CHECK CONSTRAINT [FK_Thing_ThingInstance]
GO
ALTER TABLE [dbo].[ThingInstance] WITH CHECK ADD CONSTRAINT [FK_ThingInstance_Thing] FOREIGN KEY([ThingId])
REFERENCES [dbo].[Thing] ([Id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[ThingInstance] CHECK CONSTRAINT [FK_ThingInstance_Thing]
GO
CREATE TRIGGER [dbo].[TR_ThingInstance_Insert]
ON [dbo].[ThingInstance]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @thingId bigint;
DECLARE @instanceId bigint;
declare cur CURSOR LOCAL for
select Id, ThingId from INSERTED
open cur
fetch next from cur into @instanceId, @thingId
while @@FETCH_STATUS = 0 BEGIN
DECLARE @CurrentInstanceId bigint = NULL;
SELECT @CurrentInstanceId=CurrentInstanceId FROM Thing WHERE [email protected]
IF @CurrentInstanceId IS NULL
BEGIN
UPDATE Thing SET [email protected] WHERE [email protected]
END
fetch next from cur into @instanceId, @thingId
END
close cur
deallocate cur
END
GO
ALTER TABLE [dbo].[ThingInstance] ENABLE TRIGGER [TR_ThingInstance_Insert]
GO
С# Вставки:
public Thing Inserts(long currentId, string something)
{
using (var ctx = new MyContext())
{
Thing dbThing;
ThingInstance instance;
if (currentId > 0)
{
dbThing = ctx.Things
.Include(t => t.CurrentInstance)
.Single(t => t.Id == currentId);
instance = dbThing.CurrentInstance;
}
else
{
dbThing = new Thing();
instance = new ThingInstance
{
Thing = dbThing,
SomethingElse = "asdf"
};
ctx.ThingInstances.Add(instance);
}
dbThing.Something = something;
ctx.SaveChanges();
ctx.Entry(dbThing).Reload();
return dbThing;
}
}
С# Новый ребенок:
public Thing AddInstance(long thingId)
{
using (var ctx = new MyContext())
{
var dbThing = ctx.Things
.Include(t => t.CurrentInstance)
.Single(t => t.Id == thingId);
dbThing.CurrentInstance = new ThingInstance { SomethingElse = "qwerty", ThingId = dbThing.Id };
ctx.SaveChanges(); // Reload not necessary here
return dbThing;
}
}