График обновления Entity Framework 6

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

Например, если у меня есть:

public class Person
{
     public int Id { get; set; }
     public int Name { get; set; }
     public virtual ICollection<Automobile> Automobiles { get; set; }

}

public class Automobile
{
     public int Id { get; set; }
     public int Name { get; set; }
     public short Seats { get; set; }
     public virtual ICollection<MaintenanceRecord> MaintenanceRecords { get; set ;}
     public virtual Person Person { get; set; }
}

public class MaintenanceRecord
{
     public int Id { get; set; }
     public int AutomobileId { get; set; }
     public DateTime DatePerformed { get; set; }

     public virtual Automobile Automobile{ get; set; }

}

Я редактирую модели, похожие на эти объекты выше, а затем передаю эти модели в слой данных для сохранения, где для этого экземпляра я использую сущность framework. Поэтому я переводил эти модели в объекты POCO, внутренние для DAL.

Похоже, что если мои модели не имеют состояния, указывающего, являются ли они новыми или обновленными, у меня есть довольно много работы, чтобы сделать "Сохранить" изменения. Я должен сначала выбрать объект Person, обновить его, а затем сопоставить любые существующие автомобили и обновить их, а также добавить новые, а затем для каждой проверки автомобиля для любых новых или обновленных записей технического обслуживания.

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

Ответ 1

Я столкнулся с этой проблемой некоторое время назад и следил за этим потоком на сайте EF Codeplex. https://entityframework.codeplex.com/workitem/864

Похоже, что это рассматривается для следующего выпуска, я предполагаю EF 7, который, по-видимому, является довольно большой внутренней перестройкой EF. Это может стоить проверить... http://www.nuget.org/packages/RefactorThis.GraphDiff/

Когда я работал над этим, я нашел еще одну запись EF на SO, и у кого-то был пример того, как это сделать вручную. В то время я решил сделать это вручную, не уверен, почему GraphDiff выглядит довольно круто. Вот пример того, что я сделал.

  public async Task<IHttpActionResult> PutAsync([FromBody] WellEntityModel model)
    {
        try
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            var kne = TheContext.Companies.First();
            var entity = TheModelFactory.Create(model);
            entity.DateUpdated = DateTime.Now;

            var currentWell = TheContext.Wells.Find(model.Id);

            // Update scalar/complex properties of parent
            TheContext.Entry(currentWell).CurrentValues.SetValues(entity);

            //We don't pass back the company so need to attached the associated company... this is done after mapping the values to ensure its not null.
            currentWell.Company = kne;

            // Updated geometry - ARGHHH NOOOOOO check on this once in a while for a fix from EF-Team https://entityframework.codeplex.com/workitem/864
            var geometryItemsInDb = currentWell.Geometries.ToList();
            foreach (var geometryInDb in geometryItemsInDb)
            {
                // Is the geometry item still there?
                var geometry = entity.Geometries.SingleOrDefault(i => i.Id == geometryInDb.Id);
                if (geometry != null)
                    // Yes: Update scalar/complex properties of child
                    TheContext.Entry(geometryInDb).CurrentValues.SetValues(geometry);
                else
                    // No: Delete it
                    TheContext.WellGeometryItems.Remove(geometryInDb);
            }
            foreach (var geometry in entity.Geometries)
            {
                // Is the child NOT in DB?
                if (geometryItemsInDb.All(i => i.Id != geometry.Id))
                    // Yes: Add it as a new child
                    currentWell.Geometries.Add(geometry);
            }

            // Update Surveys
            var surveyPointsInDb = currentWell.SurveyPoints.ToList();
            foreach (var surveyInDb in surveyPointsInDb)
            {
                // Is the geometry item still there?
                var survey = entity.SurveyPoints.SingleOrDefault(i => i.Id == surveyInDb.Id);
                if (survey != null)
                    // Yes: Update scalar/complex properties of child
                    TheContext.Entry(surveyInDb).CurrentValues.SetValues(survey);
                else
                    // No: Delete it
                    TheContext.WellSurveyPoints.Remove(surveyInDb);
            }
            foreach (var survey in entity.SurveyPoints)
            {
                // Is the child NOT in DB?
                if (surveyPointsInDb.All(i => i.Id != survey.Id))
                    // Yes: Add it as a new child
                    currentWell.SurveyPoints.Add(survey);
            }

            // Update Temperatures - THIS IS A HUGE PAIN = HOPE EF is updated to handle updating disconnected graphs.
            var temperaturesInDb = currentWell.Temperatures.ToList();
            foreach (var tempInDb in temperaturesInDb)
            {
                // Is the geometry item still there?
                var temperature = entity.Temperatures.SingleOrDefault(i => i.Id == tempInDb.Id);
                if (temperature != null)
                    // Yes: Update scalar/complex properties of child
                    TheContext.Entry(tempInDb).CurrentValues.SetValues(temperature);
                else
                    // No: Delete it
                    TheContext.WellTemperaturePoints.Remove(tempInDb);
            }
            foreach (var temps in entity.Temperatures)
            {
                // Is the child NOT in DB?
                if (surveyPointsInDb.All(i => i.Id != temps.Id))
                    // Yes: Add it as a new child
                    currentWell.Temperatures.Add(temps);
            }
            await TheContext.SaveChangesAsync();
            return Ok(model);
        }
        catch (Exception ex)
        {
            Trace.WriteLine(ex.Message);
        }
        return InternalServerError();
    }

Ответ 2

То, что вы ищете, это шаблон "Единица работы":

http://msdn.microsoft.com/en-us/magazine/dd882510.aspx

Вы можете либо отслеживать UoW на клиенте, либо передавать его с помощью DTO, либо сервер имеет значение. И настоящие объекты DataSet и EF имеют собственную внутреннюю реализацию UoW. Для чего-то отдельного есть эта структура, но я никогда не использовал ее, поэтому не имею обратной связи:

http://genericunitofworkandrepositories.codeplex.com/

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

Ответ 3

Это огромная боль для меня тоже. Я получил ответ от @GetFuzzy на более многоразовый метод:

public void UpdateCollection<TCollection, TKey>(
    DbContext context, IList<TCollection> databaseCollection, 
    IList<TCollection> detachedCollection, 
    Func<TCollection, TKey> keySelector) where TCollection: class where TKey: IEquatable<TKey>
{
    var databaseCollectionClone = databaseCollection.ToArray();
    foreach (var databaseItem in databaseCollectionClone)
    {
        var detachedItem = detachedCollection.SingleOrDefault(item => keySelector(item).Equals(keySelector(databaseItem)));
        if (detachedItem != null)
        {
            context.Entry(databaseItem).CurrentValues.SetValues(detachedItem);
        }
        else
        {
            context.Set<TCollection>().Remove(databaseItem);
        }
    }

    foreach (var detachedItem in detachedCollection)
    {
        if (databaseCollectionClone.All(item => keySelector(item).Equals(keySelector(detachedItem)) == false))
        {
            databaseCollection.Add(detachedItem);
        }
    }
}

Используя этот метод, я могу использовать его следующим образом:

public void UpdateProduct(Product product)
{
   ...

   var databaseProduct = productRepository.GetById(product.Id);

   UpdateCollection(context, databaseProduct.Accessories, product.Accessories, productAccessory => productAcccessory.ProductAccessoryId);
   UpdateCollection(context, databaseProduct.Categories, product.Categories, productCategory => productCategory.ProductCategoryId);

   ...

   context.SubmitChanges();
}

Однако, когда граф становится глубже, у меня такое чувство, что этого будет недостаточно.

Ответ 4

Это зависит от КАК, что вы выполняете добавление/изменение объектов.

Я думаю, вы, возможно, пытаетесь сделать слишком много с сущностью в любой момент времени. Разрешение редактирования и добавления одновременно может привести вас к ситуации, когда вы не знаете, что делается с сущностью, особенно в отключенном сценарии. Вы должны выполнять одно действие только для одного объекта за раз, если только вы не удаляете объекты. Это кажется монотонным, конечно, но 99% ваших пользователей хотят иметь чистый и понятный интерфейс. Много раз мы заканчиваем тем, что экраны экранов наших приложений "бог", где все и что угодно можно сделать. Какой 9/10 раз не нужен (YAGNI).

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

Подводя итог, вы должны ограничить количество операций, которые вы делаете доступными для одного экрана, и убедитесь, что вы предоставили определенный тип уникальной информации для объекта, чтобы вы могли попытаться найти объект, чтобы узнать, существует ли он.