Este artigo foi traduzido por máquina.

Pontos de dados

Como sobreviver sem chaves estrangeiras

Julie Lerman

Baixar o exemplo de código

Julie Lerman
Este mês eu estou escrevendo sobre um problema que eu encontrei-me ajudando as pessoas com freqüência de tarde: problemas com relacionados classes definidas no código primeiro e, em seguida, na maioria dos casos, usado na estrutura Model-View-Controller (MVC). Os problemas que têm vindo a registar os desenvolvedores não são específicos para código primeiro. Eles são o resultado do comportamento de entidade Framework (EF) subjacente e, de facto, comum à maioria dos mappers objeto-relacionais (ORMs). Mas parece que o problema é pavimentar porque os desenvolvedores estão chegando ao primeiro código com certas expectativas. MVC é-lhes causando dor devido à sua natureza altamente desconectada.

Em vez de apenas mostrando-lhe o código adequado, vou usar esta coluna para ajudá-lo a compreender o comportamento EF assim que você pode aplicar esse conhecimento para os vários cenários que você pode encontrar quando projetar suas classes ou escrever seus dados acessar código com EF APIs.

Primeira Convenção de código é capaz de detectar e inferir corretamente vários relacionamentos com diferentes combinações de propriedades em suas classes. Neste exemplo, que eu estou escrevendo como as folhas estão transformando cores espetaculares sobre árvores perto da minha casa em Vermont, usarei a árvore e folha como meus classes relacionadas. Para uma relação um-para-muitos, a maneira mais simples, você poderia descrever que em suas classes e ter código primeiro reconhecer sua intenção é ter uma propriedade de navegação na classe de árvore que representa algum tipo de coleção de tipos de folha. A classe folha precisa sem propriedades apontando para árvore. Figura 1 mostra as classes de árvore e folha.

Figura 1 árvore relacionado e Classes de folha

public class Tree
{
  public Tree()
  {
    Leaves = new List<Leaf>();
  }
  public int TreeId { get; set; }
  public string Type { get; set; }
  public double Lat { get; set; }
  public double Long { get; set; }
  public string Notes { get; set; }
  public ICollection<Leaf> Leaves { get; set; }
}
public class Leaf
{
  public int LeafId { get; set; }
  public DateTime FellFromTreeDate { get; set; }
  public string FellFromTreeColor { get; set; }
}

Por Convenção, primeiro código vai saber que é necessária uma chave estrangeira no banco de dados da tabela da folha. Ele irá supor um nome de campo de chave externa para ser "Tree_TreeId", e com esta informação fornecida nos metadados criado pelo primeiro código em tempo de execução, EF vai entender como funcionam as consultas e atualizações usando essa chave estrangeira. EF aproveita esse comportamento, contando com o mesmo processo que usa com "associações independentes" — o único tipo de associação que nós poderíamos usar antes para a Microsoft.NET Framework 4 — que não requerem uma propriedade de chave estrangeira na classe dependente.

Esta é uma maneira agradável, limpa para definir as classes quando você está confiante de que você não têm necessidade nunca navegar de uma folha de volta para sua árvore em seu aplicativo. No entanto, sem acesso direto para a chave estrangeira, você precisará ser diligente extra durante a codificação.

Criação de novos tipos dependentes sem chave estrangeira ou propriedades de navegação

Embora você possa facilmente usar essas classes para exibir uma árvore e suas folhas no seu ASP.NET MVC aplicativo e editar folhas, os desenvolvedores muitas vezes encontram problemas criando novas folhas em um aplicativo MVC normalmente arquitetado. Eu usei o modelo do pacote MVCScaffolding NuGet (mvcscaffolding.codeplex.com) para permitir que Visual Studio auto­forma construir meu controladores, exibições e repositórios simples, selecionando "MvcScaffolding: Controlador com ação de leitura/gravação e modos de exibição, usando repositórios. Observe que, porque não há nenhuma propriedade de chave estrangeira na classe Leaf, os modelos de andaimes não vai reconhecer a relação um-para-muitos. Eu fiz algumas pequenas alterações para os modos de exibição e controladores para permitir que um usuário navegar de uma árvore para suas folhas, que você pode ver no download de exemplo.

A ação de postback de criar para folha leva a folha retornada do vista criar e informa o repositório para adicioná-lo e, em seguida, salvá-lo, conforme mostrado na Figura 2.

Figura 2 Adicionando e salvando folha para o repositório

[HttpPost]
public ActionResult Create(Leaf leaf)
{
  if (ModelState.IsValid)
  {
    leafRepository.InsertOrUpdate(leaf);
    leafRepository.Save();
    return RedirectToAction("Index", 
      new { treeId = Request.Form["TreeId"] });
  }
  else
  {
    return View();
  }
}

O repositório utiliza a folha, verificações para ver se ele é novo e se assim for, adiciona-lo para a instância de contexto que foi criada como resultado de postback:

public void InsertOrUpdate(Leaf leaf,int treeId){
  if (leaf.LeafId == default(int)) {
    // New entity
    context.Leaves.Add(leaf);
  } else {
    // Existing entity
    context.Entry(leaf).State = EntityState.Modified;
  }
}

Quando é chamado de salvar, EF cria um comando Insert, que adiciona a nova folha para o banco de dados:

    exec sp_executesql N'insert [dbo].[Leaves]([FellFromTreeDate], [FellFromTreeColor],
    [Tree_TreeId]) values (@0, @1, null)
    select [LeafId]
    from [dbo].[Leaves]
    where @@ROWCOUNT > 0 and [LeafId] = scope_identity()',
    N'@0 datetime2(7),@1 nvarchar(max) ',
    @0='2011-10-11 00:00:00',@1=N'Pale Yellow'

Observe os valores passados na segunda linha do comando: @ 0 (para a data); 1 (para a cor modificada); e nula. O valor nulo é destinado para o campo de Tree_TreeId. Lembre-se que a classe de folha agradável, limpa não tem nenhuma propriedade de chave estrangeira para representar o TreeId, então não há nenhuma maneira de passar esse valor ao criar uma folha de autônomo.

Quando o tipo dependente (no caso, folha) não tem conhecimento de seu principal tipo (árvore), há apenas uma maneira de fazer um insert: A instância de folha e a instância de árvore devem ser adicionados ao contexto juntos como parte do mesmo gráfico. Isso fornecerá EF com todas as informações que precisa para trabalhar fora o valor correto para inserir a chave estrangeira do banco de dados (por exemplo, Tree_TreeId). Mas, neste caso, onde você está não trabalhando apenas com a folha, há nenhuma informação na memória para EF determinar o valor da propriedade de chave da árvore.

Se você tivesse uma propriedade de chave estrangeira na classe Leaf, a vida seria muito mais simples. Não é muito difícil manter um único valor na mão quando se deslocam entre controladores e modos de exibição. Na verdade, se você olhar para a ação de criar em Figura 2, você pode ver que o método tem acesso para o valor de TreeId para o qual a folha está sendo criada.

Existem várias maneiras para passar dados ao redor em aplicações MVC. Eu escolhi o mais simples para esta demonstração: encher o TreeId em ViewBag MVC e aproveitando Html.Hidden campos sempre que necessário. Isso torna o valor disponível como um dos itens de Request. Form do modo de exibição.

Porque eu tenho acesso para o TreeId, eu sou capaz de construir o gráfico de árvore/folha que fornecerá o TreeId para o comando ' Inserir '. Uma rápida modificação para a classe de repositório permite que o método InsertOrUpdate aceita essa variável TreeId do Vista e recupera a instância de árvore do banco de dados usando o método DbSet.Find. Aqui está a parte afetada do método:

public void InsertOrUpdate(Leaf leaf,int treeId)
{
  if (leaf.LeafId == default(int)) {
    var tree=context.Trees.Find(treeId);
    tree.Leaves.Add(leaf);
  }
...

O contexto é agora de controle da árvore e está ciente de que estou adicionando a folha para a árvore. Desta vez, quando contexto.SaveChanges é chamado, EF é capaz de navegar da folha para a árvore para descobrir o valor da chave e usá-lo no comando Insert.

Figura 3 mostra o código do controlador modificados usando a nova versão do InsertOrUpdate.

Figura 3 A nova versão do InsertOrUpdate

[HttpPost]
public ActionResult Create(Leaf leaf)
{
  if (ModelState.IsValid)
  {
    leafRepository.InsertOrUpdate(leaf);
    leafRepository.Save();
    return RedirectToAction("Index",
      new { treeId = Request.Form["TreeId"] });
  }
  else
  {
    return View();
  }
}
[HttpPost]
public ActionResult Create(Leaf leaf)
{
  if (ModelState.IsValid)
  {
    var _treeId = Request.Form["TreeId"] as int;
    leafRepository.InsertOrUpdate(leaf, _treeId);
    leafRepository.Save();
    return RedirectToAction("Index", new { treeId = _treeId });
  }
  else
  {
    return View();
  }
}

Com essas alterações, o método insert tem finalmente o valor para a chave estrangeira, que você pode ver no parâmetro chamado "2":

exec sp_executesql N'insert [dbo].[Leaves]([FellFromTreeDate], [FellFromTreeColor], [Tree_TreeId])
values (@0, @1, @2)
select [LeafId]
from [dbo].[Leaves]
where @@ROWCOUNT > 0 and [LeafId] = scope_identity()',
N'@0 datetime2(7),@1 nvarchar(max) ,
@2 int',@0='2011-10-12 00:00:00',@1=N'Orange-Red',@2=1

No final, essa solução alternativa me obriga a fazer outra viagem para o banco de dados. Este é o preço que eu vou optar por pagar neste cenário onde eu não quero a propriedade de chave estrangeira na minha aula de dependente.

Problemas com atualizações quando não houver nenhuma chave estrangeira

Existem outras maneiras que você pode pintar-se em um canto quando estiver ligado e determinado a não ter propriedades de chaves externa em suas classes. Aqui é outro exemplo.

Eu vou adicionar uma nova classe de domínio denominada TreePhoto. Porque eu não quero navegar a partir dessa classe Voltar à árvore, não há nenhuma propriedade de navegação, e novamente, eu estou seguindo o padrão onde eu não usar uma propriedade de chave estrangeira:

[Table("TreePhotos")]
public class TreePhoto
{
  public int Id { get; set; }
  public Byte[] Photo { get; set; }
  public string Caption { get; set; }
}

A classe de árvore fornece a única ligação entre as duas classes, e eu especificar que cada árvore deve ter uma foto. Aqui é a nova propriedade que eu adicionado para a classe de árvore:

[Required]
  public TreePhoto Photo { get; set; }

Isto deixa a possibilidade de órfãos fotos, mas eu uso este exemplo porque eu vi ele várias vezes — juntamente com fundamentos para ajuda — assim que eu queria abordar a questão.

Mais uma vez, código primeira Convenção dissuadir­minadas que uma propriedade de chave estrangeira seria necessária no banco de dados e criou um, Photo_Id, em meu nome. Observe que é não-nulo. Isso ocorre porque a propriedade Leaf.Photo é necessária (consulte Figura 4).

Using Code First Convention, Tree Gets a Non-Nullable Foreign Key to TreePhotos
Figura 4 usando código primeira Convenção, árvore Obtém um não-nulo chave externa para TreePhotos

Seu aplicativo pode permitem criar árvores antes que as fotografias foram tomadas, mas a árvore ainda precisa dessa propriedade foto a ser preenchido. Vou adicionar lógica para inserir do repositório árvore­OrUpdate método para criar um padrão, foto vazio para novas árvores quando um não é fornecido:

public void InsertOrUpdate(Tree tree)
{
  if (tree.TreeId == default(int)) {
    if (tree.Photo == null)
    {
      tree.Photo = new TreePhoto { Photo = new Byte[] { 0 },
                                   Caption = "No Photo Yet" };
    }
    context.Trees.Add(tree);
}
...

O maior problema que eu quero focar aqui é como esse problema afeta atualizações. Imagine que você tenha uma árvore e sua foto necessária já armazenados no banco de dados. Você quer ser capaz de editar uma árvore e não têm nenhuma necessidade para interagir com a foto. Você vai recuperar a árvore, talvez com código como "contexto.Tre­­es.Find(someId)". Quando é hora de salvar, você obterá um erro de validação porque árvore requer uma foto. Mas a árvore tem uma foto! Ele está no banco de dados! O que está acontecendo?

Aqui está o problema: Quando você primeiro executa uma consulta para recuperar a tabela, ignorando a foto relacionada, somente os valores escalares da árvore serão retornados do banco de dados e foto será nula (ver Figura 5).

A Tree Instance Retrieved from the Database Without Its Photo
Figura 5 árvore instância recuperada do banco de dados sem sua foto

O fichário de modelo MVC e EF têm a capacidade de validar a anotação necessária. Quando é hora de salvar a árvore editada, sua foto ainda será nula. Se você está deixando MVC execute sua seleção de ModelState.IsValid no código do controlador, ele reconhecerá que a foto está ausente. IsValid será false e o controlador não incomoda mesmo chamando o repositório. Em meu aplicativo, removi a validação do modelo fichário assim que eu posso deixar meu código de repositório ser responsável por qualquer validação do lado do servidor. Quando o repositório chama SaveChanges, validação de EF detectará a foto ausente e lançar uma exceção. Mas no repositório, temos a oportunidade de lidar com o problema.

Se a classe de árvore tinha uma propriedade de chave estrangeira — por exemplo, int PhotoId — que era necessário (permitindo que você remover o requisito sobre a propriedade de navegação na foto), o valor da chave estrangeiro do banco de dados já foi usado para preencher a propriedade PhotoId da instância de árvore. A árvore seria válida e SaveChanges seria capaz de enviar o comando Update para o banco de dados. Em outras palavras, se houvesse uma propriedade de chave estrangeira, a árvore teria sido válida mesmo sem a instância de foto.

Mas sem a chave estrangeira, você precisará novamente algum mecanismo para fornecer a foto antes de salvar as alterações. Se você tem seu primeiro código classes e contexto configurado para executar o carregamento lento, qualquer menção da foto em seu código fará com que EF carregar a instância do banco de dados. Eu ainda estou um pouco antiquado, quando se trata de preguiçoso para carregamento, por isso minha escolha pessoal seria, provavelmente realizar um carregamento explícito do banco de dados. A nova linha de código (a última linha no exemplo a seguir, onde eu estou chamando de carga) usa o método DbContext para carregar dados relacionados:

public void InsertOrUpdate(Tree tree)
{
  if (tree.TreeId == default(int)) {
  ...
} else {
    context.Entry(tree).State = EntityState.Modified;
    context.Entry(tree).Reference(t => t.Photo).Load();
  }
}

Isso faz EF feliz. Árvore validará porque foto está lá, e EF enviará uma atualização para o banco de dados para a árvore modificada. A chave aqui é que você precisa para garantir que a foto não é nula; Mostrei a você uma maneira de satisfazer essa restrição.

Um ponto de comparação

Se a classe de árvore simplesmente tinha uma propriedade PhotoId, nada disto seria necessário. Um efeito directo da propriedade int PhotoId é que a propriedade de foto não precisa a anotação necessária. Como um tipo de valor, sempre deve ter um valor, que satisfazem a exigência de que uma árvore deve ter uma foto, mesmo que ele não é representado como uma instância. Enquanto houver um valor em PhotoId, a exigência será satisfeita, portanto, o código a seguir funciona:

public class Tree
{
  // ...
Other properties
  public int PhotoId { get; set; }
  public TreePhoto Photo { get; set; }
}

Quando o método de edição do controlador recupera uma árvore do banco de dados, a propriedade de escalar PhotoId será preenchida. Enquanto você força MVC (ou qualquer estrutura de aplicativo que você está usando) para ida e volta que valor, quando é hora de atualizar a árvore, EF será despreocupado sobre a propriedade de foto nula.

Mais fácil, mas não mágica

Embora a equipe EF forneceu mais lógica de API para ajudar em situações desconectadas, ainda é seu trabalho para entender como funciona o EF e quais são suas expectativas quando você estiver movendo dados ao redor. Sim, a codificação é muito mais simples se incluem chaves externas em suas classes, mas eles são suas classes e você é o melhor juiz do que deve e não deve ser neles. No entanto, se seu código foi minha responsabilidade, eu teria certamente forçá-lo a me convencer de que suas razões para excluir propriedades de chaves externa compensados os benefícios de incluí-los. EF fará parte do trabalho para você se as chaves estrangeiras estão lá. Mas se eles estão ausentes, contanto que você entende o que espera EF e como satisfazer essas expectativas, você deve ser capaz de obter seus aplicativos desconectados se comportar da maneira desejada.

Julie Lerman é um Microsoft MVP,.Mentor líquido e consultor que vive nas colinas de Vermont. Você pode encontrar sua apresentação sobre acesso a dados e outro Microsoft.NET tópicos em grupos de usuários e conferências em todo o mundo. She blogs at thedatafarm.com e é o autor de "Programming Entity Framework" (2010) e "programação Entity Framework: Código primeiro"(2011), ambos do ' Reilly Media. Segui-la no Twitter em twitter.com/julielerman.

Graças aos seguintes especialistas técnicos para revisão deste artigo: Derstadt de Jeff e Rick Strahl