Many-To-Many Entityframework update does not work

Asked

Viewed 427 times

0

I have a relationship N:N between Activity and Project, where an activity has many projects:

public class Atividade : ObjetoPersistente
{

    public Atividade()
    {
        StatusAtividade = EStatusAtividade.NaoIniciado;
        TipoAtividade = ETipoAtividade.NovaImplementacao;
        Usuario = new Usuario();
        Projetos = new List<Projeto>();
    }

    [JsonConverter(typeof(FormatoDataHoraMinutoNullableConverter))]
    public DateTime? DataHoraFim { get; set; }

    [JsonConverter(typeof(FormatoDataHoraMinutoNullableConverter))]
    public DateTime? DataHoraInicio { get; set; }

    public string DescricaoAtividade { get; set; }
    public string EstimativaInicialAtividade { get; set; }      

    public Usuario Usuario { get; set; }

    public string LoginUsuario
    {
        get { return Usuario.Login; }
    }      

    public EStatusAtividade StatusAtividade { get; set; }

    public string DescricaoStatusAtividade
    {
        get { return StatusAtividade.Descricao; }
    }           

    public string DescricaoTipoAtividade
    {
        get { return TipoAtividade.Descricao; }
    }

    public ETipoAtividade TipoAtividade { get; set; }
    public string TituloAtividade { get; set; }

    public long CodigoUsuario
    {
        get { return Usuario.Codigo; }
        set { Usuario.Codigo = value; }
    }

    public List<Projeto> Projetos { get; set; }


    public List<long> CodigoProjetos
    {
        get { return ObtenhaListaDeCodigosPorListaDeObjetos(Projetos); }

        set { Projetos = ObtenhaListaDeObjetoPorListaDeCodigos<Projeto>(value); }
    }

    public override bool Equals(object obj)
    {
        return (obj is Atividade) && (obj as Atividade).Codigo.Equals(Codigo);
    }

    public override int GetHashCode()
    {
        return Codigo.GetHashCode();
    }
}

Project:

public class Projeto : ObjetoPersistente, IObjetoElementoOption
{       
    public string Nome { get; set; }


    public override bool Equals(object obj)
    {
        return (obj is Projeto) && (obj as Projeto).Codigo.Equals(Codigo);
    }

    public override int GetHashCode()
    {
        return Codigo.GetHashCode();
    }

    public string Valor
    {
        get { return Codigo.ToString(); }
    }

    public string Descricao {
        get { return Nome; }
    }
}

The mappings are this way:

    public AtividadeMap()
    {
        HasKey(a => a.Codigo);

        ToTable("tb_atividade");

        Property(a => a.TituloAtividade).HasColumnName("titulo_atividade");
        Property(a => a.DescricaoAtividade).HasColumnName("descricao_atividade");
        Property(a => a.DataHoraInicio).HasColumnName("data_hora_inicio");
        Property(a => a.DataHoraFim).HasColumnName("data_hora_fim");
        Property(a => a.Codigo).HasColumnName("pk_atividade");
        Property(a => a.StatusAtividade.Identificador).HasColumnName("status_atividade");
        Property(a => a.EstimativaInicialAtividade).HasColumnName("estimativa_inicial_atividade");
        Property(a => a.TipoAtividade.Identificador).HasColumnName("tipo_atividade");
        Property(a => a.CodigoUsuario).HasColumnName("fk_usuario");

        HasRequired(a => a.Usuario)
            .WithMany()
            .HasForeignKey(a => a.CodigoUsuario);

        HasMany(a => a.Projetos)
            .WithMany()
            .Map(pa =>
            {
                pa.MapLeftKey("fk_atividade");
                pa.MapRightKey("fk_projeto");
                pa.ToTable("tb_atividade_projeto");
            });
    }


public class ProjetoMap : EntityTypeConfiguration<Projeto>
{
    public ProjetoMap()
    {            
        HasKey(a => a.Codigo);

        ToTable("tb_projeto");

        Property(p => p.Codigo).HasColumnName("pk_projeto");
        Property(a => a.Nome).HasColumnName("nome");
    }

}

The consultation process is OK, however, inclusions and changes do not work.

What is missing?

After researching the subject, I found several answers like this, which in short, in my situation, I would have to carry each Project of the object list Activity to accomplish a Attach in the context class:

https://stackoverflow.com/questions/14631309/updating-many-to-many-navigation-property-in-entity-framework-6-changes-not-bei

Is this the only way to update the relationship situation Many-to-Many? Could it make the persistence of these relationships generic?

  • Yeah, it’s confusing at first. You need to recover the objects and give attach, if you do not and try to add, the framework will probably create NEW objects instead of using the old ones. If you get what you needed, summarize what you needed as an answer. This question is very important.

  • @Rsinohara actually I’ve seen working this way, however, I’m interested in more simplified and optimized ways to do this.

  • I get it... I spent a lot of time getting used to the way this works. And I wrote gigantic codes along the way.

  • I was going to try to answer, but I didn’t understand any of your question. I don’t even know where is the relation N to N.

  • @Joaquimmagalhães The relationship you describe in the code is 1:N. I was going to suggest an edit on the question and the title, but I thought it would be a very 'heavy' change. But I think you should change.

  • As @Ciganomorrisonmendez noted, my code visualizes the relation N:N in a different way from the conventions of N:N of EF. But it’s an N:N relationship, just as I exemplified in an analogy in his reply.

Show 1 more comment

2 answers

3

Well, a priori you do not need to load related entities always. Especially when making changes.

Now, whenever using a reference to an entity that exists in the database, you need to give attach or recover entities. Do not always search in the database, but need to give attach in the dbcontext.

Interestingly, I have a question just like that in the OS, which got no answer (and I had to go back and answer): Question OS.

At first it seems strange: having to recover the entities that are already there. But it works like this. Let’s say you already have the projects, let’s say returned from the user interface in a web application. Let’s also say that you trust that these objects are ok. You could create a new activity and insert these projects, right? No. If you do it like this, DBContext will actually create new projects, because the ones you are using are not attached.

An alternative way to do it is to give attach and change the state of the entities to unchanged. So the DBContext does not change these projects, but neither does it modify them.

But deep down, the practice of picking them up again at the bank is correct. I can’t imagine a scenario where you might have dettached objects that are safe, even if it is because they were previously recovered (possibly WELL before) may have been removed from the BD.


Therefore, yes, this logic with attach is the only way to update entities with N:N or 1:N relations.

You can encapsulate this in their repositories, but not in a way generic. It is exactly the fact that each entity has special relationships that make these logics necessary.

  • I fill the bigger problem in a macro situation, where I have several one-to-Many relationships like this. And the entity’s key link should precisely encapsulate this bureaucracy. So I look forward to a more optimistic response to this.

  • 1

    If there is, I’ll be happy too :)

  • I need this answer because I’m doing proof of concept with the framework, a migration from a pure ADO.NET project to a ORM, whether EF or Nhibernate... And these situations as well as others that I’ve encapsulated, need to be generic, seeking productivity.

  • As far as I know it is necessary to attach, or search in the BD the entities of the relation and replace the entities dettached by the ones that were searched. Marginalemte, you could carry 'empty' objects, but with the correct ID, give attach and set the status for unchanged... The DBContext will not insert or update them, and will be able to be used in relation N:N (or 1:N)

  • I understand. This in Hibernate is so simple! Having the mapped keys and the mapped relationships, EF should understand this process...

  • So, there’s your weirdness, though more practical. If the ORM sees the ID of an entity and already 'gets' that it is an item that exists in the BD, all right, it is practical, but where do I get this ID? If it’s web, for example, it probably went through a post. What if the user changes it? Any verification involves checking on the BD, so why not use the returned entity? Thus, the ORM doing this implicitly can be dangerous.

Show 1 more comment

3


I think I understand what you are trying to do. Your relationship is wrong. If a Projeto has N Atividades, and a Atividade belongs to N Projetos, you never could wear something like that:

public class Atividade : ObjetoPersistente
{

    public Atividade()
    {
        ...
        Projetos = new List<Projeto>();
    }

    ...
    public List<Projeto> Projetos { get; set; }    
    ...
}

With that you’re saying that a Atividade has N Projetos, but that a Projeto belongs to only one Atividade.

The right thing would be:

[Table("tb_atividade")]
public class Atividade : ObjetoPersistente
{

    public Atividade()
    {
        ...
        // Retire isso
        // Projetos = new List<Projeto>();
    }

    ...
    // Não use Projetos diretamente. Use uma tabela associativa.
    public virtual ICollection<ProjetoAtividade> ProjetoAtividades { get; set; }    
    ...
}

Projeto also receives ProjetoAtividades, because it’s an association:

[Table("tb_projeto")]
public class Projeto : ObjetoPersistente, IObjetoElementoOption
{       
    ...
    public virtual ICollection<ProjetoAtividade> ProjetoAtividades { get; set; }  
}

Now you need to map too ProjetoAtividade:

[Table("tb_atividade_projeto")]
public class ProjetoAtividade
{
    [Key]
    [Column("fk_atividade")]
    public int AtividadeId { get; set; }

    [Key]
    [Column("fk_projeto")]
    public int ProjetoId { get; set; }

    // Estas são propriedades de navegação.
    // O Entity Framework carrega essas classes automaticamente.
    public virtual Projeto Projeto { get; set; }
    public virtual Atividade Atividade { get; set; }
}

Note that I don’t need to use Fluent API here. I can just decorate the properties with Attributes and the Entity Framework does everything else on its own.

Inclusions and Modifications

How to map the Models is wrong, you will need to map Codigo in the classes to function:

[Table("tb_atividade")]
public class Atividade : ObjetoPersistente
{
    [Key]
    // Não sei se a coluna no banco chama "codigo", mas suponho que sim.
    [Column("codigo")]
    public int Codigo { get; set; }

    ...
}

[Table("tb_projeto")]
public class Projeto : ObjetoPersistente, IObjetoElementoOption
{       
    ...
    [Key]
    // Não sei se a coluna no banco chama "codigo", mas suponho que sim.
    [Column("codigo")]
    public int Codigo { get; set; }
}

About inclusions, to insert a new association, do as follows:

// Carregue o projeto
var projeto = contexto.Projetos.FirstOrDefault(p => p.Codigo == /* Coloque o valor da chave aqui */);
// Carregue a atividade
var atividade = contexto.Atividades.FirstOrDefault(a => a.Codigo == /* Coloque o valor da chave aqui */);
if (projeto != null && atividade != null)
{
    var projetoAtividade = new ProjetoAtividade 
    {
        // É assim mesmo que usa. __NUNCA__ defina o Id diretamente.
        Atividade = atividade,
        Projeto = projeto
    }

    contexto.ProjetoAtividades.Add(projetoAtividade);
    contexto.SaveChanges();
}

As the change to the associative entity does not make sense, I will teach you how to make an exclusion:

var projetoAtividade = contexto.ProjetoAtividades.FirstOrDefault(/* Coloque aqui a condição para selecionar a associação, aqui usando os Ids */);
if (projetoAtividade != null)
{
    contexto.ProjetoAtividades.Remove(projetoAtividade);
    contexto.SaveChanges();
}
  • Thank you for the reply Gypsy, however, the objects are not wrong. In an analogy, Atividade would be a Carro, and Projeto would be a Acessorio. A car has several accessories, and the same accessory can belong to several cars .

  • So it’s not association N to N. Your question is not well posed. It’s 1 to N. Correct the statement so I can correct the answer, please.

  • Sorry, improving the analogy: a car can have leather seat and neon headlights. Any car can have these accessories (leather seat and neon headlights). I believe you did not understand the analogy.

  • So in fact your code is incorrect anyway. Entity Framework is not Hibernate: you cannot express objects in the same way.

  • Exactly, Gypsy. I am currently using Nhibernate and I see that the Entityframework conventions are very particular. That’s the conclusion I came to.

  • I will reward the best solution for the two exemplified objects. (Car and Accessory) or those of the problem (Activity and Project).

  • @Joaquimmagalhães Particularly, I never liked the type of mapping of Hibernate because it is prone to ambiguities. In addition, in the associative case, for the case of Hibernate, it is complex to define extra information belonging to the association. For example, in your case (Projects and Activities), if I wanted to stipulate that for each project the activity has a "Hours" field, in EF it is quite simple. What’s left for the answers to meet you?

  • Your answer clarified, it really wasn’t what I expected. Thank you.

Show 3 more comments

Browser other questions tagged

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