Подтвердить что ты не робот

Поддерживает ли платформа Entity Framework циклические ссылки?

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

class Parent
{
   int ParentId;
   int? MainChildId;
}

class Child
{
   int ChildId;
   int ParentId;
}

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

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

4b9b3361

Ответ 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;
    }
}