Como faço para impedir que o Entity Framework tente salvar / inserir objetos filho?

100

Quando salvo uma entidade com estrutura de entidade, naturalmente assumi que só tentaria salvar a entidade especificada. No entanto, ele também está tentando salvar as entidades filhas dessa entidade. Isso está causando todos os tipos de problemas de integridade. Como faço para forçar o EF a salvar apenas a entidade que desejo salvar e, portanto, ignorar todos os objetos filho?

Se eu definir manualmente as propriedades como nulas, recebo um erro "A operação falhou: o relacionamento não pôde ser alterado porque uma ou mais propriedades de chave estrangeira não podem ser anuladas." Isso é extremamente contraproducente, pois eu defini o objeto filho como nulo especificamente para que o EF o deixasse sozinho.

Por que não quero salvar / inserir os objetos filho?

Uma vez que isso está sendo discutido continuamente nos comentários, darei algumas justificativas de porque quero que meus objetos filhos sejam deixados sozinhos.

No aplicativo que estou construindo, o modelo de objeto EF não está sendo carregado do banco de dados, mas usado como objetos de dados que estou preenchendo ao analisar um arquivo simples. No caso dos objetos filho, muitos deles se referem a tabelas de pesquisa que definem várias propriedades da tabela pai. Por exemplo, a localização geográfica da entidade primária.

Já que eu mesmo povoei esses objetos, EF assume que esses são novos objetos e precisam ser inseridos junto com o objeto pai. No entanto, essas definições já existem e não quero criar duplicatas no banco de dados. Eu só uso o objeto EF para fazer uma pesquisa e preencher a chave estrangeira na minha entidade de tabela principal.

Mesmo com os objetos filhos que são dados reais, eu preciso salvar o pai primeiro e obter uma chave primária ou o EF parece bagunçar as coisas. Espero que isso dê alguma explicação.

Mark Micallef
fonte
Pelo que eu sei, você terá que anular os objetos filhos.
Johan
Olá, Johan. Não funciona. Ele lança erros se eu anular a coleção. Dependendo de como eu faço isso, ele reclama que as chaves são nulas ou que a coleção I foi modificada. Obviamente, essas coisas são verdadeiras, mas eu fiz isso de propósito para deixar de lado os objetos que não deveria tocar.
Mark Micallef
Eufórico, isso é completamente inútil.
Mark Micallef
@Euphoric Mesmo quando não muda os objetos filhos, EF ainda tenta inseri-los por padrão e não os ignora ou atualiza.
Johan
O que realmente me irrita é que, se eu sair do meu caminho para realmente anular esses objetos, ele reclama, em vez de perceber que quero apenas deixá-los em paz. Uma vez que esses objetos filho são todos opcionais (anuláveis ​​no banco de dados), há alguma maneira de forçar EF a esquecer que eu tinha esses objetos? ou seja, limpar seu contexto ou cache de alguma forma?
Mark Micallef

Respostas:

55

Pelo que eu sei, você tem duas opções.

Opção 1)

Anular todos os objetos filho, isso irá garantir que EF não adicione nada. Ele também não excluirá nada do seu banco de dados.

Opção 2)

Defina os objetos filho como separados do contexto usando o código a seguir

 context.Entry(yourObject).State = EntityState.Detached

Observe que você não pode desanexar um List/ Collection. Você terá que fazer um loop em sua lista e destacar cada item de sua lista assim

foreach (var item in properties)
{
     db.Entry(item).State = EntityState.Detached;
}
João
fonte
Olá, Johan, Eu tentei detectar uma das coleções e gerou o seguinte erro: O tipo de entidade HashSet`1 não faz parte do modelo para o contexto atual.
Mark Micallef
@MarkyMark Não destaque a coleção. Você terá que percorrer a coleção e desanexá-la objeto por objeto (atualizarei minha resposta agora).
Johan
11
Infelizmente, mesmo com uma lista de loops para detectar tudo, EF parece ainda estar tentando inserir em algumas das tabelas relacionadas. Neste ponto, estou pronto para retirar o EF e voltar ao SQL, que pelo menos se comporta de maneira sensata. Que dor.
Mark Micallef de
2
Posso usar context.Entry (yourObject) .State antes mesmo de adicioná-lo?
Thomas Klammer
1
@ mirind4 Eu não usei o estado. Se eu inserir um objeto, certifico-me de que todos os filhos são nulos. Na atualização, recebo o objeto primeiro sem os filhos.
Thomas Klammer
41

Resumindo: use a chave estrangeira e isso salvará o seu dia.

Suponha que você tenha uma entidade Escola e uma entidade Cidade , e esse é um relacionamento muitos para um em que uma cidade tem muitas escolas e uma Escola pertence a uma cidade. E suponha que as cidades já existam na tabela de pesquisa, então você NÃO deseja que elas sejam inseridas novamente ao inserir uma nova escola.

Inicialmente, você pode definir suas entidades assim:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public City City { get; set; }
}

E você pode fazer a inserção School assim (suponha que você já tenha a propriedade City atribuída ao newItem ):

public School Insert(School newItem)
{
    using (var context = new DatabaseContext())
    {
        context.Set<School>().Add(newItem);
        // use the following statement so that City won't be inserted
        context.Entry(newItem.City).State = EntityState.Unchanged;
        context.SaveChanges();
        return newItem;
    }
}

A abordagem acima pode funcionar perfeitamente neste caso, entretanto, eu prefiro a abordagem de chave estrangeira , que para mim é mais clara e flexível. Veja a solução atualizada abaixo:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [ForeignKey("City_Id")]
    public City City { get; set; }

    [Required]
    public int City_Id { get; set; }
}

Desta forma, você define explicitamente que a escola tem uma chave estrangeira City_Id e se refere à entidade City . Então, quando se trata de inserção de Escola , você pode fazer:

    public School Insert(School newItem, int cityId)
    {
        if(cityId <= 0)
        {
            throw new Exception("City ID no provided");
        }

        newItem.City = null;
        newItem.City_Id = cityId;

        using (var context = new DatabaseContext())
        {
            context.Set<School>().Add(newItem);
            context.SaveChanges();
            return newItem;
        }
    }

Nesse caso, você especifica explicitamente o City_Id do novo registro e remove a cidade do gráfico para que EF não se preocupe em adicioná-lo ao contexto junto com Escola .

Embora à primeira impressão a abordagem da chave estrangeira pareça mais complicada, mas acredite em mim, essa mentalidade vai lhe poupar muito tempo quando se trata de inserir um relacionamento muitos para muitos (imaginando que você tem um relacionamento Escola e Aluno, e o Aluno tem uma propriedade City) e assim por diante.

Espero que este seja útil para você.

Lenglei
fonte
Ótima resposta, me ajudou muito! Mas não seria melhor declarar o valor mínimo de 1 para City_Id usando o [Range(1, int.MaxValue)]atributo?
Dan Rayson
Como um sonho!! Obrigado a um milhão!
CJH
Isso removeria o valor do objeto Escola no chamador. O SaveChanges recarrega o valor das propriedades de navegação nulas? Caso contrário, o chamador deve recarregar o objeto City após chamar o método Insert () se ele precisar dessa informação. Este é o padrão que tenho usado com frequência, mas ainda estou aberto a melhores padrões, se alguém tiver um bom.
Desequilibrado
1
Isso foi realmente útil para mim. EntityState.Unchaged é exatamente o que eu precisava para atribuir um objeto que representa uma chave estrangeira da tabela de pesquisa em um gráfico de objeto grande que estava salvando como uma única transação. EF Core lança uma mensagem de erro menos intuitiva IMO. Eu armazenei minhas tabelas de pesquisa que raramente mudam por motivos de desempenho. Você está supondo que, como o PK do objeto da tabela de pesquisa é o mesmo que o que já está no banco de dados, ele sabe que está inalterado e apenas atribuir o FK ao item existente. Em vez de tentar inserir o objeto da tabela de pesquisa como novo.
tnk479
Nenhuma combinação de anular ou definir o estado como inalterado está funcionando para mim. EF insiste que precisa inserir um novo registro filho quando desejo apenas atualizar um campo escalar no pai.
BobRz
21

Se você deseja apenas armazenar alterações em um objeto pai e evitar o armazenamento de alterações em qualquer um de seus objetos filho , por que não fazer o seguinte:

using (var ctx = new MyContext())
{
    ctx.Parents.Attach(parent);
    ctx.Entry(parent).State = EntityState.Added;  // or EntityState.Modified
    ctx.SaveChanges();
}

A primeira linha anexa o objeto pai e todo o gráfico de seus objetos filhos dependentes ao contexto no Unchangedestado.

A segunda linha muda o estado apenas para o objeto pai, deixando seus filhos no Unchangedestado.

Observe que eu uso um contexto recém-criado, portanto, isso evita salvar quaisquer outras alterações no banco de dados.

keykey76
fonte
2
Essa resposta tem a vantagem de que, se alguém vier depois e adicionar um objeto filho, isso não quebrará o código existente. Esta é uma solução "opcional" em que as outras exigem que você exclua explicitamente os objetos filho.
Jim
Isso não funciona mais no núcleo. "Anexar: Anexa todas as entidades alcançáveis, exceto onde uma entidade alcançável tem uma chave gerada pela loja e nenhum valor de chave é atribuído; eles serão marcados como adicionados." Se as crianças também forem novas, elas serão adicionadas.
mmix
14

Uma das soluções sugeridas é atribuir a propriedade de navegação do mesmo contexto de banco de dados. Nesta solução, a propriedade de navegação atribuída de fora do contexto do banco de dados seria substituída. Por favor, veja o exemplo a seguir para ilustração.

class Company{
    public int Id{get;set;}
    public Virtual Department department{get; set;}
}
class Department{
    public int Id{get; set;}
    public String Name{get; set;}
}

Salvando no banco de dados:

 Company company = new Company();
 company.department = new Department(){Id = 45}; 
 //an Department object with Id = 45 exists in database.    

 using(CompanyContext db = new CompanyContext()){
      Department department = db.Departments.Find(company.department.Id);
      company.department = department;
      db.Companies.Add(company);
      db.SaveChanges();
  }

A Microsoft considera isso um recurso, mas acho isso irritante. Se o objeto departamento associado ao objeto da empresa possui um Id que já existe no banco de dados, então por que EF não associa apenas o objeto da empresa ao objeto do banco de dados? Por que devemos cuidar da associação sozinhos? Cuidar da propriedade de navegação durante a adição de um novo objeto é algo como mover as operações do banco de dados de SQL para C #, complicado para os desenvolvedores.

Bikash Bishwokarma
fonte
Concordo 100%, é ridículo que EF tente criar um novo registro quando o ID é fornecido. Se houvesse uma maneira de preencher as opções da lista de seleção com o objeto real, estaríamos no negócio.
T3.0
10

Primeiro você precisa saber que existem duas maneiras de atualizar a entidade no EF.

  • Objetos anexados

Quando você altera o relacionamento dos objetos anexados ao contexto do objeto usando um dos métodos descritos acima, o Entity Framework precisa manter as chaves estrangeiras, referências e coleções em sincronia.

  • Objetos desconectados

Se você estiver trabalhando com objetos desconectados, deverá gerenciar manualmente a sincronização.

No aplicativo que estou construindo, o modelo de objeto EF não está sendo carregado do banco de dados, mas usado como objetos de dados que estou preenchendo ao analisar um arquivo simples.

Isso significa que você está trabalhando com um objeto desconectado, mas não está claro se você está usando uma associação independente ou uma associação de chave estrangeira .

  • Adicionar

    Ao adicionar uma nova entidade com um objeto filho existente (objeto que existe no banco de dados), se o objeto filho não for rastreado por EF, o objeto filho será reinserido. A menos que você anexe manualmente o objeto filho primeiro.

      db.Entity(entity.ChildObject).State = EntityState.Modified;
      db.Entity(entity).State = EntityState.Added;
  • Atualizar

    Você pode apenas marcar a entidade como modificada, então todas as propriedades escalares serão atualizadas e as propriedades de navegação serão simplesmente ignoradas.

      db.Entity(entity).State = EntityState.Modified;

Gráfico Diff

Se você deseja simplificar o código ao trabalhar com um objeto desconectado, você pode tentar criar uma biblioteca de diff em gráfico .

Aqui está a introdução, Apresentando GraphDiff para Entity Framework Code First - permitindo atualizações automatizadas de um gráfico de entidades desanexadas .

Código de amostra

  • Insira a entidade se ela não existir, caso contrário, atualize.

      db.UpdateGraph(entity);
  • Insira a entidade se ela não existir, caso contrário, atualize E insira o objeto filho se ele não existir, caso contrário atualize.

      db.UpdateGraph(entity, map => map.OwnedEntity(x => x.ChildObject));
Yuliam Chandra
fonte
3

A melhor maneira de fazer isso é substituindo a função SaveChanges em seu texto de dados.

    public override int SaveChanges()
    {
        var added = this.ChangeTracker.Entries().Where(e => e.State == System.Data.EntityState.Added);

        // Do your thing, like changing the state to detached
        return base.SaveChanges();
    }
Wouter Schut
fonte
2

Estou com o mesmo problema quando tento salvar perfil, já tabela saudação e novidade para criar perfil. Quando eu insiro o perfil, ele também insere na saudação. Então eu tentei assim antes de savechanges ().

db.Entry (Profile.Salutation) .State = EntityState.Unchanged;

ZweHtatNaing
fonte
1

Isso funcionou para mim:

// temporarily 'detach' the child entity/collection to have EF not attempting to handle them
var temp = entity.ChildCollection;
entity.ChildCollection = new HashSet<collectionType>();

.... do other stuff

context.SaveChanges();

entity.ChildCollection = temp;
Klaus
fonte
0

O que fizemos é antes de adicionar o pai ao dbset, desconectar as coleções filho do pai, certificando-se de enviar as coleções existentes para outras variáveis ​​para permitir trabalhar com elas mais tarde, e então substituir as coleções filho atuais por novas coleções vazias. Definir as coleções filho como null / nothing pareceu falhar para nós. Depois de fazer isso, adicione o pai ao dbset. Dessa forma, os filhos não são adicionados até que você queira.

Shaggie
fonte
0

Eu sei que é um post antigo, no entanto, se você estiver usando a abordagem de código primeiro, você pode obter o resultado desejado usando o seguinte código em seu arquivo de mapeamento.

Ignore(parentObject => parentObject.ChildObjectOrCollection);

Isso basicamente dirá ao EF para excluir a propriedade "ChildObjectOrCollection" do modelo para que não seja mapeada para o banco de dados.

Syed dinamarquês
fonte
Em que contexto "Ignorar" está sendo usado? Não parece existir no contexto presumido.
T3.0
0

Eu tive um desafio semelhante ao usar o Entity Framework Core 3.1.0, minha lógica de repositório é bastante genérica.

Isso funcionou para mim:

builder.Entity<ChildEntity>().HasOne(c => c.ParentEntity).WithMany(l =>
       l.ChildEntity).HasForeignKey("ParentEntityId");

Observe que "ParentEntityId" é o nome da coluna da chave estrangeira na entidade filha. Eu adicionei a linha de código mencionada acima neste método:

protected override void OnModelCreating(ModelBuilder builder)...
Manelisi Nodada
fonte