Как обновить основные детали в Entity Framework с помощью Automapper - PullRequest
0 голосов
/ 05 марта 2020

Я пытаюсь смоделировать обновление ребенка с помощью AutoMapper. Отношения «один ко многим» с «Каскадом при удалении».

Мои шаги:

  1. Загрузка мастера с включенными деталями
  2. Сопоставление мастера с masterDTO
  3. обновить / изменить детали в masterDTO
  4. Отобразить обратно masterDTO на master
  5. Сохранить его.

Проблема: мастер обновлен , новые детали добавлены. но обновленная деталь не постоянство .

Вот мои классы:

public class Master 
{
        public int id {get;set;}
        public string masterInfo {get;set;}
        public virtual ICollection<Detail> details { get;set; } = new Collection<Detail>();
}

public class Detail 
{
    public int id {get;set;}
    public int masterId {get;set;}
    public virtual Master master {get;set;} 
    public string detailInfo {get;set;}
}

public class MasterDTO 
{
    public int id {get;set;}
    public string masterInfo {get;set;}
    public virtual ICollection<DetailDTO> details { get; set;} = new Collection<DetailDTO>();
}

public class DetailDTO 
{
    public int id {get;set;}
    public int masterId {get;set;}
    public virtual MasterDTO master {get;set;} 
    public string detailInfo {get;set;}
}

Настройка DbContext:

public class MyContext : DbContext 
{
    public DbSet<Master> Masters {get;set;}
    public DbSet<Detail> Details {get;set;}

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseSqlServer(
            @"Server=localhost;Database=Test_AutoMapper;Trusted_Connection=True");

    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        modelBuilder.Entity<Master>()
                    .HasMany<Detail>(m => m.details)
                    .WithOne(d => d.master)
                    .HasForeignKey(d => d.masterId)
                    .OnDelete(DeleteBehavior.Cascade);
    }
}

А вот is StartUp программа и настройка Automapper:

static void Main(string[] args)
{
    var config = new MapperConfiguration(cfg => {
            cfg.CreateMap<Master, MasterDTO>().ForMember(a => a.details, map => map.MapFrom(src => src.details));
            cfg.CreateMap<Detail, DetailDTO>();
            cfg.CreateMap<MasterDTO, Master>().ForMember(a => a.details, map => map.MapFrom(src => src.details));
            cfg.CreateMap<DetailDTO, Detail>();              
                });

    IMapper mapper = config.CreateMapper();

        var context = new MyContext();
        var master = context.Masters.Include(m => m.details).find(1);
             // there is master in db with id = 1

        var masterDTO = mapper.Map<Master, MasterDTO>(master);

        masterDTO.masterInfo = "Changed value";
        foreach (DetailDTO element in masterDTO.details) {
            element.detailInfo = "Changed value";
        }

        var newElement = new DetailDTO {id = 0,  masterId = 1, detailInfo="New Detail"};
        masterDTO.details.Add(newElement);

        master = mapper.Map(masterDTO, master);

        context.SaveChanges();

}

Последнее моделирование, которое я получил этот результат

Before :                                After :
Master                                  Master
+----+-------------------+              +----+-------------------+   
| id | masterInfo        |              | id | masterInfo        |
+----+-------------------+              +----+-------------------+
| 1  | Old Master Info 1 |              | 1  | Changed value     |
| 2  | Old Master Info 2 |              | 2  | Old Master Info 2 |
+----+-------------------+              +----+-------------------+

Detail                                  Detail
+----+----------+-------------------+   +----+----------+-------------------+   
| id | masterId | detailInfo        |   | id | masterId | masterInfo        |
+----+----------+-------------------+   +----+----------+-------------------+
| 1  | 1        | Old Detail Info 1 |   | 1  | 1        | old Detail Info 1 |
| 2  | 1        | Old Detail Info 2 |   | 2  | 1        | Old Detail Info 2 |
|    |          |                   |   | 3  | 1        | New detail        | 
+----+----------+-------------------+   +----+----------+-------------------+

2 ряда значений не обновляются

Спасибо за совет. - Джигу

Ответы [ 2 ]

2 голосов
/ 05 марта 2020

Вам не нужно использовать context.Masters.Add(master);

, и вы должны изменить конфигурацию mapper на

        var config = new MapperConfiguration(cfg =>
        {
            cfg.CreateMap<Master, MasterDTO>().ForMember(a => a.details, map => map.MapFrom(src => src.details));
            cfg.CreateMap<Detail, DetailDTO>();
            cfg.CreateMap<MasterDTO, Master>().ForMember(a => a.details, map => map.MapFrom(src => src.details));
            cfg.CreateMap<DetailDTO, Detail>(); 
        });

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

IMapper mapper = config.CreateMapper();

var context = new MyContext();
var master = context.Masters.Include(m => m.details).FirstOrDefault();

var masterDTO = mapper.Map<Master, MasterDTO>(master);

masterDTO.masterInfo = "master - changed to new value";
foreach (DetailDTO element in masterDTO.details)
{
    element.detailInfo = "detail - changed to new value";
}

// try to add new element 
var newElement = new DetailDTO { id = 0, masterId = 1, detailInfo = "New Detail" };
masterDTO.details.Add(newElement);

Console.Write(context.Entry(master).State.ToString());  //--> Detached
master = mapper.Map(masterDTO, master);
Console.Write(context.Entry(master).State.ToString());  //--> Detached


if (context.Entry(master).State == EntityState.Detached)
{
    context.Masters.Attach(master);
}
context.SaveChanges();
1 голос
/ 05 марта 2020

При выполнении обновления вашего кода с помощью mapper.Map правильно, однако вам необходимо удалить следующие строки:

context.Masters.Add(master);
context.Entry(master).State = EntityState.Modified;

Ваш контекст загружен и отслеживает экземпляр Master, поэтому все, что вам нужно сделать обновить свойства (что делает Mapper.Map), затем вызвать SaveChanges в контексте, а EF позаботится об остальном.

Add используется для добавления нового экземпляра сущности в DbContext. Установка состояния в Modified необходима только при подключении экземпляра к DbContext. В вашем случае сущность уже связана.

Обычно эта проблема возникает, когда разработчики используют вызов по умолчанию mapper.Map:

// Loads the entity which the Context will track, but then mapper.Map() returns a new instance in the reference. The context is still tracking the first reference.    
var master = context.Masters.Single(x => x.MasterId = masterDTO.MasterId);
master = mapper.Map<Master>(masterDTO);

Этот метод создает новую сущность Mapper со свойствами, которые не связан с контекстом, поэтому они будут использовать Add, Update или Attach + .State = EntitySate.Modified, чтобы попытаться поместить его в контекст, что приведет к ошибке, когда контекст уже отслеживает соответствующий объект.

Обновление: чтобы включить отслеживание изменений через связанные свойства, необходимо пометить свойства навигации как virtual, чтобы включить прокси.

public class Master 
{
        public int id {get;set;}
        public string masterInfo {get;set;}
        public virtual ICollection<Detail> details { get;set; } = new Collection<Detail>();
}

public class Detail 
{
    public int id {get;set;}
    public int masterId {get;set;}
    public virtual Master master {get;set;} 
    public string detailInfo {get;set;}
}

Обновление 2: Сбой при обновлении сценария ios.

Похоже, что путаница основана на смешении понятий из двух основных способов обновления сущностей в EF. Вот краткий анализ двух подходов:

Подход 1: С отслеживанием / прокси. По умолчанию EF DbContexts будет отслеживать объекты, которые они загружают, используя прокси-оболочки. Это позволяет загружать связанные объекты с отложенной загрузкой, но, что более важно, позволяет EF обнаруживать, когда отдельные столбцы изменяются для использования в инструкциях UPDATE. Чтобы использовать этот подход, свойства навигации должны быть помечены как virtual, контекст БД должен быть настроен для автоматического обнаружения изменений. (включено по умолчанию) и запросы должны не использовать AsNoTracking. Использование этого подхода является самым простым способом загрузки данных, внесения обновлений и сохранения изменений. Для связанных сущностей, которые вы хотите обновить, используйте Include для их загрузки.

var parent = context.Parents.Include(x => x.Children).Single(x => x.ParentId == parentId);
parent.PhoneNumber= "0456-7689";
foreach(var child in parent.Children)
{
   child.IsAttending = true;
}
context.SaveChanges();

Преимущества этого подхода в том, что он прост. Нет необходимости устанавливать измененное состояние, присоединять к контексту или беспокоиться о дубликатах записей. Недостатком этого подхода является попытка обновить большие объемы данных. Чем больше строк отслеживает DbContext, тем дольше считывания и обновления будут выполняться. Кроме того, что-то простое, например, случайное добавление AsNoTracking() к запросу или отключение виртуального свойства навигации, приведет к ухудшению поведения.

Подход 2: Без отслеживания. Иногда код, использующий EF, захочет работать с отсоединенными сущностями. Это может быть связано с тем, что объекты сериализуются назад и вперед к клиенту / потребителю, или имеют дело с большим количеством объектов, или просто предпочтительным (хотя и сложным) решением проекта, разработанным командой разработчиков. В этом случае DbContext не должен отслеживать экземпляры, и эти экземпляры должны находиться в отдельном состоянии. Так что простым примером этого будет что-то вроде:

var parent = context.Parents.AsNoTracking().Include(x => x.Children.AsNoTracking()).Single(x => x.ParentId == parentId);
parent.PhoneNumber= "0456-7689";
foreach(var child in parent.Children)
{
   child.IsAttending = true;
}

Теперь в этом случае мы не можем просто вызвать context.SaveChanges(). Не будет ошибки, но ничего не будет сохранено, поскольку контекст не отслеживает эти объекты или не обнаруживает изменения.

Мы должны явно связать их обратно с DbContext и установить их измененное состояние:

context.Attach(parent); // This will attach the parent, and the children, but in an Unmodified state.
context.Entity(parent).State = EntityState.Modified;
foreach(var child in parent.Children)
{
   context.Entity(child).State = EntityState.Modified;
}
context.SaveChanges();
// In some cases we will want to detach the parent and children again here.

При таком подходе вам нужно быть более осмотрительным при повторной ассоциации сущностей с DbContext. Проблема может возникнуть, когда рассматриваемая сущность была десериализована или контекст довольно долгоживущий, где она, возможно, уже отслеживала сущность. В этих случаях вызов Attach() может завершиться ошибкой, поэтому, чтобы быть в безопасности, вы должны убедиться, что объект еще не отслежен контекстом. Если сущность была передана в метод, в котором вы хотите выполнить обновление, вы также должны проверить, что сущность не отслеживается другим DbContext.

Например, с помощью метода, подобного приведенному ниже:

public void UpdateParentDetails(Parent parent)
{
    parent.PhoneNumber= "0456-7689";
    foreach(var child in parent.Children)
    {
       child.IsAttending = true;
    }
    _context.Attach(parent); 
    _context.Entity(parent).State = EntityState.Modified;
    foreach(var child in parent.Children)
    {
       context.Entity(child).State = EntityState.Modified;
    }
    _context.SaveChanges();
}

Код, подобный этому, может быть подвержен проблемам и неправильному использованию. Был ли переданный родительский объект уже связан с контекстом, с тем же _context или другим экземпляром контекста? _Context отслеживает другую ссылку на этого родителя? Дети были загружены? Кто-нибудь из детей отслеживается? Что мы должны делать в любом из этих случаев?

Как минимум, мы должны утверждать, что переданный родительский элемент не был нулевым, не был связан с DbContext, и проверить, что мы еще не отслеживаем parent:

public void UpdateParentDetails(Parent parent)
{
    if (parent == null)
        throw new ArgumentNullException("parent");

    if (parent.State != EntityState.Detached)
        throw new ArgumentException("Parent was associated to a DbContext");

    var existingParent = _context.Parents.Local.Single(x => x.ParentId == parentId);
    if (existingParent != null)
    {
        existingParent.PhoneNumber= "0456-7689";
        foreach(var child in existingParent.Children)
        {
           child.IsAttending = true;
        }
    }
    else
    {
        parent.PhoneNumber= "0456-7689";
        foreach(var child in parent.Children)
        {
           child.IsAttending = true;
        }
        _context.Attach(parent); 
        _context.Entity(parent).State = EntityState.Modified;
        foreach(var child in parent.Children)
        {
           context.Entity(child).State = EntityState.Modified;
        }
    }
    _context.SaveChanges();
}

Как вы можете видеть, это начинает немного усложняться, чтобы попытаться убедиться, что предположения о состоянии объекта и о том, отслеживает ли DbContext экземпляр. Вот почему я обычно не советую командам разработчиков пытаться работать с отдельными объектами. Код / намерение начинается достаточно просто, но почти всегда начинает сталкиваться с проблемами, которые приводят к большему количеству кода, большей сложности и большему количеству ошибок. По этой причине я рекомендую, чтобы сущности никогда не передавались за пределы области DbContext, в которой они были прочитаны. Использование DTO или ViewModels является наиболее предпочтительным подходом к этому, а затем использует подход № 1 выше для загрузки, обновления и сохранения объекта. Ключ, чтобы избежать смешивания элементов из подхода № 2.

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