ADO.NET Entity Framework генерирует неожиданные проблемные вставки

Мне нужна помощь в понимании ADO.NET Entity Framework.

Я пытаюсь представить и управлять иерархическими данными в элементе управления WPF TreeView с помощью ADO.NET Entity Framework.

http://img14.imageshack.us/img14/7158/thingpi1.gif

У каждой из этих вещей есть единственный родитель и ноль или более детей.

Моя кнопка "удалить" ...

Private Sub ButtonDeleteThing_Click(...)
    db.DeleteObject(DirectCast(TreeViewThings.SelectedItem, Thing))
    db.SaveChanges()
End Sub

Я наблюдаю за профилировщиком SQL Server, пока отлаживаю свое приложение:

  1. Первое нажатие кнопки удаляет просто отлично.
  2. При нажатии второй кнопки выполняется вставка повторяющегося родителя (но с пустым первичным ключом GUID uniqueidentifier), а затем выполняется удаление.
  3. При третьем нажатии кнопки происходит сбой (нарушение ограничения PRIMARY KEY), поскольку невозможно вставить другую строку с пустым первичным ключом GUID.

Неожиданное, сгенерированное дублирование T-SQL ...

exec sp_executesql N'insert [dbo].[Thing]([Id], [ParentId], ...)
values (@0, @1, ...) ',N'@0 uniqueidentifier,@1 uniqueidentifier,...',
@0='00000000-0000-0000-0000-000000000000',
@1='389D987D-79B1-4A9D-970F-CE15F5E3E18A',
...

Но это не просто удаление. Моя кнопка «добавить» ведет себя аналогичным образом с неожиданными вставками. Он следует той же схеме.

Это заставляет меня думать, что существует более фундаментальная проблема, связанная с тем, как я привязываю эти классы сущностей к WPF TreeView или к самой моей модели данных.

Вот соответствующий код ...

XAML ...

<TreeView Name="TreeViewThings"
          ItemsSource="{Binding}"
          TreeViewItem.Expanded="TreeViewThings_Expanded"
          TreeViewItem.Selected="TreeViewThings_Selected"
          ... >
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type local:Thing}"
                                  ItemsSource="{Binding Children}">
            <TextBlock Text="{Binding Path=Title}" />
        </HierarchicalDataTemplate>
    </TreeView.Resources>
</TreeView>
<Button Name="ButtonAddThing" Content="Add Thing" ... />
<Button Name="ButtonDeleteThing" Content="Delete Thing" ... />

Visual Basic...

Partial Public Class Window1

    Dim db As New ThingProjectEntities

    Private Sub Window1_Loaded(...) Handles MyBase.Loaded
        TreeViewThings.ItemsSource = _
            From t In db.Thing.Include("Children") _
            Where (t.Parent Is Nothing) _
            Select t
    End Sub

    Private Sub TreeViewThings_Expanded(...)
        Dim ExpandedTreeViewItem As TreeViewItem = _
            DirectCast(e.OriginalSource, TreeViewItem)
        LoadTreeViewChildren(ExpandedTreeViewItem)
    End Sub

    Sub LoadTreeViewChildren(ByRef Parent As TreeViewItem)
        Dim ParentId As Guid = DirectCast(Parent.DataContext, Thing).Id
        Dim ChildThings As System.Linq.IQueryable(Of Thing)
        ChildThings = From t In db.Thing.Include("Children") _
                      Where t.Parent.Id = ParentId _
                      Select t
        Parent.ItemsSource = ChildThings
    End Sub

    Private Sub ButtonAddThing_Click(...)
        Dim NewThing As New Thing
        NewThing.Id = Guid.NewGuid()
        Dim ParentId As Guid = _
            DirectCast(TreeViewThings.SelectedItem, Thing).Id
        NewThing.Parent = (From t In db.Thing _
                           Where t.Id = ParentId _
                           Select t).First
        ...
        db.AddToThing(NewThing)
        db.SaveChanges()
        TreeViewThings.UpdateLayout()
    End Sub

    Private Sub ButtonDeleteThing_Click(...)
        db.DeleteObject(DirectCast(TreeViewThings.SelectedItem, Thing))
        db.SaveChanges()
    End Sub

    ...

End Class

Что я делаю неправильно? Почему он генерирует эти странные вставки?


Обновлять:

Я совершил прорыв. Но я все еще не могу этого объяснить.

Я избавился от причины, когда упростил свой код для этого вопроса.

Вместо использования Linq, например:

From t In db.Thing.Include("Children") Where ...

Я использовал Linq, например:

From t In db.Thing.Include("Children").Include("Brand") Where ...

Видите ли, сущность My Thing связана с другой сущностью Бренда.

http://img25.imageshack.us/img25/3268/thingbrandct4.gif

Я подумал, что это не имеет отношения к делу, поэтому не включил его в вопрос выше.

По-видимому, это было причиной моих неожиданных проблемных вставок в мою таблицу Thing.

Но почему? Кто-нибудь может объяснить, почему это происходило? Я бы хотел лучше это понять.


person Zack Peterson    schedule 16.02.2009    source источник


Ответы (3)


Почему вы снова загружаете родительский элемент при добавлении нового дочернего элемента, когда у вас уже есть объект? Хотя это не проблема для базы данных, это приведет к несогласованности на уровне объектов. Вы можете просто использовать существующего родителя следующим образом:

Private Sub ButtonAddThing_Click(...)
    Dim NewThing As New Thing
    NewThing.Id = Guid.NewGuid()
    Dim Parent As Thing = DirectCast(TreeViewThings.SelectedItem, Thing)
    NewThing.Parent = Parent
    ...
    db.AddToThing(NewThing)
    db.SaveChanges()
    TreeViewThings.UpdateLayout()
End Sub

По поводу удаления: вы указали каскадное удаление в базе данных?

person Inferis    schedule 24.02.2009
comment
Entity Framework сохраняет идентичность сущности. Хотя загружать родительский элемент из базы данных каждый раз, возможно, неэффективно, это не приведет к несогласованности. Вы можете загружать один и тот же объект столько раз, сколько хотите, и он по-прежнему будет рассматриваться как один и тот же объект. - person Craig Stuntz; 24.02.2009
comment
Я не пытаюсь делать каскадирование. Я удаляю только одну строку из базы данных. ВСТАВОК быть не должно. - person Zack Peterson; 24.02.2009
comment
Одно время у меня был NewThing.Parent = DirectCast (TreeViewThings.SelectedItem, Thing), но я изменил его, пытаясь устранить неполадки. Наверное, можно будет вернуть обратно. - person Zack Peterson; 24.02.2009

Я не изучал код подробно, но первое, что нужно учесть, это то, что вам не нужно вызывать DeleteObject на каждом уровне вашей иерархии. EF, как и другие O / RM, отслеживает объекты и их ассоциации за вас.

Скажем, например, что у вас есть отношения родитель-> потомок 1 .. *. Если вы запрашиваете родительский объект, удаляете дочерний объект из родительской коллекции Children, а затем вызываете SaveChanges (), EF сгенерирует для вас соответствующие операторы DELETE SQL - вам не нужно отслеживать это самостоятельно.

Итак, лучший способ реализовать ваш сценарий - это сделать следующее:

  1. Запросите иерархию объектов из EF.
  2. Привяжите это к своему пользовательскому интерфейсу. Позвольте вашему пользовательскому интерфейсу изменять объекты в памяти по своему усмотрению.
  3. Когда вы закончите, вызовите SaveChanges, и пусть EF решит, что делать.

Сообщите мне, если это поможет.

person Andrew Peters    schedule 19.02.2009
comment
Я удаляю только одну вещь за раз. - person Zack Peterson; 20.02.2009
comment
Это то, что позволило EF решить, что делать, и это доставляет мне проблемы. - person Zack Peterson; 20.02.2009
comment
Чтобы удалить дочерний объект, достаточно удалить его из графа объектов. Точно так же, чтобы добавить новый объект, добавьте его в граф объектов. EF отслеживает график, поэтому будет знать, что делать, когда вы вызываете SaveChanges. Возможно, попробуйте написать модульный тест вне вашего пользовательского интерфейса, чтобы понять, как это работает. - person Andrew Peters; 20.02.2009

Здесь происходят две вещи. Я не совсем понимаю отношения между ними, но думаю, что смогу помочь вам.

Первое, что вам нужно понять, это то, что Entity Framework плохо справляется с удалением не полностью материализованного экземпляра. Вот почему Include изменяет поведение, которое вы видите. Поэтому, если у вас есть сущность, которая объединяет список дочерних элементов, вам необходимо загрузить эти дочерние элементы перед вызовом delete. Только если дочерние экземпляры находятся в памяти, они будут удалены раньше родительских. Итак, с включением или без него вам нужно сделать что-то подобное перед вызовом Delete.

if (!thing.BrandReference.IsLoaded) thing.BrandReference.Load();

Если вы вызвали Include для отношения, то это ничего не даст, если вы этого не сделали, тогда это гарантирует, что все будет материализовано до вас.

Второе, что уникально понимают, - это то, что вставка новой сущности с отношением к существующей сущности концептуально представляет собой две разные вставки. Это следствие того факта, что отношения являются первоклассными в Entity Framework. Первая вставка - это сама сущность, вторая - это отношение. В этом случае нет отдельной таблицы для отношения, поэтому только одна фактическая вставка в базу данных по мере необходимости. Однако Entity Framework может выяснить это только в том случае, если он правильно сопоставлен.

Так что же происходит в этом случае? Далее следует мое предположение, основанное на некоторых вещах, которые я вижу здесь. Но я думаю, что ситуация даже сложнее, чем я описываю, поэтому я считаю, что нижеследующее неверно в некоторых деталях. Однако это может быть достаточно близко, чтобы помочь вам решить настоящую проблему.

  1. У вас был экземпляр, который не был полностью материализован до того, как вы попытались его удалить.
  2. Когда вы пытались удалить что-то еще, фреймворк пытался установить те же отношения. Он обнаружил, что все вышло из строя, и безуспешно пытался вернуть все в хорошее состояние.
  3. Затем вы попытались удалить еще раз, 2 повтора, только на этот раз это было еще менее успешно из-за ограничений базы данных.
  4. Использование Include устраняет проблему в 1.
person Craig Stuntz    schedule 23.02.2009
comment
Это не удалось: From t In db.Thing.Include (Children) .Include (Brand) Где ... без этого второго Include () он работает, как ожидалось. Похоже, что вместо того, чтобы быть менее чем полностью материализованными, они были чрезмерно материализованы. Но почему это возможно? - person Zack Peterson; 24.02.2009