Entity Framework foreign key

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

Interaction layer 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 registry 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 it 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 can not   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 IntelliTrace:

  

Exception: Caught: "Violation of PRIMARY KEY constraint   'PK_dbo.SBE_ST_CourseSBE_ST_DoctorCode'. Can not insert duplicate key   in object 'dbo.SBE_ST_CourseSBE_ST_Doctor'. 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_CourseSBE_ST_CopyCenter'. Can not insert   duplicate key in object 'dbo.SBE_ST_CourseSBE_ST_Doctor_body'. The   duplicate key value is (8, 1). The statement has been terminated. "   Time: 11/08/2014 15:28:19 Thread: Worker Thread [3508]

I did a GAMBIARRA to solve this problem. (ALMOST GAMBIARRA)

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();
            }
}
    
asked by anonymous 11.08.2014 / 20:00

4 answers

6

You are not making the context correctly observe your changes. Context always thinks that the entity coming from the screen is new because it did not load the existing record before. The right thing would be 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 Coordenacao again. Although there are apparently no changes, the Entity Framework has a logic that observes the variables. Since there was a change, and you filled Coordenacao from the screen, it tries to insert the records as if they were new. The right thing would be to populate the data with the variable from the context , not from the screen.

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

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

The end result looks more or less 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 and several fixes 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 Teaching 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 are also deleting the original record, since the database modeling is wrong. An additional associative entity should be created linking Courses to Teaching Bodies, something like CursoAssocCorpoDocente . There goes your choices.

    
11.08.2014 / 22:56
1

Because they are 2 relationships from 1 to many, it is best to associate them to the list of context elements, using the ids of the last 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 if you do not know, but it is not recommended to work with objects that come directly from the Request, as parameters of the controller actions due to security issues. To do this, always use a specific model for data transfer. These models are called ViewModel by some and are just classes without some 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 structure and ancillary mapping methods, you can safely work by sending and receiving data with this class and retrieving 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 return, just do the opposite, using the method in the MapearParaCurso() instance, passing a reference to the course with the ID that came from the method:

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();
    }
}
    
12.08.2014 / 22:53
1

You can not have a ManyToMany relationship that way. Use EntityTypeConfiguration of System.Data.Entity.ModelConfiguration lib. And create the Mapping:

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 put in the plural your properties that are list. use ICollection<Curso> Cursos

Good luck

    
19.08.2014 / 17:27
1

I came up with a solution.

I'm not sure why this works, or if it makes sense to do it right.

Solution

If before updating I give Clear() to the list, it no longer generates the error.

That is:

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();
}
    
29.09.2014 / 16:25