Entity Framework - Update nested lists

5

I have the following schema in my DB MySql:

Modelshavealistofstandard_imagesandeachstandard_imageshasastandard_regionslist.IneedtouseEntityFramework5.0.0.Ihaveatemplatewith3standard_imagesandeachonehas4standard_regions.IneedtodoanupdateonthistemplatewhereIwillremovestandard_region2and3fromstandard_image2.

To do the update, I'm going to the DB, searching for the ID of the model I want to change, and through a foreach I'll be sweeping the DB model and updating with the data coming from the GUI.

But how to do this update in this model when I need to remove a stdRegion, or a StdImg, that is, when there is a divergence between the lists that originally exist in the DB and the new data that the user wants to change, either adding or removing components of both lists?

The code to delete a standard_region is this:

public void AlterarModelo(Modelo modAlterado)
{
    //Busca no BD o modelo a ser alterado
    models modBanco = db.models.Find(modAlterado.id);

    //apaga do banco caso não exista na lista passada via interface
    foreach(var image in modBanco.standard_images)
    {
        List<standard_regions> lista = new List<standard_regions>();

        foreach (var regiao in image.standard_regions)
        {
            foreach (var a in modAlterado.lstImagemPadrao)
            {
                if (a.lstRegiaoInteressePadrao.Count(x => x.id == regiao.id) == 0)
                {
                    var regTemp = db.standard_regions.Find(regiao.id);

                    lista.Add(regTemp);
                }
            }
        }

        foreach (var reg in lista)
        {
            image.standard_regions.Remove(reg);
        }
    }


    //adiciona caso não esteja no banco
    foreach (var imgAlterado in modAlterado.lstImagemPadrao)
    {
        foreach (var imgBanco in modBanco.standard_images)
        {
            foreach (var regAlterado in imgAlterado.lstRegiaoInteressePadrao)
            {
                if (regAlterado.id == 0)
                {
                    var regTemp = db.standard_regions.Find(regAlterado.id);

                    standard_regions sr = new standard_regions
                    {
                        coordinate = regAlterado.coordinate,
                        description = regAlterado.descricao,
                        standard_images_id = imgBanco.id
                    };
                    imgBanco.standard_regions.Add(sr);
                }
            }
        }
    }

    modBanco.date = modAlterado.data;

    db.SaveChanges();
}

The Model, PivotCode, and RegionInterestPadrao classes define the transfer objects where I bring the data changed by the user in the graphical interface:

public class Modelo
{
    public int id { get; set; }
    public string nomeModelo { get; set; }
    public string statusModelo { get; set; }
    public DateTime data { get; set; }
    public Usuario usuario { get; set; }
    public bool foiInspecionado { get; set; }

    public ImagemPadraoColecao lstImagemPadrao { get; set; }

    public Modelo()
    {
        lstImagemPadrao = new ImagemPadraoColecao();
        usuario = new Usuario();
    }
}

public class ImagemPadrao
{
    public int id { get; set; }
    public string nomeImagemPadrao { get; set; }
    public string caminhoImagemPadrao { get; set; }
    public Modelo modelo { get; set; }

    public RegiaoInteressePadraoColecao lstRegiaoInteressePadrao { get; set; }

    public ImagemPadrao()
    {
        lstRegiaoInteressePadrao = new RegiaoInteressePadraoColecao();
    }
}

public class RegiaoInteressePadrao
{
    public int id { get; set; }
    public string descricao { get; set; }
    public string coordinate { get; set; }
    public int imagemPadraoId { get; set; }
}
    
asked by anonymous 10.06.2014 / 14:08

3 answers

2

With the tips that colleagues Cigano Morrison Mendez and Rogério Amaral (ramaral) passed me, I came up with a solution that met what I needed.

My model in BD really needed to look this way, as Gypsy directed me:

models modBanco = db.models.Include("standard_images.standard_regions").SingleOrDefault(m => m.id == modAlteradoUi.id);

That way I was able to get my model there in the DB to change it with the data coming from the UI.

The colleague Rogério Amaral (ramaral) showed his approach to updating composite entities and studying this type of approach that he did not know, I adjusted my code to correctly update the list of images and their ROIS sublots (regions of interest). The biggest work was the execution of the logic to update the DB model with the data coming from the Ui. Here is the code that updates the template:

public void AlterarModelo(Modelo modAlteradoUi)
        {
            models modBanco = db.models.Include("standard_images.standard_regions").SingleOrDefault(m => m.id == modAlteradoUi.id);
            modBanco.name = modAlteradoUi.nomeModelo;
            modBanco.users_id = modAlteradoUi.usuario.id;
            modBanco.status = modAlteradoUi.statusModelo;
            modBanco.date = modAlteradoUi.data;

            //Adiciona imagens novas, vindas da UI, com suas novas ROIS
            foreach (var imagemDaUi in modAlteradoUi.lstImagemPadrao)
            {
                if (imagemDaUi.id == 0)//Se for uma imagem nova
                {
                    //Cria essa imagem para adicionar no modelo do bd
                    standard_images siTmp = new standard_images();
                    siTmp.id = 0;
                    siTmp.name = imagemDaUi.nomeImagemPadrao;
                    siTmp.models_id = modBanco.id;
                    siTmp.path = imagemDaUi.caminhoImagemPadrao;

                    //Cria lista de Roi dessa imagem
                    List<standard_regions> lstRoitmp = new List<standard_regions>();

                    siTmp.standard_regions = lstRoitmp; //Add a lista de Roi na Nova Imagem

                    if (imagemDaUi.lstRegiaoInteressePadrao != null)//Adiciona ROI se existir na imagem da UI
                    {
                        foreach (var roiTmp in imagemDaUi.lstRegiaoInteressePadrao)
                        {
                            standard_regions stdRoiTmp = new standard_regions();
                            stdRoiTmp.id = 0;
                            stdRoiTmp.coordinate = roiTmp.coordinate;
                            stdRoiTmp.description = roiTmp.descricao;

                            lstRoitmp.Add(stdRoiTmp);
                        } 
                    }

                    modBanco.standard_images.Add(siTmp);//Add a nova imagem com suas ROIS no modelo
                }
                else    //Se a imagem já existir no BD tenho que alterar 
                {       //as Rois e adicionar as Rois novas para essa imagem já existente
                    foreach (var roiDaUi in imagemDaUi.lstRegiaoInteressePadrao)
                    {
                        if (roiDaUi.id == 0)
                        {
                            standard_regions novoRoiDaUi = new standard_regions
                            {
                                coordinate = roiDaUi.coordinate,
                                description = roiDaUi.descricao,
                                standard_images_id = imagemDaUi.id
                            };

                            foreach (var imgBanco in modBanco.standard_images.ToList())
                            {
                                if (imgBanco.id == imagemDaUi.id)
                                {
                                    //Se estou atribuindo um id da imagemDaUi em uma imgBanco,
                                    //essa imagem do banco existe não está sendo removida do
                                    //BD. Então preciso mudar o status dela para modified e adicionar
                                    //a nova roi.
                                    db.Entry(imgBanco).State = System.Data.EntityState.Modified;
                                    imgBanco.standard_regions.Add(novoRoiDaUi);
                                }
                            }
                        }
                        else
                        {
                            foreach (var imgBanco in modBanco.standard_images.ToList())
                            {
                                if (imgBanco.id == imagemDaUi.id)
                                {
                                    db.Entry(imgBanco).State = System.Data.EntityState.Modified;

                                    foreach (var roiBd in imgBanco.standard_regions)
                                    {
                                        if (roiDaUi.id == roiBd.id)
                                        {
                                            roiBd.coordinate = roiDaUi.coordinate;
                                            roiBd.description = roiDaUi.descricao;
                                            roiBd.standard_images_id = roiDaUi.imagemPadraoId;
                                            db.Entry(roiBd).State = System.Data.EntityState.Modified;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }


            db.models.Attach(modBanco);

            foreach (var imagem in modBanco.standard_images.ToList())
            {
                RemoveDeletedRoi(imagem.standard_regions);              
            }

            RemoveDeletedImg(modBanco.standard_images);

            db.Entry(modBanco).State = System.Data.EntityState.Modified;

            db.SaveChanges();
        }

When there is data in the DB template that does not exist in the UI, I need to delete it. For this I used the statuses that the EntityFramework returns. All entities that return from the DB have the status Unchanged . When I add something to the model, I change its status to Added , when I update I use the status Modified , and in those that do nothing, they have the status Unchanged . They exist in my model and not in the UI, so I need to exclude them from my model. For this I did the two methods below to help me.

private void RemoveDeletedImg(ICollection<standard_images> LstStdImagesBd)
        {
            var deletedImg = (from img in LstStdImagesBd where db.Entry(img).State == System.Data.EntityState.Unchanged select img).ToList();

            foreach (var img in deletedImg)
            {
                db.Entry(img).State = System.Data.EntityState.Deleted;
            }
        }

        private void RemoveDeletedRoi(ICollection<standard_regions> LstStdRoisBd)
        {
            var deletedRoi = (from roi in LstStdRoisBd where db.Entry(roi).State == System.Data.EntityState.Unchanged select roi).ToList();

            foreach (var roi in deletedRoi)
            {
                db.Entry(roi).State = System.Data.EntityState.Deleted;
            }
        }

Another thing that helped me a lot was a story I found on the Macoratti website:

link

There are explanations of this part of EntityFramework States ...

    
16.06.2014 / 05:20
3

First, make sure that Image references Region with cascadeDelete enabled. Here is an example of Migration :

CreateTable(
        "dbo.Images",
        c => new
            {
                /* Coloque a declaração das colunas aqui */
            })
        .PrimaryKey(t => t.ImageId)
        .ForeignKey("dbo.Regions", t => t.RegionId, cascadeDelete: true)
        .Index(t => t.ImageId);

You can enable the same mechanism for Model , which references Image with cascadeDelete enabled:

CreateTable(
        "dbo.Models",
        c => new
            {
                /* Coloque a declaração das colunas aqui */
            })
        .PrimaryKey(t => t.ModelId)
        .ForeignKey("dbo.Images", t => t.ImageId, cascadeDelete: true)
        .Index(t => t.ModelId);

So, just delete a Image so that its Regions is updated.

In the case of the update, I believe you will have to continue going through element to element to update. There is no easy way.

This code:

models modBanco = db.models.Find(modAlterado.id);

Not good, since you only load the Model registry, not its dependent entities. What's happening here:

var regTemp = db.standard_regions.Find(regiao.id);

It's that you're performing a context-sensitive load. The Entity Framework does not admit that these Region records are the original table records, so when you delete a Region :

image.standard_regions.Remove(reg);

When saving the changes in context, the Regions are erased correctly. Only this here:

foreach (var imgAlterado in modAlterado.lstImagemPadrao)
{ ... }

You can have Regions reinserted as new records. After all, SaveChanges() is at the end of the method.

Which way is correct?

Change your code to the following:

//Busca no BD o modelo a ser alterado
models modBanco = db.models.Include(m => m.standard_images.standard_regions).SingleOrDefault(m => m.id == modAlterado.id);

//apaga do banco caso não exista na lista passada via interface
foreach(var image in modBanco.standard_images)
{
    foreach (var regiao in image.standard_regions)
    {
        foreach (var a in modAlterado.lstImagemPadrao)
        {
            if (a.lstRegiaoInteressePadrao.Count(x => x.id == regiao.id) == 0)
            {
                db.standard_regions.Remove(regiao);
            }
        }
    }
}

db.SaveChanges();
    
10.06.2014 / 18:24
3

This is the approach I use when I want to update composite entities:

I define a IObjectWithState interface that all classes in my model implement:

public interface IObjectWithState
{
    ObjectState State { get; set; }
}
public enum ObjectState
{
    Unchanged,
    Added,
    Modified,
    Deleted
}

I define a help class to convert ObjectState to EntityState :

public static class StateHelpers
{
    public static EntityState ConvertState(ObjectState objectState)
    {
        switch (objectState)
        {
            case ObjectState.Added:
                return EntityState.Added;
            case ObjectState.Modified:
                return EntityState.Modified;
            case ObjectState.Deleted:
                return EntityState.Deleted;
            default:
                return EntityState.Unchanged;
        }
    }
}

In my Repository I have a method that deals with composite entities:

public class EFEntityRepository<TEntity> :
    where TEntity : class, IObjectWithState
{
    public DbContext Context { get; set; }

    ------------------
    -------------------
    ------------------- 

    public void UpdateEntityGraph(params TEntity[] entities)
    {
        IDbSet<TEntity> set = Context.Set<TEntity>();
        foreach (var entity in entities)
        {
            switch (entity.State)
            {
                case ObjectState.Deleted:
                    set.Attach(entity);
                    set.Remove(entity);
                    break;
                case ObjectState.Added:
                    set.Add(entity);
                    break;
                case ObjectState.Modified:
                case ObjectState.Unchanged:
                    set.Add(entity);
                    foreach (var entry in Context.ChangeTracker.Entries<IObjectWithState>())
                    {
                        IObjectWithState objectState = entry.Entity;
                        entry.State = StateHelpers.ConvertState(objectState.State);
                    }
                    break;
                default:
                    throw new ArgumentException("Uma ou mais entidades estão num estado inválido");
            }
        }
    }

}

Before using the method, I only need to indicate in each entity of the composite entity its status: Added , Modified or Deleted .

The method, like params TEntity[] , can be used to handle multiple entities at the same time.

Based on some articles I've read, including Julie Lerman.

    
11.06.2014 / 13:21