2017-01-05 7 views
4

Я пытаюсь обновить вложенную коллекцию (Города) объекта Country.EF & Automapper. Обновление вложенных коллекций

Только простое enitities и DTO-х:

// EF Models 
public class Country 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
    public virtual ICollection<City> Cities { get; set; } 
} 

public class City 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
    public int CountryId { get; set; } 
    public int? Population { get; set; } 

    public virtual Country Country { get; set; } 
} 

// DTo's 
public class CountryData : IDTO 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
    public virtual ICollection<CityData> Cities { get; set; } 
} 

public class CityData : IDTO 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
    public int CountryId { get; set; } 
    public int? Population { get; set; } 
} 

И сам код (протестировано в консольном приложении для простоты):

 using (var context = new Context()) 
     { 
      // getting entity from db, reflect it to dto 
      var countryDTO = context.Countries.FirstOrDefault(x => x.Id == 1).ToDTO<CountryData>(); 

      // add new city to dto 
      countryDTO.Cities.Add(new CityData 
             { 
              CountryId = countryDTO.Id, 
              Name = "new city", 
              Population = 100000 
             }); 

      // change existing city name 
      countryDTO.Cities.FirstOrDefault(x => x.Id == 4).Name = "another name"; 

      // retrieving original entity from db 
      var country = context.Countries.FirstOrDefault(x => x.Id == 1); 

      // mapping 
      AutoMapper.Mapper.Map(countryDTO, country); 

      // save and expecting ef to recognize changes 
      context.SaveChanges(); 
     } 

Этого код генерирует исключение:

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

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

Я потратил много времени на поиск решения, но не получил никакого результата. Пожалуйста помоги.

+0

делает 'country.cities [0] .Id' имеет значение после отображения? если EF не пытается установить значение null для ключа foriegn, и это вызывает проблему – esiprogrammer

+0

@esiprogrammer, да, это так. –

ответ

8

Проблема: country Вы получаете из базы данных уже некоторые города. При использовании AutoMapper так:

// mapping 
     AutoMapper.Mapper.Map(countryDTO, country); 

AutoMapper делает что-то вроде создания IColletion<City> правильно (с одного города в вашем примере), и назначая эту новую коллекцию вашей country.Cities собственности.

Проблема заключается в том, что EntityFramework не знает, что делать со старой коллекцией городов.

  • Следует ли удалить старые города и принять только новую коллекцию?
  • Должно ли оно просто объединить два списка и сохранить оба в базе данных?

Фактически, EF не может решить для вас. Если вы хотите продолжать использовать AutoMapper, вы можете настроить отображение так:

// AutoMapper Profile 
public class MyProfile : Profile 
{ 

    protected override void Configure() 
    { 

     Mapper.CreateMap<CountryData, Country>() 
      .ForMember(d => d.Cities, opt => opt.Ignore()) 
      .AfterMap((d,e) => AddOrUpdateCities(d, e) 
      ); 


    } 

    private void AddOrUpdateCities(CountryData dto, Country country) 
    { 
     foreach (var cityDTO in dto.Cities) 
     { 
      if (cityDTO.Id == 0) 
      { 
       country.Cities.Add(Mapper.Map<City>(cityDTO)); 
      } 
      else 
      { 
       Mapper.Map(cityDTO, country.Cities.SingleOrDefault(c => c.Id == cityDTO.Id)); 
      } 
     } 
    } 
} 

Конфигурация Ignore() используется для Cities делает AutoMapper просто сохранить оригинальную ссылку прокси построенную EntityFramework.

Тогда мы просто используем AfterMap(), чтобы вызвать действие делает именно то, что вы thougth:

  • Для новых городов мы отображаем от DTO к Entity (AutoMapper создает новый экземпляр) и добавить его к коллекции страны.
  • Для существующих городов мы используем перегрузку Map, где мы передаем существующий объект как второй параметр и прокси-сервер города в качестве первого параметра, поэтому automapper просто обновляет свойства существующей сущности.

Затем вы можете сохранить исходный код:

using (var context = new Context()) 
    { 
     // getting entity from db, reflect it to dto 
     var countryDTO = context.Countries.FirstOrDefault(x => x.Id == 1).ToDTO<CountryData>(); 

     // add new city to dto 
     countryDTO.Cities.Add(new CityData 
            { 
             CountryId = countryDTO.Id, 
             Name = "new city", 
             Population = 100000 
            }); 

     // change existing city name 
     countryDTO.Cities.FirstOrDefault(x => x.Id == 4).Name = "another name"; 

     // retrieving original entity from db 
     var country = context.Countries.FirstOrDefault(x => x.Id == 1); 

     // mapping 
     AutoMapper.Mapper.Map(countryDTO, country); 

     // save and expecting ef to recognize changes 
     context.SaveChanges(); 
    } 
+0

Мне все еще не ясно, извините. Если после внесения изменений в 'countryDTO.Cities' я создаю' newCities' так же, как вы написали, у него есть все города и добавление их в цикл foreach в стране. Города делают дубликаты. Если я удаляю города после получения исходного объекта и запускаю foreach после этого, я получаю то же самое ** Не удалось выполнить операцию: ** ошибка. Что я пропустил? –

+1

@AkmalSalikhov Я редактирую свой код, предоставляя еще один способ настроить automapper, чтобы делать то, что вы хотите. Итак, код, который вы правильно работаете, я инкапсулирован в automapper, я думаю, что это то, что вы хотели. Теперь EF знает, что добавить, и что обновить. – Alisson

+0

Alisson, инкапсулирующая функция в конфигурации automapper очень полезна, спасибо! Но есть одно. Ваш код работает хорошо, если я использую '.ForMember (dest => dest.Cities, src => src.Ignore())', а не 'UseDestinationValue'. Использование UseDestinationValue вызывает то же самое ** Связь не может быть изменена ** ошибка –

1

когда сохранить изменения все города рассматриваются как добавил EF еще потому не теперь о них до экономии времени. Таким образом, EF пытается установить значение null для внешнего ключа старого города и вставить его вместо обновления.

, используя ChangeTracker.Entries(), вы узнаете, какие изменения CRUD будут сделаны EF.

Если вы хотите просто обновить существующий город вручную, вы можете просто сделать:

foreach (var city in country.cities) 
{ 
    context.Cities.Attach(city); 
    context.Entry(city).State = EntityState.Modified; 
} 

context.SaveChanges(); 
0

Похоже, я нашел решение:

var countryDTO = context.Countries.FirstOrDefault(x => x.Id == 1).ToDTO<CountryData>(); 
countryDTO.Cities.Add(new CityData { CountryId = countryDTO.Id, Name = "new city 2", Population = 100000 }); 
countryDTO.Cities.FirstOrDefault(x => x.Id == 11).Name = "another name"; 

var country = context.Countries.FirstOrDefault(x => x.Id == 1); 

foreach (var cityDTO in countryDTO.Cities) 
{ 
    if (cityDTO.Id == 0) 
    { 
     country.Cities.Add(cityDTO.ToEntity<City>()); 
    } 
    else 
    { 
     AutoMapper.Mapper.Map(cityDTO, country.Cities.SingleOrDefault(c => c.Id == cityDTO.Id)); 
    } 
} 

AutoMapper.Mapper.Map(countryDTO, country); 

context.SaveChanges(); 

этот код обновления измененных объектов и добавлять новые. Но, может быть, есть некоторые подводные камни, которые я пока не могу обнаружить?

+0

Почему вы конвертируете DTO в объект, а не Entity в DTO в том же контексте? просто отредактируйте объект объекта и преобразовывайте конечный результат в DTO – esiprogrammer

+1

@esiprogrammer. Я думаю, он просто упростил код, чтобы проиллюстрировать его проблему. Он может конвертироваться в DTO в некоторый метод Get, затем метод post receisis редактирует/новые города как DTO и конвертирует в Entity для сохранения. – Alisson

+0

@ Alisson Я так не думаю, как он упомянул в своем вопросе «протестирован в консольном приложении ради простоты». это фактический код, который он использует – esiprogrammer

0

Очень хорошее решение Alisson. Вот мое решение ... Как мы знаем, EF не знает, нужен ли запрос для обновления или вставки, так что я бы сделал, сначала удалил метод RemoveRange() и отправил коллекцию, чтобы вставить ее снова. В фоновом режиме это то, как работает база данных, мы можем эмулировать это поведение вручную.

Вот код:

//country object from request for example

var cities = dbcontext.Cities.Where(x=>x.countryId == country.Id);

dbcontext.Cities.RemoveRange(cities);

/* Now make the mappings and send the object this will make bulk insert into the table related */