Ao aprender LINQ, quase me deparei com uma dificuldade, que é a operação de atualização do banco de dados que você vê no título. Agora vou levá-lo passo a passo neste atoleiro. Por favor, prepare seus tijolos e saliva, siga-me.
Vamos começar com o caso mais simples. Tomemos o banco de dados Northwind como exemplo. Quando você precisar modificar o ProductName de um produto, você pode escrever diretamente o seguinte código no cliente:
// Lista 0NorthwindDataContext db = new NorthwindDataContext();
Produto produto = db.Products.Single(p => p.ProductID == 1);
product.ProductName = "Chai alterado";
db.SubmitChanges();
Teste-o e a atualização será bem-sucedida. Porém, acredito que tal código não aparecerá em seus projetos, pois é simplesmente impossível reutilizá-lo. Ok, vamos refatorá-lo e extraí-lo em um método. Quais devem ser os parâmetros? é o novo nome do produto e o ID do produto a ser atualizado. Bem, esse parece ser o caso.
public void UpdateProduct(int id, string productName)
{
NorthwindDataContext db = new NorthwindDataContext();
Produto produto = db.Products.Single(p => p.ProductID == id);
produto.NomeDoProduto = nomeDoProduto;
db.SubmitChanges();
}Em projetos reais, não podemos apenas modificar o nome do produto. Outros campos do Produto também estão sujeitos a modificações. Então a assinatura do método UpdateProduct ficará da seguinte forma:
public void UpdateProduct(int id,
string nomeDoProduto,
int fornecedorId,
int categoriaId,
string quantidadePorUnit,
unidade decimalPreço,
unidades curtasEm estoque,
unidades curtasOnOrder,
short reorderLevel) Claro, este é apenas um banco de dados simples. Em projetos reais, não é incomum ter vinte, trinta ou até centenas de campos. Quem pode tolerar tais métodos? Se você escrever assim, o que o objeto Produto faz?
Isso mesmo, use Product como parâmetro do método e jogue a chata operação de atribuição no código do cliente. Ao mesmo tempo, extraímos o código para obter a instância Product para formar o método GetProduct e colocamos os métodos relacionados às operações do banco de dados em uma classe ProductRepository que é especificamente responsável por lidar com o banco de dados. Ah, sim, SRP!
// Lista 1
//ProdutoRepositório
Produto público GetProduct (int id)
{
NorthwindDataContext db = new NorthwindDataContext();
retornar db.Products.SingleOrDefault(p => p.id == id);
}
public void UpdateProduct (produto produto)
{
NorthwindDataContext db = new NorthwindDataContext();
db.Produtos.Attach(produto);
db.SubmitChanges();
}
//Código do cliente
Repositório ProductRepository = new ProductRepository();
Produto produto = repositório.GetProduct(1);
product.ProductName = "Chai alterado";
repositório.UpdateProduct(produto);
Aqui eu uso o método Attach para anexar uma instância de Product a outro DataContext. Para o banco de dados Northwind padrão, o resultado disso é a seguinte exceção:
// Exceção 1 NotSupportException:
Tentativa de anexar ou adicionar entidade, a entidade não é uma entidade nova e pode ter sido carregada de outro DataContext. Esta operação não é suportada.
Foi feita uma tentativa de anexar ou adicionar uma entidade que não é nova,
Talvez tenha sido carregado de outro DataContext. Isso não é suportado. Olhando para o MSDN, sabemos que ao serializar entidades para o cliente, essas entidades são desanexadas de seu DataContext original. O DataContext não rastreia mais alterações nessas entidades ou suas associações com outros objetos. Se quiser atualizar ou excluir dados neste momento, você deve usar o método Attach para anexar a entidade ao novo DataContext antes de chamar SubmitChanges, caso contrário, a exceção acima será lançada.
No banco de dados Northwind, a classe Produto contém três classes relacionadas (ou seja, associações de chave estrangeira): Order_Detail, Categoria e Fornecedor. No exemplo acima, embora anexemos o Produto, não há nenhuma classe Attach associada a ele, portanto, uma NotSupportException é lançada.
Então, como associar classes relacionadas ao Produto? Isso pode parecer complicado, mesmo para um banco de dados simples como o Northwind. Parece que devemos primeiro obter as classes originais de Order_Detail, Category e Supplier relacionadas ao Produto original e, em seguida, anexá-las ao DataContext atual respectivamente, mas na verdade, mesmo se fizermos isso, NotSupportException será lançada.
Então, como implementar a operação de atualização? Para simplificar, excluímos outras classes de entidade em Northwind.dbml e mantemos apenas Produto. Desta forma, podemos iniciar a análise a partir do caso mais simples.
Após excluir outras classes por problemas, executamos novamente o código da Lista 1, mas o banco de dados não alterou o nome do produto. Observando a versão sobrecarregada do método Attach, podemos encontrar facilmente o problema.
O método Attach(entity) chama a sobrecarga Attach(entity, false) por padrão, que anexará a entidade correspondente em um estado não modificado. Se o objeto Product não tiver sido modificado, devemos chamar esta versão sobrecarregada para anexar o objeto Product ao DataContext em um estado não modificado para operações subsequentes. Neste momento, o status do objeto Produto é "modificado" e só podemos chamar o método Attach(entity, true).
Então alteramos o código relevante na Lista 1 para Attach(product, true) e vimos o que aconteceu?
// Exceção 2 InvalidOperationException:
Se uma entidade declarar um membro de versão ou não tiver uma política de verificação de atualização, ela só poderá ser anexada como uma entidade modificada sem o estado original.
Uma entidade só pode ser anexada como modificada sem estado original
se declarar um membro de versão ou não tiver uma política de verificação de atualização.
LINQ to SQL usa a coluna RowVersion para implementar a verificação de simultaneidade otimista padrão, caso contrário, o erro acima ocorrerá ao anexar entidades ao DataContext em um estado modificado. Existem duas maneiras de implementar a coluna RowVersion. Uma é definir uma coluna do tipo timestamp para a tabela do banco de dados e a outra é definir o atributo IsVersion=true no atributo da entidade correspondente à chave primária da tabela. Observe que você não pode ter a coluna TimeStamp e o atributo IsVersion=true ao mesmo tempo, caso contrário, uma InvalidOprationException será lançada: Os membros "System.Data.Linq.Binary TimeStamp" e "Int32 ProductID" são ambos marcados como versões de linha. Neste artigo, usamos a coluna timestamp como exemplo.
Depois de criar uma coluna chamada TimeStamp e digitar timestamp para a tabela Produtos, arraste-a de volta para o designer e execute o código da Lista 1. Graças a Deus, finalmente funcionou.
Agora, arrastamos a tabela Categorias para o designer. Aprendi a lição desta vez e primeiro adicionei a coluna timestamp à tabela Categorias. Depois de testá-lo, descobriu-se que era o erro na Exceção 1 novamente! Depois de excluir a coluna timestamp das Categorias, o problema permanece. Oh meu Deus, o que exatamente é feito no terrível método Attach?
Ah, a propósito, existe uma versão sobrecarregada do método Attach, vamos tentar.
public void UpdateProduct (produto produto)
{
NorthwindDataContext db = new NorthwindDataContext();
Produto antigoProduto = db.Products.SingleOrDefault(p => p.ProductID == product.ProductID);
db.Products.Attach(produto, produtoantigo);
db.SubmitChanges();
} Ou erro de exceção 1!
Eu vou cair! Anexar, Anexar, o que aconteceu com você?
Para explorar o código-fonte LINQ to SQL, usamos o plug-in FileDisassembler do Reflector para descompilar System.Data.Linq.dll em código cs e gerar arquivos de projeto, o que nos ajuda a encontrá-lo e localizá-lo no Visual Studio.
Quando a exceção 1 é lançada?
Primeiro encontramos as informações descritas na Exceção 1 de System.Data.Linq.resx e obtemos a chave "CannotAttachAddNonNewEntities", depois encontramos o método System.Data.Linq.Error.CannotAttachAddNonNewEntities(), encontramos todas as referências a este método e encontramos que existem dois Este método é usado em três lugares, ou seja, o método StandardChangeTracker.Track e o método InitializeDeferredLoader.
Abrimos o código de Table.Attach(entity, bool) novamente e, como esperado, descobrimos que ele chama o método StandardChangeTracker.Track (o mesmo é verdadeiro para o método Attach(entity,entity)):
trackedObject = this.context.Services.ChangeTracker.Track(entity, true); No método Track, o código a seguir lança a Exceção 1:
if (trackedObject.HasDeferredLoaders)
{
lançar System.Data.Linq.Error.CannotAttachAddNonNewEntities();
}Então voltamos nossa atenção para a propriedade StandardTrackedObject.HasDeferredLoaders:
substituição interna bool HasDeferredLoaders
{
pegar
{
foreach (associação MetaAssociation em this.Type.Associations)
{
if (this.HasDeferredLoader(associação.ThisMember))
{
retornar verdadeiro;
}
}
foreach (membro MetaDataMember de p em this.Type.PersistentDataMembers
onde p.IsDeferred && !p.IsAssociation
selecione p)
{
if (this.HasDeferredLoader(membro))
{
retornar verdadeiro;
}
}
retornar falso;
}
} A partir disso podemos deduzir aproximadamente que enquanto houver itens carregados lentamente na entidade, a operação Attach lançará a Exceção 1. Isso está exatamente de acordo com o cenário em que ocorre a Exceção 1 - a classe Produto contém itens de carregamento lento.
Surgiu então uma maneira de evitar essa exceção - remover os itens que precisam ser carregados com atraso no Produto. Como removê-lo? Você pode usar DataLoadOptions para carregar imediatamente ou definir itens que exigem carregamento lento como nulos. Mas o primeiro método não funcionou, então tive que usar o segundo método.
// Lista 2
classe ProdutoRepositório
{
Produto público GetProduct (int id)
{
NorthwindDataContext db = new NorthwindDataContext();
retornar db.Products.SingleOrDefault(p => p.ProductID == id);
}
Produto público GetProductNoDeffered (int id)
{
NorthwindDataContext db = new NorthwindDataContext();
//Opções de DataLoadOptions = new DataLoadOptions();
//options.LoadWith<Produto>(p => p.Categoria);
//db.LoadOptions = opções;
var produto = db.Products.SingleOrDefault(p => p.ProductID == id);
produto.Categoria = null;
devolver produto;
}
public void UpdateProduct (produto produto)
{
NorthwindDataContext db = new NorthwindDataContext();
db.Products.Attach(produto, verdadeiro);
db.SubmitChanges();
}
}
//Código do cliente
Repositório ProductRepository = new ProductRepository();
Produto produto = repositório.GetProductNoDeffered(1);
product.ProductName = "Chai alterado";
repositório.UpdateProduct(produto);
Quando a exceção 2 é lançada?
Seguindo o método da seção anterior, encontramos rapidamente o código que gerou a Exceção 2. Felizmente, havia apenas este em todo o projeto:
if (asModified && ((inheritanceType.VersionMember == null) && HeritageType.HasUpdateCheck))
{
lançar System.Data.Linq.Error.CannotAttachAsModifiedWithoutOriginalState();
}
Como você pode ver, quando o segundo parâmetro asModified de Attach for verdadeiro, não contém a coluna RowVersion (VersionMember=null) e contém uma coluna de verificação de atualização (HasUpdateCheck), a Exceção 2 será lançada. O código do HasUpdateCheck é o seguinte:
substituição pública bool HasUpdateCheck
{
pegar
{
foreach (membro MetaDataMember em this.PersistentDataMembers)
{
if (membro.UpdateCheck! = UpdateCheck.Never)
{
retornar verdadeiro;
}
}
retornar falso;
}
}Isso também é consistente com nosso cenário - a tabela Products não possui uma coluna RowVersion, e no código gerado automaticamente pelo designer, as propriedades UpdateCheck de todos os campos são o padrão Always, ou seja, a propriedade HasUpdateCheck é verdadeira.
A maneira de evitar a Exceção 2 é ainda mais simples, adicione uma coluna TimeStamp a todas as tabelas ou defina o campo IsVersion=true nos campos de chave primária de todas as tabelas. Como o último método modifica as classes geradas automaticamente e pode ser substituído por novos designs a qualquer momento, recomendo usar o método anterior.
Como usar o método Attach?
Após a análise acima, podemos descobrir duas condições relacionadas ao método Attach: se existe uma coluna RowVersion e se existe uma associação de chave estrangeira (ou seja, itens que precisam ser carregados lentamente). Resumi essas duas condições e o uso de várias sobrecargas do Attach em uma tabela. Ao olhar a tabela abaixo, você precisa estar totalmente preparado mentalmente.
número de série
Anexar método
Se a coluna RowVersion tem uma descrição associada
1 Anexar(entidade) Não Não Sem modificação
2 Attach(entity) Não Sim NotSupportException: Foi tentada uma entidade Attach ou Add A entidade não é uma nova entidade e pode ser carregada de outros DataContexts. Esta operação não é suportada.
3 Attach(entity) Se não há modificação
4 Attach(entity) não é modificado. O mesmo que 2 se o subconjunto não tiver uma coluna RowVersion.
5 Attach(entity, true) Não Não InvalidOperationException: Se a entidade declarar um membro de versão ou não tiver política de verificação de atualização, ela só poderá ser anexada como uma entidade modificada sem estado original.
6 Attach(entity, true) Não Sim NotSupportException: Foi tentada uma entidade Attach ou Add A entidade não é uma nova entidade e pode ser carregada de outros DataContexts. Esta operação não é suportada.
7 Attach(entity, true) Se a modificação é normal (modificar à força a coluna RowVersion reportará um erro)
8 Attach(entity, true) Sim NotSupportException: uma entidade Attach ou Add foi tentada. A entidade não é uma nova entidade e pode ser carregada de outros DataContexts. Esta operação não é suportada.
9 Attach(entity,entity) Não Não DuplicateKeyException: Não é possível adicionar uma entidade cuja chave já esteja em uso.
10 Attach(entidade, entidade) Não Sim NotSupportException: Foi tentada uma entidade Anexar ou Adicionar. A entidade não é uma entidade nova e pode ser carregada de outros DataContexts. Esta operação não é suportada.
11 Attach(entity,entity) DuplicateKeyException: Não é possível adicionar uma entidade cuja chave já esteja em uso.
12 Attach(entidade, entidade) Sim NotSupportException: Foi tentada uma entidade Anexar ou Adicionar. A entidade não é uma entidade nova e pode ser carregada de outros DataContexts. Esta operação não é suportada.
Attach só pode ser atualizado normalmente na 7ª situação (incluindo a coluna RowVersion e sem associação de chave estrangeira)! Esta situação é quase impossível para um sistema baseado em banco de dados! Que tipo de API é essa?
Resumo Vamos nos acalmar e começar a resumir.
Se você escrever o código LINQ to SQL diretamente na UI, como a Lista 0, nada de ruim acontecerá. Mas se você tentar abstrair uma camada separada de acesso a dados, ocorrerá um desastre. Isso significa que o LINQ to SQL não é adequado para o desenvolvimento de arquitetura multicamadas? Muitas pessoas dizem que o LINQ to SQL é adequado para o desenvolvimento de sistemas pequenos, mas o tamanho pequeno não significa que não esteja em camadas. Existe alguma maneira de evitar tantas exceções?
Na verdade, este artigo deu algumas pistas. No próximo ensaio desta série, tentarei fornecer várias soluções para que todos possam escolher.