Pontos de dados

Por que o Entity Framework reinsere objetos existentes em meu banco de dados?

Julie Lerman

Baixar o código de exemplo

Julie LermanQuando chegou a hora de ter uma ideia para essa coluna, três pessoas me perguntaram, via twitter e email, por que o Entity Framework reinsere objetos existentes em seus banco de dados? Decidir a respeito do que escrever ficou fácil.

Por causa de seus recursos de gerenciamento de estado, quando o Entity Framework trabalha com gráficos, o comportamento do estado de sua entidade nem sempre está alinhado com suas ideias de como ele deveria funcionar. Vamos examinar um exemplo típico.

Vamos supor que eu tenha duas classes, Screencast e Topic, em que a cada Screencast é atribuída uma única Topic, como mostrado na Figura 1.

Figura 1 As classes Screencast e Topic

public class Screencast
{
  public int Id { get; set; }
  public string Title { get; set; }
  public string Description { get; set; }
  public Topic Topic { get; set; }
  public int TopicId { get; set; }
}
public class Topic
{
  public int Id { get; set; }
  public string Name { get; set; }
}

Se eu tivesse de recuperar uma lista de Topics, atribuir uma delas a uma nova Screencast e, em seguida, salvar—com o conjunto inteiro de operações contidas em um único contexto—não haveria nenhum problema, como o exemplo a seguir mostra:

using (var context = new ScreencastContext())
{
  var dataTopic = 
    context.Topics.FirstOrDefault(t=>t.Name.Contains("Data"));
  context.Screencasts.Add(new Screencast
                               {
                                 Title="EF101",
                                 Description = "Entity Framework 101",
                                 Topic = dataTopic
                               });
  context.SaveChanges();
}

Uma única Screencast seria inserida no banco de dados com a chave estrangeira apropriada da Topic escolhida.

Quando você está trabalhando em aplicativos cliente ou executando essas etapas em uma única Unidade de Trabalho em que o contexto está controlando todas as atividades, esse é o comportamento que provavelmente você esperaria. No entanto, se estiver trabalhando com dados desconectados, o comportamento será bastante diferente. Isso tem surpreendido muitos desenvolvedores.

Gráficos adicionados em cenários desconectados

Um padrão comum que uso para manusear listas de referências é utilizar um contexto diferente, que não estaria mais no escopo quando você salvasse suas modificações de usuário. Essa situação é comum para aplicativos e serviços Web, mas também pode acontecer em um aplicativo do lado do cliente. Eis um exemplo que usa um repositório para dados de referência com o seguinte método GetTopicList para recuperar uma lista de Topics:

public class SimpleRepository
{
  public List<Topic> GetTopicList()
  {
    using (var context = new ScreencastContext())
    {
      return context.Topics.ToList();
    }
  }
 ...
}

Você deve então apresentar os Topics em uma lista em um formulário do Windows Presentation Foundation (WPF) que permite aos usuários criar uma nova Screencast, como a mostrada na Figura 2.

A Windows Presentation Foundation Form for Entering New Screencasts
Figura 2 Um formulário do Windows Presentation Foundation para inserir novas screencasts

Em um aplicativo cliente, como o formulário do WPF na Figura 2, você deve então definir o item selecionado da lista suspensa para a propriedade Topic da Screencast com um código como este:

private void Save_Click(object sender, RoutedEventArgs e)
{
  repo.SaveNewScreencast(new Screencast
                {
                  Title = titleTextBox.Text,
                  Description = descriptionTextBox.Text,
                  Topic = topicListBox.SelectedItem as Topic
                });
}

Agora a variável Screencast é um gráfico que contém a nova Screencast e a instância Topic. Passar essa variável para o método SaveNewScreencast do repositório adiciona o gráfico a uma nova instância de contexto e, em seguida, salva-o no banco de dados, desta forma:

public void SaveNewScreencast(Screencast screencast)
{
  using (var context = new ScreencastContext())
  {
    context.Screencasts.Add(screencast);
    context.SaveChanges();
  }
}

Criar o perfil de atividades do banco de dados revela que não só a Screencast foi inserida, mas, antes disso, uma nova linha foi inserida para o tópico Data Dev na tabela Topics, mesmo que esse tópico já existisse:

    exec sp_executesql N'insert [dbo].[Topics]([Name])
    values (@0)
    select [Id]
    from [dbo].[Topics]
    where @@ROWCOUNT > 0 and [Id] = 
      scope_identity()',N'@0 nvarchar(max) ',@0=N'Data Dev'

Esse comportamento tem confundido muitos desenvolvedores. A razão disso acontecer é que ao usar o método DbSet.Add (ou seja, Screencasts.Add), não apenas o estado da entidade raiz é marcado como “Adicionado”, mas tudo no gráfico de que o contexto não estava ciente anteriormente é marcado também como Adicionado. Embora o desenvolvedor possa estar ciente de que o Topic tem um valor Id existente, o Entity Framework honra seu EntityState (Adicionado) e cria um comando de banco de dados Insert para o Topic, independentemente do Id existente.

Embora muitos desenvolvedores possam prever esse comportamento, há muitos que não o fazem. E nesse caso, se você não estiver criando o perfil de atividades do banco de dados, poderá não perceber que está ocorrendo até da próxima vez que você (ou um usuário) descobrir itens duplicados na lista Topics.

Observação: se não estiver familiarizado com como o EF insere novas linhas, poderá estar curioso a respeito do Select no meio do SQL anterior. É para garantir que o EF retornará o valor Id da recém-criada Screencast para que possa definir o valor na instância Screencast.

Não apenas um problema ao adicionar gráficos inteiros

Vamos examinar outro cenário onde esse problema pode ocorrer.

E se, em vez de passar um gráfico para o repositório, o método do repositório solicitar a nova Screencast e o Topic selecionado como parâmetros? Em vez de adicionar um novo gráfico, adiciona a entidade Screencast e então define sua propriedade de navegação Topic:

public void SaveNewScreencastWithTopic(Screencast screencast,
  Topic topic)
{
  using (var context = new ScreencastContext())
  {
    context.Screencasts.Add(screencast);
    screencast.Topic = topic;
    context.SaveChanges();
  }
}

Nesse caso, o comportamento SaveChanges é o mesmo que com o gráfico Adicionado. Você pode estar familiarizado com a utilização do método EF Attach para anexar uma entidade não controlada a um contexto. Nesse caso, o estado da entidade começa como Não alterado. Mas aqui, onde estamos atribuindo o Topic à instância Screencast, não ao contexto, o EF considera essa como uma entidade não reconhecida e seu comportamento padrão para entidades não reconhecidas sem estado é marcá-las como Adicionadas. Novamente, o Topic será inserido no banco de dados quando SaveChanges for chamado.

É possível controlar o estado, mas isso requer um entendimento mais aprofundado do comportamento do EF. Por exemplo, se tivesse de anexar o Topic diretamente ao contexto, em vez de à Screencast Adicionada, seu EntityState começaria como Não Alterado. Defini-lo como screencast.Topic não alteraria o estado, pois o contexto já está ciente do Topic. Eis aqui o código modificado que demonstra essa lógica:

using (var context = new ScreencastContext())
{
  context.Screencasts.Add(screencast);
  context.Topics.Attach(topic);
  screencast.Topic = topic;
  context.SaveChanges();
}

Como alternativa, em lugar de context.Topics.Attach(tópico), você poderia definir o estado do Topic antes ou depois do fato, definindo explicitamente seu estado como Não alterado.

context.Entry(topic).State = EntityState.Unchanged

Chamar esse código antes que o contexto esteja ciente do Topic fará com que o contexto anexe o Topic e, em seguida, defina o estado.

Embora sejam padrões corretos de lidar com esse problema, eles não são óbvios. A menos que tenha aprendido sobre esse comportamento e os padrões de código necessários antecipadamente, você está mais apto a escrever código que parece lógico, então chegar a esse problema e só nesse ponto começar a tentar descobrir o que está acontecendo.

Evite o sofrimento e use aquela chave estrangeira

Mas há uma maneira muito mais simples de evitar este estado de confusão (desculpa o trocadilho), que é aproveitar as propriedades da chave estrangeira.

Ao invés de definir a propriedade de navegação e ter de se preocupar com o estado do Topic, simplesmente defina a propriedade TopicId, porque você tem acesso a esse valor na instância Topic. Com muita frequência eu me encontro sugerindo isso aos desenvolvedores. Mesmo no Twitter vejo a pergunta: “Por que o EF insere dados que já existem?” e com frequência acerto na resposta: “Alguma chance de que esteja definindo uma propriedade de navegação em uma nova entidade em vez de uma chave estrangeira? J”

Vamos rever o método Save_Click no formulário do WPF e definir a propriedade TopicId em vez da propriedade de navegação Topic:

repo.SaveNewScreencast(new Screencast
               {
                 Title = titleTextBox.Text,
                 Description = descriptionTextBox.Text,
                 TopicId = (int)topicListBox.SelectedValue)
               });

A Screencast que é enviada ao método do repositório agora é apenas a entidade única, não um gráfico. O Entity Framework pode usar a propriedade da chave estrangeira para definir diretamente o TopicId da tabela. Então, é simples (e mais rápido) para o EF criar um método de inserção para a entidade Screencast incluindo o TopicId (no meu caso, o valor 2):

    exec sp_executesql N'insert [dbo].[Screencasts]([Title], [Description], [TopicId])
    values (@0, @1, @2)
    select [Id]
    from [dbo].[Screencasts]
    where @@ROWCOUNT > 0 and [Id] = scope_identity()',
    N'@0 nvarchar(max) ,@1 nvarchar(max) ,@2 int',
      @0=N'EFFK101',@1=N'Using Foreign Keys When Setting Navigations',@2=2

Se você desejasse manter a lógica de construção no repositório e não forçar o desenvolvedor da interface do usuário a se preocupar com a definição da chave estrangeira, poderia especificar uma Screencast e o Id do Topic como parâmetros do método do repositório e definir o valor no método como a seguir:

public void SaveNewScreencastWithTopicId(Screencast screencast, 
  int topicId)
{
  using (var context = new ScreencastContext())
  {
    screencast.TopicId = topicId;
    context.Screencasts.Add(screencast);
    context.SaveChanges();
  }
}

Nas nossas preocupações sem fim com o que poderia acontecer, precisamos considerar a possibilidade de que um desenvolvedor possa definir a propriedade de navegação Topic de qualquer forma. Em outras palavras, mesmo que queiramos usar a chave estrangeira para evitar o problema do EntityState, e se a instância Topic fosse parte do gráfico, como nesse código alternativo para o botão Save_Click:

repo.SaveNewScreencastWithTopicId(new Screencast
    {
      Title = titleTextBox.Text,
      Description = descriptionTextBox.Text,
      Topic=topicListBox.SelectedItem as Topic
    },
  (int) topicListBox.SelectedValue);

Infelizmente, isso nos leva de volta ao problema original: O EF vê a entidade Topic no gráfico e a adiciona ao contexto junto com Screencast—mesmo que a propriedade Screencast.TopicId tenha sido definida. E novamente, o EntityState da instância Topic cria confusão: O EF irá inserir um novo Topic e usar o valor desse Id da nova linha ao inserir a Screencast.

A maneira mais segura de evitar isso é definir a propriedade Topic como nula ao inserir o valor da chave estrangeira. Se o método de repositório for usado por outras interfaces do usuário em que você não possa ter certeza que apenas Topics existentes serão usados, talvez você queira até fornecer a possibilidade de um Topic recém-criado ser passado. A Figura 3 mostra o método de repositório modificado novamente para executar essa tarefa.

Figura 3 Método de repositório projetado para proteger contra inserção acidental de propriedade de navegação no banco de dados

public void SaveNewScreencastWithTopicId(Screencast screencast, 
  int topicId)
{
  if (topicId > 0)
  {
    screencast.Topic = null;
    screencast.TopicId = topicId;
  }
  using (var context = new ScreencastContext())
  {
    context.Screencasts.Add(screencast);
    context.SaveChanges();
  }
}

Agora tenho um método de repositório que cobre uma variedade de cenários, fornecendo até mesmo lógica para acomodar novos Topics sendo passados para o método.

O código gerado por scaffolding do ASP.NET MVC 4 evita o problema

Embora esse seja um problema inerente aos aplicativos desconectados, vale a pena apontar que se você estiver usando o scaffolding do ASP.NET MVC 4 para gerar exibições e controladores MVC, você evitará o problema de entidades de navegação duplicadas sendo inseridas no banco de dados.

Considerando o relacionamento de um para muitos entre a Screencast e o Topic, assim como a propriedade TopicId que é a chave estrangeira no tipo Screencast, o scaffolding gera o seguinte método Create no controlador:

public ActionResult Create()
{
  ViewBag.TopicId = new SelectList(db.Topics, "Id", "Name");
  return View();
}

Foi criada uma lista de Topics para passar para a exibição e chamada essa lista de TopicId, o mesmo nome da propriedade da chave estrangeira.

O scaffolding também incluiu a seguinte Lista na marcação da exibição Create:

    <div class="editor-field">
      @Html.DropDownList("TopicId", String.Empty)
      @Html.ValidationMessageFor(model => model.TopicId)
    </div>

Quando a exibição retorna, o HttpRequest.Form inclui um valor de cadeia de caracteres de consulta chamado TopicId que vem da propriedade ViewBag. O valor de TopicId é aquele do item selecionado da DropDownList. Como o nome da cadeia de caracteres de consulta corresponde ao nome da propriedade Screencast, a associação de modelo do ASP.NET MVC usa o valor da propriedade TopicId da instância Screencast que ele cria para o parâmetro do método, como pode ser visto na Figura 4.

The New Screencast Gets Its TopicId from the Matching HttpRequest Query-String Value
Figura 4 A nova Screencast obtém seu TopicId do valor da cadeia de caracteres de consulta HttpRequest correspondente

Para verificar isso, você poderia alterar as variáveis TopicId do controlador para alguma outra coisa, como TopicIdX, e fazer a mesma alteração na cadeia de caracteres “TopicId” no @Html.DropDownList da exibição; o valor da cadeia de caracteres de consulta (agora TopicIdX) seria ignorado e screencast.TopicId seria 0.

Não há nenhuma instância Topic sendo passada de volta pelo pipeline. Assim, o ASP.NET MVC depende da propriedade da chave estrangeira s por padrão e evita o problema específico da reinserção de um Topic duplicado existente no banco de dados.

Não é você! Gráficos desconectados são complicados

Embora a equipe do EF tenha feito muito para tornar o trabalho com dados desconectados mais fácil de uma versão do EF para a próxima, ainda é um problema que assusta muitos desenvolvedores que não estão bem familiarizados com o comportamento esperado do EF. Em nosso livro, “Programming Entity Framework: DbContext” (O’Reilly Media, 2012), Rowan Miller e eu dedicamos um capítulo inteiro ao trabalho com entidades desconectadas e gráficos. E ao criar um curso recente da Pluralsight, adicionei 25 minutos não planejados focalizados na complexidade de gráficos desconectados em repositórios.

É muito conveniente trabalhar com gráficos ao consultar e interagir com dados, mas ao se tratar de construir relacionamentos com dados existentes, as chaves estrangeiras são suas amigas. Examine a minha coluna de janeiro de 2012, “Como sobreviver sem chaves estrangeiras” (msdn.microsoft.com/magazine/hh708747), que é também a respeito de algumas das armadilhas da codificação sem chaves estrangeiras.

Em uma coluna futura continuarei minha busca para minimizar alguns dos problemas que os desenvolvedores encontram ao trabalhar com gráficos em cenários desconectados. Essa coluna, que será a Parte 2 desse tópico, se concentrará no controle do EntityState em relacionamentos muitos para muitos e coleções de navegação.

Julie Lerman é uma Microsoft MVP, mentora e consultora do .NET, que reside nas colinas de Vermont. Você pode encontrá-la fazendo apresentações sobre acesso a dados e outros tópicos do Microsoft .NET em grupos de usuários e conferências em todo o mundo. Seu blog está em thedatafarm.com/blog e ela é autora do livro “Programming Entity Framework” (2010), além das edições Code First (2011) e DbContext (2012), todos da O’Reilly Media. Siga-a no Twitter, em twitter.com/julielerman.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Diego Vega (Microsoft)