Поддерживает ли Entity Framework циклические ссылки? - PullRequest
13 голосов
/ 23 декабря 2010

У меня есть две сущности в отношениях родитель / ребенок. Кроме того, parent содержит ссылку на «основного» дочернего элемента, поэтому упрощенная модель выглядит следующим образом:

class Parent
{
   int ParentId;
   int? MainChildId;
}

class Child
{
   int ChildId;
   int ParentId;
}

Проблема, с которой я сейчас сталкиваюсь, заключается в том, что EF, похоже, не в состоянии обрабатывать создание Parent и Child в одной операции. Я получаю сообщение об ошибке «System.Data.UpdateException: невозможно определить действительный порядок для зависимых операций. Зависимости могут существовать из-за ограничений внешнего ключа, требований модели или значений, созданных хранилищем».

MainChildId имеет значение NULL, поэтому должна быть возможность сгенерировать родителя, потомка и затем обновить родителя с помощью вновь сгенерированного ChildId. Это то, что EF не поддерживает?

Ответы [ 4 ]

5 голосов
/ 14 октября 2011

У меня была именно эта проблема. Кажущаяся «круговая ссылка» - это просто хороший дизайн базы данных. Наличие флага на дочерней таблице, такой как «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  
5 голосов
/ 23 декабря 2010

Нет, это поддерживается.Попробуйте это с помощью ключа GUID или назначаемой последовательности.Ошибка означает именно то, что говорится: EF не может понять, как это сделать за один шаг.Вы можете сделать это в два этапа (два вызова SaveChanges()).

1 голос
/ 19 декабря 2017

Это старый вопрос, но он все еще актуален для Entity Framework 6.2.0.Мое решение состоит из трех частей:

  1. НЕ установите для столбца MainChildId значение HasDatabaseGeneratedOption(Computed) (это блокирует его последующее обновление)
  2. ИспользованиеТриггер для обновления родительского элемента, когда я вставляю обе записи одновременно (это не проблема, если родительский элемент уже существует, а я просто добавляю нового дочернего элемента, поэтому убедитесь, что триггер объясняет это как-то - было легко в моем случаеcase)
  3. После вызова ctx.SaveChanges() также обязательно вызовите ctx.Entry(myParentEntity).Reload(), чтобы получить какие-либо обновления для столбца MainChildId из Trigger (EF не получит их автоматически).

В моем коде ниже Thing является родителем, а ThingInstance является дочерним и имеет следующие требования:

  • Всякий раз, когда вставляется Thing (родительский), a ThingInstance (дочерний элемент) также должен быть вставлен и установлен как Thing 'CurrentInstance (основной дочерний элемент).
  • Другой ThingInstances (дочерний элемент) может быть добавлен к Thing (родительский)с или без CurrentInstance (основной ребенок)

Это привело к следующему дизайну: * EF Потребитель должен вОбслужите обе записи, но оставьте CurrentInstanceId как ноль, но обязательно установите ThingInstance.Thing для родителя.* Триггер обнаружит, если ThingInstance.Thing.CurrentInstanceId равно нулю.Если это так, то он обновит его до ThingInstance.Id.* EF Consumer должен перезагрузить / повторно загрузить данные, чтобы просмотреть любые обновления по триггеру* Два обхода все еще необходимы, но необходим только один атомарный вызов ctx.SaveChanges, и мне не приходится иметь дело с ручными откатами.* У меня есть дополнительный триггер для управления, и, возможно, есть более эффективный способ сделать это, чем то, что я сделал с курсором, но я никогда не буду делать это в том объеме, где производительность будет иметь значение.

База данных:

(Извините, не проверял этот скрипт - просто сгенерировал его из моей БД и поместил его сюда из-за спешки. Вы определенно должны быть в состоянии получить важные сведения отсюда.)

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 Id=@thingId
            IF @CurrentInstanceId IS NULL
            BEGIN
                UPDATE Thing SET CurrentInstanceId=@instanceId WHERE Id=@thingId
            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

C # Вставки:

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

C # New Child:

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;
    }
}
1 голос
/ 05 августа 2012

Как в EF, так и в LINQ to SQL существует проблема невозможности сохранения циклических ссылок, даже если они могут быть намного более полезными, просто инкапсулируя 2 или более вызовов SQL в транзакции за кулисами вместо того, чтобы бросать Исключение.

Я написал исправление для этого в LINQ to SQL, но пока не удосужился сделать это в EF, потому что я только что избегал циклических ссылок в своем дизайне БД.

Что вы можете сделать, это создать вспомогательный метод, который откладывает циклические ссылки, запустить его перед вызовом SaveChanges (), запустить другой метод, который возвращает циклические ссылки на место, и снова вызвать SaveChanges (). Вы можете заключить все это в один метод, может быть, SaveChangesWithCircularReferences().

Чтобы вернуть циклические ссылки обратно, вам нужно отследить, что вы удалили, и вернуть этот журнал.

public class RemovedReference() . . .

public List<RemovedReference> SetAsideReferences()
{
    . . .
}

Таким образом, в основном код в SetAsideReferences выслеживает циклические ссылки, откладывая одну половину в каждом случае и записывая их в список.

В моем случае я создал класс, в котором сохранены объект, имя свойства и значение (другой объект), который был удален, и просто сохранил их в списке, например:

public class RemovedReference
{
    public object Object;
    public string PropertyName;
    public object Value;
}

Вероятно, есть более разумная структура для достижения этой цели; вы можете использовать объект PropertyInfo, например, вместо строки, и вы можете кэшировать тип, чтобы удешевить второй раунд отражения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...