Скажите DbContext не добавлять весь граф объектов? - PullRequest
1 голос
/ 09 июня 2011

В моей модели есть класс, который может ссылаться на один и тот же дочерний класс из двух разных ассоциаций / полей FK. Обе эти ссылки заполняются одним и тем же экземпляром дочернего объекта при создании родительского объекта, а затем позже один или два дочерних объекта могут быть обновлены или изменены (это не всегда происходит), и оригинал хранится, потому что других детей никогда не трогают. Я надеюсь, что это имеет смысл.

Когда родительский объект создается или извлекается из базы данных, на которую дважды ссылается один и тот же дочерний объект, при попытке добавить родительский элемент в DbContext мы видим страшную ошибку: An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key. Это выбрасывается, потому что DbContext является попытка добавить весь граф объекта в его трекер изменений, то есть две дочерние ссылки, указывающие на один и тот же дочерний объект.

Нам не нужно отслеживать изменения. Мы не возражаем бросать полностью заполненный оператор UPDATE в базу данных. Есть ли способ заставить DbContext не добавлять весь граф объекта, чтобы добавить только один экземпляр, о котором мы говорим добавить? Если так, какую функциональность мы потеряем, если отключим это глобально?

РЕДАКТИРОВАТЬ: Обновленный пример кода.

РЕДАКТИРОВАТЬ 2: Обновлен пример кода, чтобы включить сериализацию для имитации взаимодействия веб-службы.

[TestClass]
public class EntityFrameworkTests
{
  [TestMethod]
  public void ObjectGraphTest()
  {
    Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0");
    Database.SetInitializer(new DropCreateDatabaseAlways<MyDbContext>());

    string connectionString = String.Format("Data Source={0}\\EntityFrameworkTests.sdf", Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
    MyDbContext context = new MyDbContext(connectionString);

    Child child = new Child() { ID = 1, SomeProperty = "test value" };
    //context.Entry<Child>(child).State = EntityState.Added;
    Parent parent = new Parent()
    {
      ID = 1,
      SomeProperty = "some value",
      OriginalChild = child,
      ChangeableChild = child
    };
    context.Entry<Parent>(parent).State = EntityState.Added;
    context.SaveChanges();

    context = new MyDbContext(connectionString);
    //parent = context.Set<Parent>().AsNoTracking().Include(p => p.OriginalChild).Include(p => p.ChangeableChild).FirstOrDefault();
    parent = context.Set<Parent>().Include(p => p.OriginalChild).Include(p => p.ChangeableChild).FirstOrDefault();

    // mimic receiving object via a web service
    SaveToStorage(parent);
    parent = GetSavedItem(1);

    parent.SomeProperty = "some new value";
    context = new MyDbContext(connectionString);
    context.Entry<Parent>(parent).State = EntityState.Modified; // error here
    context.SaveChanges();
  }
}

Методы сериализации для имитации взаимодействия веб-сервисов:

private void SaveToStorage(Parent parent)
{
  string savedFilePath = String.Format("{0}\\Parent{1}.xml", Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), parent.ID);

  using (FileStream fileStream = new FileStream(savedFilePath, FileMode.Create, FileAccess.Write))
  {
    using (XmlWriter writer = XmlWriter.Create(fileStream))
    {
      DataContractSerializer serializer = new DataContractSerializer(typeof(Parent));
      serializer.WriteObject(writer, parent);
    }
  }
}

private Parent GetSavedItem(int parentID)
{
  string savedFilePath = String.Format("{0}\\Parent{1}.xml", Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), parentID);

  using (FileStream fileStream = new FileStream(savedFilePath, FileMode.Open, FileAccess.Read))
  {
    using (XmlDictionaryReader xmlReader = XmlDictionaryReader.CreateTextReader(fileStream, new XmlDictionaryReaderQuotas()))
    {
      DataContractSerializer serializer = new DataContractSerializer(typeof(Parent));
      Parent savedItem = (Parent)serializer.ReadObject(xmlReader, true);
      return savedItem;
    }
  }
}

Используемые классы (обновлено для сериализации):

[DataContract]
internal class Child
{
  [DataMember]
  public int ID { get; set; }
  [DataMember]
  public string SomeProperty { get; set; }
}

[DataContract]
internal class Parent
{
  [DataMember]
  public int ID { get; set; }
  [DataMember]
  public string SomeProperty { get; set; }

  [DataMember]
  public int OriginalChildID { get; set; }
  [DataMember]
  public Child OriginalChild { get; set; }
  [DataMember]
  public int ChangeableChildID { get; set; }
  [DataMember]
  public Child ChangeableChild { get; set; }
}

internal class MyDbContext : DbContext
{
  public DbSet<Parent> Parents { get; set; }
  public DbSet<Child> Children { get; set; }

  public MyDbContext(string connectionString)
    : base(connectionString) { }

  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
    modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
  }
}

1 Ответ

3 голосов
/ 09 июня 2011

Уродливый раствор:

Child originalChild = parent.OriginalChild;
Child changeableChild = parent.ChangeableChild;
parent.OriginalChild = null;
parent.ChangeableChild = null;

context.Entry<Parent>(parent).State = EntityState.Modified;
context.SaveChanges();

parent.OriginalChild = originalChild;
parent.ChangeableChild = changeableChild;

Если вам больше не нужны parent с дочерними объектами после сохранения, то для установки, конечно, будет достаточно null.

Другое и гораздо лучшее решение: снова вытащить исходного родителя из базы данных - без дочерних, так как вы знаете, что хотите сохранить только измененные родительские свойства:

var originalParent = context.Set<Parent>()
    .Where(p => p.ID == parent.ID)
    .FirstOrDefault();

context.Entry(originalParent).CurrentValues.SetValues(parent);
context.SaveChanges();

Вы должны сначала загрузить родителя (с активным отслеживанием изменений!) Из базы данных, но с другой стороны, команда UPDATE, введенная здесь, будет содержать только измененные свойства. Поскольку вы говорите, что не возражаете отправить полную команду UPDATE (установив состояние Modified), я полагаю, у вас нет проблем с производительностью. Таким образом, загрузка оригинала и последующая отправка небольшого ОБНОВЛЕНИЯ только с измененными свойствами не может быть хуже или намного хуже с точки зрения производительности, чем отправка полной команды ОБНОВЛЕНИЕ.

...