Entity Foreign Key Framework

Asked

Viewed 2,911 times

9

My domain:

public class SBE_ST_CorpoDocente
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public virtual ICollection<SBE_ST_Curso> Cursos { get; set; }
}

public class SBE_ST_Curso
{
    public int Id { get; set; }
    public string Titulo { get; set; }
    public virtual ICollection<SBE_ST_CorpoDocente> Coordenacao { get; set; }
}

Layer of interaction with the bank:

public void Salvar(SBE_ST_Curso entidade)
    {
        var idsCoordenacao = entidade.Coordenacao.Select(c => c.Id).ToList();
        var coordenacao = contexto.CorpoDocente.Where(cd => idsCoordenacao.Contains(cd.Id)).ToList();
        if (entidade.Id > 0)
        {
            var cursoAlterar = contexto.Curso.First(x => x.Id == entidade.Id);
            cursoAlterar.Titulo = entidade.Titulo;
            cursoAlterar.Coordenacao = coordenacao;
            contexto.Entry(cursoAlterar).State = EntityState.Modified;
            contexto.SaveChanges();
        }
        else
        {
            entidade.Coordenacao = coordenacao;
            contexto.Curso.Add(entidade);
            contexto.SaveChanges();
        }

When I enter a new record it works perfectly. When I update a record it gives error. If I clean the table that stores the relationships, then I can edit normal the first time, then gives the same error.

Error:

Additional information: An error occurred while saving entities that do not Expose Foreign key properties for their relationships. The Entityentries Property will Return null because a single Entity cannot be identified as the source of the Exception. Handling of exceptions while saving can be made easier by exposing Foreign key properties in your Entity types. See the Innerexception for Details.

Analyzing the Intellitrace:

Exception:Caught: "Violation of PRIMARY KEY Constraint 'Pk_dbo.Sbe_st_cursosbe_st_corpodocente'. Cannot Insert Duplicate key in Object 'dbo.Sbe_st_cursosbe_st_corpodocente'. The Duplicate key value is (8, 1). The statement has been terminated." (System.Data.Sqlclient.Sqlexception) A System.Data.Sqlclient.Sqlexception was Caught: "Violation of PRIMARY KEY Constraint 'Pk_dbo.Sbe_st_cursosbe_st_corpodocente'. Cannot Insert Dcate uplikey in Object 'dbo.Sbe_st_cursosbe_st_corpodocente'. The Duplicate key value is (8, 1). The statement has been terminated." Time: 11/08/2014 15:28:19 Thread:Worker Thread[3508]

I made a GAMBIARRA to solve this problem. (GAMBIARRA REALLY)

public void LimparRelacionamentos(int id)
{
    SqlConnection MinhaConexao = new SqlConnection(ConfigurationManager.ConnectionStrings["BancoDados"].ConnectionString);
    MinhaConexao.Open();
    string query = "DELETE FROM SBE_ST_CursoSBE_ST_CorpoDocente WHERE SBE_ST_Curso_Id = " + id;
    SqlCommand comando = new SqlCommand(query, MinhaConexao);
    comando.ExecuteNonQuery();
    MinhaConexao.Close();
}



 public void Salvar(SBE_ST_Curso entidade)
        {
            var idsCoordenacao = entidade.Coordenacao.Select(c => c.Id).ToList();
            var coordenacao = contexto.CorpoDocente.Where(cd => idsCoordenacao.Contains(cd.Id)).ToList();
            if (entidade.Id > 0)
            {
                var cursoAlterar = contexto.Curso.First(x => x.Id == entidade.Id);
                cursoAlterar.Titulo = entidade.Titulo;
                LimparRelacionamentos(entidade.Id);
                cursoAlterar.Coordenacao = coordenacao;
                contexto.SaveChanges();
            }
            else
            {
                entidade.Coordenacao = coordenacao;
                contexto.Curso.Add(entidade);
                contexto.SaveChanges();
            }
}
  • The gambiarra only makes you delete the record to then insert again, which does not correctly solve the problem. Could you please fix my Fork on Github so I can simulate the problem by?

  • @Ciganomorrisonmendez, edited the github, see if now it works!

  • @Ciganomorrisonmendez, I added an answer. It would be like you to take a look?

4 answers

7

You are not making the context correctly observe your changes. The context always thinks that the entity coming from the screen is new because it has not loaded the existing record before. It would be right to load a new record if it does not have Id (Id > 0). Otherwise, you need to indicate to the context that the entity has been changed by you, and that the current information of the object should be treated as true.

Another thing is that:

cursoAlterar.Coordenacao = entidade.Coordenacao.Select(
    coordenacao => contexto.CorpoDocente.FirstOrDefault(x => x.Id == coordenacao.Id)).ToList();

This statement makes the context always fill in Coordenacao again. While there seems to be no change, the Entity Framework has a logic that looks at variables. How there was change, and you filled in Coordenacao from the screen, it tries to insert the records as if they were new. It would be right to fill the data with the variable coming from the context, and not the screen.

The code can also be changed to something simpler and removed directly from the context, for example:

cursoAlterar.Coordenacao = contexto.CorpoDocente.Where(
    x => entidade.Coordenacao.Select(e => e.Id).Contains(x.Id))).ToList();

The end result looks something like this.

public void Salvar(SBE_ST_Curso entidade)
{
    if (entidade.Id > 0)
    {
        // Esta linha tem que pelo menos levantar exceção, evitando inserir 
        // alguma bobagem no banco, por isso troquei para SingleOrDefault.
        var cursoAlterar = contexto.Curso.SingleOrDefault(x => x.Id == entidade.Id);

        cursoAlterar.Titulo = entidade.Titulo;
        cursoAlterar.Coordenacao = contexto.CorpoDocente.Where(
            x => entidade.Coordenacao.Select(e => e.Id).ToList().Contains(x.Id)).ToList();
        contexto.Entry(cursoAlterar).State = EntityState.Modified;

        contexto.SaveChanges();
    }
    else
    {
        cursoAlterar.Coordenacao = contexto.CorpoDocente.Where(
            x => entidade.Coordenacao.Select(e => e.Id).ToList().Contains(x.Id)).ToList();
        contexto.Curso.Add(entidade);

        contexto.SaveChanges();
    }
}

EDIT

I made several corrections in your code. I put everything in a Github repository. For what matters to the question, it lacked a robust logic that makes exclusion and inclusion of Faculty Bodies, as explained below:

            // Entradas Excluídas
            var coordenacoesOriginais = contexto.Curso.AsNoTracking().SingleOrDefault(c => c.Id == cursoAlterar.Id).Coordenacao;

            foreach (var coordenacaoPossivelmenteExcluida in coordenacoesOriginais)
            {
                if (cursoAlterar.Coordenacao.All(c => c.Id != coordenacaoPossivelmenteExcluida.Id))
                {
                    contexto.CorpoDocente.Remove(coordenacaoPossivelmenteExcluida);
                }
            }

            // Novas Entradas
            foreach (var novaCoordenacao in coordenacao)
            {
                if (cursoAlterar.Coordenacao.All(c => c.Id != novaCoordenacao.Id))
                {
                    cursoAlterar.Coordenacao.Add(novaCoordenacao);
                }
            }

            contexto.SaveChanges();

However, note that by deleting a Faculty in this way, you will also be deleting the original record, since the bank’s modeling is wrong. It should be created an extra associative entity that links Courses to Faculty, something like CursoAssocCorpoDocente. There goes your choice.

  • I think you typed wrong . Enty only found . Entry, but it didn’t work, it generated the same error.

  • I typed ;) I’m correcting.

  • It now generated the following error: Additional information: Unable to create a Constant value of type 'Dominio.Sbe_st_corpodocente'. Only Primitive types or enumeration types are supported in this context.

  • I think it’s the generation of Id’s list: entidade.Coordenacao.Select(e => e.Id). Try generating this list outside, before calling contexto.CorpoDocente.Where().

  • Or, better yet, put one ToList() amid Select() and Contains(). I’ll do it in response.

  • Same mistake! ... Edit: The expression is in extra parentheses!

Show 1 more comment

1

You can’t have a Manytomany relationship like this. Use EntityTypeConfiguration of lib System.Data.Entity.ModelConfiguration. And create the Map:

public class SBE_ST_CorpoDocenteMap : EntityTypeConfiguration<SBE_ST_CorpoDocente>
{
    public SBE_ST_CorpoDocenteMap()
    {
        HasMany(t => t.Cursos)
            .WithMany(t => t.Coordenacao)
            .Map(m =>
            {
                m.ToTable("TB_CURSO_COORDENADO", "schema");
                m.MapLeftKey("id_curso");
                m.MapRightKey("id_coordenado");
            });
    }
}

And plural your properties that are lists. use ICollection<Curso> Cursos

Good luck

  • Didn’t work out!!

  • What didn’t work ? still gives validation error ? Entity was able to create the mapping correctly ?

1

Because they are 2 relationships of 1 for many, it is best to associate to the list of elements of the context, using the ids of the past object.

public void Salvar(SBE_ST_Curso curso)
{
    var idsCoordenacao = curso.Coordenacao.Select(c => c.Id).ToList();
    var coordenacao = contexto.CorpoDocente.Where(cd => idsCoordenacao.Contains(cd.Id)).ToList();

    if (curso.Id > 0)
    {
        var cursoAlterar = contexto.Curso.FirstOrDefault(c => c.Id == curso.Id);
        cursoAlterar.Titulo = curso.Titulo;
        cursoAlterar.Coordenacao = coordenacao;
    }
    else
    {
         curso.Coordenacao = coordenacao;
         contexto.Curso.Add(curso);
    }

    contexto.SaveChanges();
}

Just to reinforce, in case you do not know, but it is not recommended to work with objects that come directly from Request, as parameters of actions of the controller due to security issues. For this, always use a specific model for data transfer. These models are called Viewmodel by some and are only classes without any vículo, that serve only to transfer the data.

In your case, you could have a class for this data in the form of:

public class CursoViewModel
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public List<int> Coordenacao { get; set; }

    public static CursoViewModel MapearDoCurso(SBE_ST_Curso curso)
    {
        return new CursoViewModel
        {
            Id = curso.Id,
            Nome = curso.Nome,
            Coordenacao = curso.Coordenacao.Select(c => c.Id).ToList()
        };
    }

    public void MapearParaCurso(SBE_ST_Curso curso)
    {
        curso.Id = this.Id;
        curso.Nome = this.Nome;
        curso.Coordenacao = context.CorpoDocente.Where(c => this.Coordenacao.Contains(c.Id));
    }
}

With this auxiliary mapping structure and methods, you can work safely, sending and receiving data with this class and retrieve your model in the controller action:

public ActionResult Edit(int id)
{
    var curso = contexto.Cursos.FirstOrDefault(c => c.Id == id);

    if (curso != null)
    {
        var cursoParaEditar = CursoViewModel.MapearDoCurso(curso);

        return View(cursoParaEditar);
    }
    else
    {
        ViewBag["erro"] = "Curso inválido!";
        return View();
    }
}

On the return, just do the opposite, using the method in the instance MapearParaCurso(), passing a reference to the course with the method came ID:

public ActionResult Edit(CursoViewModel cursoAlterado)
{
    if (ModelState.IsValid)
    {
        var curso = contexto.Cursos.FirstOrDefault(c => c.Id == cursoAlterado.Id);

        if (curso != null)
        {
            cursoAlterado.MapearParaCurso(curso);
            // caso não salve, utilize: contexto.Entry(curso).EntityState = EntityState.Modifyed;
            contexto.SaveChanges();
            return RedirectToAction("Index");
        }
        else
        {
            ViewBag["erro"] = "Curso inválido!";
            return View();
        }
    }
    else
    {
        ViewBag["erro"] = "Erros de validação!";
        return View();
    }
}
  • It worked... It was not very clear the concept of Viewmodel, but I think it is not up to this question clarifications. Thank you.

  • He’s in trouble again, out of the blue... Is there an explanation? A solution?

  • @Diegozanardo O ViewModel is basically a Model that is not persisted. It only serves to popular the screen and collect data from it. As the Controller can receive it, what the answer proposes is to better organize what comes from your View.

  • @Ciganomorrisonmendez, as for the error, why did it happen again? The first time I changed was normal, now today returned the error.

  • @Diegozanardo What a mistake?

  • Same as the question!

  • @Diegozanardo, did the error come back? What was done before you made it work and what was done afterwards? You use version control?

  • @rcdmk, has no version control. And I didn’t change anything, related to this.

  • Um... First thing: ALWAYS use version control. Even if it is a copy of the code that is working for another folder. But git isn’t hard to use, especially with Tortoisegit. Take a look.

  • Second, if it ever worked, it was after I made the change that I suggested?

  • So @rcdmk, I made the changes you suggested and it worked. Then I started to popular the bank, and then after a few days came back the error.

  • There is probably some problem with the database structure. This is very strange. It is likely that the error was never solved. It only stopped giving the error because you must have emptied the bank. Try it with the empty seat and try to see if it rotates. (Ps.: do yourself a favor, install the Git with the Tortoisegit or the Sourcetree. Both have in en.)

  • @rcdmk, to generate the database I use Migrations, and I have tried using another database.

  • @Diegozanardo It is not clear to me yet why your mistake. Could you create a Github account, put your project there, and share the link in the question? With the source, we can better test and identify the problem.

  • @Ciganomorrisonmendez, I believe this is: https://github.com/dgo13/Web_SBE. I believe I have removed all database and ftp data. But since it’s my first use of github, I’d like you to notify me.

  • @Diegozanardo I pulled a request for you on Github with a few changes. Just accept, download the changes back and fix what’s missing to be able to save records, because I can’t save anything.

  • @Ciganomorrisonmendez, I accidentally deleted my repository on github, I don’t know how to move it. I believe the error was to upload the photos.

  • @Diegozanardo You may have deleted it, but I still have my Fork. Only download it back: https://github.com/cigano/Web_SBE

Show 13 more comments

1

I managed to come up with a solution.

I’m not sure why it works, or if it makes sense to make it work.

Solution

If before I upgrade I give one Clear() in the list, it no longer generates the error.

That is to say:

var idsCoordenacao = entidade.Coordenacao.Select(c => c.Id).ToList();
var coordenacao = contexto.CorpoDocente.Where(cd => idsCoordenacao.Contains(cd.Id)).ToList();
if (entidade.Id > 0)
{
    var cursoAlterar = contexto.Curso.First(x => x.Id == entidade.Id);
    cursoAlterar.Titulo = entidade.Titulo;
    cursoAlterar.Coordenacao.Clear(); //ADICIONAR ESSA LINHA.
    cursoAlterar.Coordenacao = coordenacao;
    contexto.Entry(cursoAlterar).State = EntityState.Modified;
    contexto.SaveChanges();
}
  • Actually this only makes you delete old records and add new ones. Works, but your application will delete followed by insertion every time.

Browser other questions tagged

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