Entity Framework - Update nested lists

Asked

Viewed 1,128 times

5

I have the following schema in my Mysql BD:

inserir a descrição da imagem aqui

Models have a list of standard_images and each standard_images has a list of standard_regions. I must use Entity Framework 5.0.0. I have a model with 3 standard_images and each of them has 4 standard_regions. I need to update this template where I will remove standard_region 2 and 3 from standard_image 2.

inserir a descrição da imagem aqui

To update, I’m going to the BD, looking for the Id of the model I want to change, and through a foreach I’m scanning the BD model and updating with the data coming from the graphical interface.

But how to update this model when I need to remove a stdRegion, or a Stdimg, IE, when there is the divergence between the lists that originally exist in BD and the new data that the user wants to change, either adding or removing components from 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, Image and Region classes define the transfer objects where I bring the changed user data 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; }
}
  • To improve understanding, it would be an update with an deletion (or addition) of some item from one of the two lists.

  • It has so many forms and scenarios, because, everything is in relation to the graphical interface that manipulates the data and depending can be direct exclusions since each model (entity) has its primary key.

  • His model assembly is very peculiar. I still don’t understand where are the statements of standard_images and standard_regions.

3 answers

3

First, make sure that Image refer Region with cascadeDelete enabled. This 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, what reference Image with cascadeDelete qualified:

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);

Thus, it is sufficient to exclude a Image so that their respective Regions are updated.

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


This code:

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

Not good, since you only carry the record of Model, and not its dependent entities. What is happening here:

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

It is that you are performing a detached context load. The Entity Framework does not admit that these Region are the original records in table, therefore when you delete a Region:

image.standard_regions.Remove(reg);

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

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

Can make the Regions be reinserted as new records. After all, the SaveChanges() is at the end of the method.

What’s the right way?

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();
  • Gypsy, bd Mysql was not set to Cascade. I did the setup and it worked. But now when I delete a Stdregion, actually the record is not deleted, only null is placed in the standard_regions table in the standard_images_id column. id='145', Description='520-3', coordinate= '1660;2070;-0,6174323;190;135', standard_images_id= 'null'. Registration should not be deleted completely?

  • 1

    Very odd. Did not apply the cascade right. You can update your question with the code being used to delete the Region?

  • @Emerson I updated the answer.

  • OK understood, I will update and let you know the result. I believed that doing this models modBanco = db.models.Find(modAlterado.id); the Entity would also seek relationships. In my understanding, I need to explain the level of granularity that it should look for in BD. That’s right what I’m thinking?

  • 1

    Exactly. The Find is the cheapest operation because it brings only the first level of data. SingleOrDefault() with the Includeexplicit s ensures the search up to the third level, which is your case.

3

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

I define an interface IObjectWithStatethat all classes of my model implement:

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

I set a help class to convert ObjectStatein 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 compound entity its status: Added, Modified or Deleted.

The method, as accepted params TEntity[], can be used to treat multiple entities at the same time.

Based on some articles I read, notably by Julie Lerman.

2


With the tips that colleagues Gypsy Morrison Mendez and Rogério Amaral (ramaral) passed me, I came to a solution that met what I was needing.

My model in the comic book really needed to look that way, as the Gypsy guided 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 in the BD to change it with the data coming from the UI.

The colleague Rogério Amaral (ramaral) showed his approach to update composite entities and studying this type of approach that he did not know, adjusted my code to correctly update the list of images and its sublists of ROIS (regions of interest). The biggest work was the execution of logic to update the BD model with the data coming from the Ui. Below follows the code that updates the model:

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 BD model that does not exist in the UI, I need to delete it. I used the status that Entityframework returns. All entities that return from the BD have the status Unchanged. When I add something to the template, I change its status to Added , when I update use the status Modified, and those who do nothing have the status Unchanged. They exist in my model and not in the UI, so I need to delete them from my model. For this I made 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 really helped me, was a story I found on the Macoratti website:

http://www.macoratti.net/11/11/ef4_cve1.htm

There are explanations about this part of the Entityframework States...

  • The code can still be minimized. Anyway, I think it’s good. + 1.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.