Salvar dados de volta no banco de dados em aplicativos do .NET Framework

Observação

Os conjuntos de dados e as classes relacionadas são tecnologias herdadas do .NET Framework do início dos anos 2000 que permitem que os aplicativos trabalhem com dados na memória enquanto os aplicativos estão desconectados do banco de dados. As tecnologias são bastante úteis em aplicativos que permitem que os usuários modifiquem dados e persistam as alterações no banco de dados. Embora os conjuntos de dados tenham se mostrado uma tecnologia muito bem-sucedida, é recomendado que os novos aplicativos .NET usem o Entity Framework Core. O Entity Framework proporciona uma forma mais natural de trabalhar com dados tabulares como modelos de objeto e conta com uma interface de programação mais simples.

O conjunto de dados é uma cópia de dados carregados na memória. Se você modificar esses dados, uma boa prática é salvar essas alterações no banco de dados. Você faz isso de uma das três maneiras:

  • Chamando um dos métodos Update de um TableAdapter

  • Chamando um dos métodos DBDirect do TableAdapter

  • Chamando o método UpdateAll no TableAdapterManager gerado pelo Visual Studio, quando o conjunto de dados contém tabelas relacionadas a outras tabelas no conjunto de dados

Quando você associa as tabelas de conjunto de dados aos controles em uma página do Windows Form ou XAML, a arquitetura de associação de dados faz todo o trabalho.

Se você estiver familiarizado com o TableAdapters, poderá ir diretamente para um destes tópicos:

Tópico Descrição
Inserir novos registros em um banco de dados Como executar atualizações e inserções usando os objetos do TableAdapters ou Command
Atualizar dados usando um TableAdapter Como executar atualizações com o TableAdapters
Atualização hierárquica Como executar atualizações de um conjunto de dados com duas ou mais tabelas relacionadas
Tratar uma exceção de simultaneidade Como lidar com exceções quando dois usuários tentam alterar os mesmos dados em um banco de dados ao mesmo tempo
Como salvar dados usando uma transação Como salvar dados em uma transação usando o Sistema. Namespace das transações e um objeto do TransactionScope
Salvando dados em uma transação Passo a passo que cria um aplicativo do Windows Forms para demonstrar como salvar dados em um banco de dados dentro de uma transação
Salvar dados em um banco de dados (várias tabelas) Como editar registros e salvar alterações em várias tabelas no banco de dados
Salvar dados de um objeto em um banco de dados Como passar dados de um objeto que não está em um conjunto de dados para um banco de dados usando um método DbDirect do TableAdapter
Salvar os dados com os métodos TableAdapter DBDirect Como usar o TableAdapter para enviar consultas SQL diretamente para o banco de dados
Salvar um conjunto de dados como XML Como salvar um conjunto de dados em um documento XML

Atualizações em duas fases

Atualizar uma fonte de dados é um processo de duas etapas. A primeira etapa é atualizar o conjunto de dados com novos registros, registros alterados ou registros excluídos. Se o aplicativo nunca enviar essas alterações à fonte de dados, a atualização será concluída.

Se você enviar as alterações ao banco de dados, a segunda etapa será necessária. Se você não estiver usando controles associados a dados, precisará chamar manualmente o método Update do mesmo TableAdapter (ou adaptador de dados) usado para preencher o conjunto de dados. No entanto, você também pode usar adaptadores diferentes, por exemplo, para mover dados de uma fonte de dados para outra ou atualizar várias fontes de dados. Se você não estiver usando a associação de dados e estiver salvando alterações para tabelas relacionadas, precisará instanciar manualmente uma variável da classe TableAdapterManager gerada automaticamente e, em seguida, chamar seu método UpdateAll.

Diagrama conceitual de atualizações de conjunto de dados

Um conjunto de dados contém coleções de tabelas, que contêm coleções de linhas. Se você pretende atualizar uma fonte de dados subjacente posteriormente, deve usar os métodos na propriedade DataTable.DataRowCollection ao adicionar ou remover linhas. Esses métodos executam o controle de alterações necessário para atualizar a fonte de dados. Se você chamar a coleção RemoveAt na propriedade Rows, a exclusão não será comunicada ao banco de dados.

Mesclar conjuntos de dados

Você pode atualizar o conteúdo de um conjunto de dados mesclando-o com outro conjunto de dados. Isso envolve copiar o conteúdo de um conjunto de dados de origem para o conjunto de dados de chamada (conhecido como conjunto de dados de destino). Quando você mescla conjuntos de dados, novos registros no conjunto de dados de origem são adicionados ao conjunto de dados de destino. Além disso, colunas extras no conjunto de dados de origem são adicionadas ao conjunto de dados de destino. A mesclagem de conjuntos de dados é útil quando você tem um conjunto de dados local e obtém um segundo conjunto de dados de outro aplicativo. Também é útil quando você obtém um segundo conjunto de dados de um componente, como um serviço Web XML, ou quando precisa integrar dados de vários conjuntos de dados.

Ao mesclar conjuntos de dados, você pode passar um argumento Booliano (preserveChanges) que informa ao método Merge se deve manter as modificações existentes no conjunto de dados de destino. Como os conjuntos de dados mantêm várias versões de registros, é importante ter em mente que mais de uma versão dos registros está sendo mesclada. A tabela a seguir mostra como um registro em dois conjuntos de dados é mesclado:

DataRowVersion Conjunto de dados de destino Conjunto de dados de origem
Original James Wilson James C. Wilson
Current Jim Wilson James C. Wilson

Chamar o método Merge na tabela anterior com o preserveChanges=false targetDataset.Merge(sourceDataset) resulta nos seguintes dados:

DataRowVersion Conjunto de dados de destino Conjunto de dados de origem
Original James C. Wilson James C. Wilson
Current James C. Wilson James C. Wilson

Chamar o método Merge com o preserveChanges = true targetDataset.Merge(sourceDataset, true) resulta nos seguintes dados:

DataRowVersion Conjunto de dados de destino Conjunto de dados de origem
Original James C. Wilson James C. Wilson
Current Jim Wilson James C. Wilson

Cuidado

No cenário preserveChanges = true, se o método RejectChanges for chamado em um registro no conjunto de dados de destino, ele será revertido para os dados originais do conjunto de dados de origem. Isso significa que, se você tentar atualizar a fonte de dados original com o conjunto de dados de destino, talvez não consiga encontrar a linha original a ser atualizada. Você pode evitar uma violação de simultaneidade preenchendo outro conjunto de dados com os registros atualizados da fonte de dados e, em seguida, executando uma mesclagem. (Uma violação de simultaneidade ocorre quando outro usuário modifica um registro na fonte de dados, depois que o conjunto de dados é preenchido.)

Atualizar restrições

Para fazer alterações em uma linha de dados existente, adicione ou atualize dados nas colunas individuais. Se o conjunto de dados contiver restrições (como chaves estrangeiras ou restrições não anuláveis), é possível que o registro possa estar temporariamente em um estado de erro ao atualizá-lo. Ou seja, ele pode estar em um estado de erro depois que você terminar de atualizar uma coluna, mas antes de chegar à próxima.

Para evitar violações de restrição prematuras, você pode suspender temporariamente as restrições de atualização. Isso atende a duas finalidades:

  • Isso evita que um erro seja gerado depois que você terminar de atualizar uma coluna, mas não tiver iniciado a atualização de outra.

  • Isso impede que determinados eventos de atualização sejam gerados (eventos que geralmente são usados para validação).

Observação

No Windows Forms, a arquitetura de associação de dados incorporada ao datagrid suspende a verificação de restrição até que o foco saia de uma linha e você não precise chamar explicitamente os métodos BeginEdit, EndEdit ou CancelEdit.

As restrições são desabilitadas automaticamente quando o método Merge é invocado em um conjunto de dados. Quando a mesclagem for concluída, se houver restrições no conjunto de dados que não possam ser habilitadas, uma ConstraintException será gerada. Nessa situação, a propriedade EnforceConstraints será definida como false, e todas as violações de restrição devem ser resolvidas antes de redefinir a propriedade EnforceConstraints como true.

Depois de concluir uma atualização, você poderá reabilitar a verificação de restrição, que também reabilita e gera os eventos de atualização.

Para obter mais informações sobre como suspender eventos, confira Desativar restrições ao preencher um conjunto de dados.

Erros de atualização do conjunto de dados

Quando você atualiza um registro em um conjunto de dados, há uma possibilidade de erro. Por exemplo, você pode gravar inadvertidamente dados do tipo errado em uma coluna, dados muito longos ou dados que têm algum outro problema de integridade. Ou você pode ter verificações de validação específicas do aplicativo que podem gerar erros personalizados durante qualquer fase de um evento de atualização. Para obter mais informações, confira Validar dados em conjuntos de dados.

Manter informações sobre alterações

As informações sobre as alterações em um conjunto de dados são mantidas de duas maneiras: sinalizando as linhas que indicam que elas foram alteradas (RowState) e mantendo várias cópias de um registro (DataRowVersion). Usando essas informações, os processos podem determinar o que mudou no conjunto de dados e podem enviar atualizações apropriadas para a fonte de dados.

Propriedade RowState

A propriedade RowState de um objeto DataRow é um valor que fornece informações sobre o status de uma linha de dados específica.

A tabela a seguir detalha os possíveis valores da enumeração DataRowState:

Valor de DataRowState Descrição
Added A linha foi adicionada como um item a um DataRowCollection. (Uma linha nesse estado não tem uma versão original correspondente, pois não existia quando o último método AcceptChanges foi chamado).
Deleted A linha foi excluída usando o Delete de um objeto DataRow.
Detached A linha foi criada, mas não faz parte de nenhum DataRowCollection. Um objeto DataRow entra nesse estado imediatamente depois de ser criado, antes de ser adicionado a uma coleção ou depois de ter sido removido de uma coleção.
Modified Um valor de coluna na linha foi alterado de alguma forma.
Unchanged A linha não foi alterada desde a última chamada a AcceptChanges.

Enumeração DataRowVersion

Os conjuntos de dados mantêm várias versões de registros. Os campos DataRowVersion são usados ao recuperar o valor encontrado em um DataRow, usando a propriedade Item[] ou o método GetChildRows do objeto DataRow.

A tabela a seguir detalha os possíveis valores da enumeração DataRowVersion:

Valor de DataRowVersion Descrição
Current A versão atual de um registro contém todas as modificações executadas no registro desde que a última vez o AcceptChanges foi chamado. Se a linha tiver sido excluída, não haverá versão atual.
Default O valor padrão de um registro conforme definido pelo esquema ou fonte de dados do conjunto de dados.
Original A versão original de um registro é uma cópia do registro, pois foi a última vez que as alterações foram confirmadas no conjunto de dados. Na prática, normalmente essa é a versão de um registro como lido em uma fonte de dados.
Proposed A versão proposta de um registro que está disponível temporariamente enquanto você está no meio de uma atualização, ou seja, entre o momento em que você chamou o método BeginEdit e o método EndEdit. Normalmente, você acessa a versão proposta de um registro em um manipulador para um evento como RowChanging. Invocar o método CancelEdit inverte as alterações e exclui a versão proposta da linha de dados.

As versões original e atual são úteis quando as informações de atualização são transmitidas para uma fonte de dados. Normalmente, quando uma atualização é enviada para a fonte de dados, as novas informações do banco de dados estão na versão atual de um registro. As informações da versão original são usadas para localizar o registro a ser atualizado.

Por exemplo, em um caso em que a chave primária de um registro é alterada, você precisa de uma maneira de localizar o registro correto na fonte de dados para atualizar as alterações. Se nenhuma versão original existisse, provavelmente o registro seria acrescentado à fonte de dados, resultando não apenas em um registro adicional indesejado, mas em um registro impreciso e desatualizado. As duas versões também são usadas no controle de simultaneidade. Você pode comparar a versão original com um registro na fonte de dados para determinar se o registro foi alterado desde que foi carregado no conjunto de dados.

A versão proposta é útil quando você precisa executar a validação antes de realmente confirmar as alterações no conjunto de dados.

Mesmo que os registros tenham sido alterados, nem sempre há versões original ou atual dessa linha. Quando você insere uma nova linha na tabela, não há versão original, apenas uma versão atual. Da mesma forma, se você excluir uma linha chamando o método Delete da tabela, haverá uma versão original, mas não haverá versão atual.

Você pode testar para ver se existe uma versão específica de um registro, consultando o método HasVersion de uma linha de dados. Você pode acessar qualquer versão de um registro passando um valor de enumeração DataRowVersion como um argumento opcional ao solicitar o valor de uma coluna.

Obter registros alterados

É uma prática comum não atualizar todos os registros em um conjunto de dados. Por exemplo, um usuário pode estar trabalhando com um controle DataGridView do Windows Forms que exibe muitos registros. No entanto, o usuário pode atualizar apenas alguns registros, excluir um e inserir um novo. Os conjuntos de dados e as tabelas de dados fornecem um método (GetChanges) para retornar apenas as linhas modificadas.

Você pode criar subconjuntos de registros alterados usando o método GetChanges da tabela de dados (GetChanges) ou do próprio conjunto de dados (GetChanges). Se você chamar o método para a tabela de dados, ele retornará uma cópia da tabela com apenas os registros alterados. Da mesma forma, se você chamar o método no conjunto de dados, obterá um novo conjunto de dados com apenas registros alterados.

GetChanges por si só retorna todos os registros alterados. Por outro lado, ao passar o DataRowState desejado como parâmetro para o método GetChanges, você pode especificar qual subconjunto de registros alterados deseja: registros recém-adicionados, registros marcados para exclusão, registros desanexados ou registros modificados.

Obter um subconjunto de registros alterados é útil quando você deseja enviar registros para outro componente para processamento. Em vez de enviar todo o conjunto de dados, você pode reduzir a sobrecarga de comunicação com o outro componente, obtendo apenas os registros de que o componente precisa.

Confirmar alterações no conjunto de dados

À medida que as alterações são feitas no conjunto de dados, a propriedade RowState das linhas alteradas é definida. As versões original e atual dos registros são estabelecidas, mantidas e disponibilizadas pela propriedade RowVersion. Os metadados armazenados nas propriedades dessas linhas alteradas são necessários para enviar as atualizações corretas para a fonte de dados.

Se as alterações refletirem o estado atual da fonte de dados, você não precisará mais manter essas informações. Normalmente, há dois momentos em que o conjunto de dados e sua origem estão sincronizados:

  • Imediatamente depois de carregar as informações no conjunto de dados, como quando você lê os dados na origem.

  • Depois de enviar as alterações do conjunto de dados para a fonte de dados (mas não antes, porque você perderia as informações de alteração necessárias para enviar alterações ao banco de dados).

Você pode confirmar as alterações pendentes no conjunto de dados chamando o método AcceptChanges. Normalmente, AcceptChanges é chamado nos seguintes momentos:

  • Depois de carregar o conjunto de dados. Se você carregar um conjunto de dados chamando um método Fill do TableAdapter, o adaptador confirmará as alterações automaticamente. No entanto, se você carregar um conjunto de dados mesclando outro conjunto de dados, será necessário confirmar as alterações manualmente.

    Observação

    Você pode impedir que o adaptador confirme as alterações automaticamente ao chamar o método Fill, definindo a propriedade AcceptChangesDuringFill do adaptador como false. Se estiver definido como false, o RowState de cada linha inserida durante o preenchimento será definido como Added.

  • Depois de enviar as alterações do conjunto de dados para outro processo, como um serviço Web XML.

    Cuidado

    Confirmar a alteração dessa maneira apaga as informações de alteração. Não confirme as alterações até concluir a execução de operações que exigem que seu aplicativo saiba quais alterações foram feitas no conjunto de dados.

Esse método realiza o seguinte:

O método AcceptChanges está disponível em três níveis. Você pode chamá-lo em um objeto DataRow para confirmar alterações apenas para essa linha. Você também pode chamá-lo em um objeto DataTable para confirmar todas as linhas em uma tabela. Por fim, você pode chamá-lo no objeto DataSet para confirmar todas as alterações pendentes em todos os registros de todas as tabelas do conjunto de dados.

A tabela a seguir descreve quais alterações são confirmadas com base no objeto em que o método é chamado:

Método Result
System.Data.DataRow.AcceptChanges As alterações são confirmadas somente na linha específica.
System.Data.DataTable.AcceptChanges As alterações são confirmadas em todas as linhas da tabela específica.
System.Data.DataSet.AcceptChanges As alterações são confirmadas em todas as linhas em todas as tabelas do conjunto de dados.

Observação

Se você carregar um conjunto de dados chamando um método Fill do TableAdapter, não precisará aceitar as alterações explicitamente. Por padrão, o método Fill chama o método AcceptChanges depois que termina de preencher a tabela de dados.

Um método relacionado, RejectChanges, desfaz o efeito das alterações copiando a versão Original para a versão Current dos registros. Ele também define o RowState de cada registro como Unchanged.

Validação de dados

Para verificar se os dados no seu aplicativo atendem aos requisitos dos processos para os quais são passados, geralmente você precisa adicionar a validação. Isso pode envolver verificar se a entrada de um usuário em um formulário está correta, validar dados enviados para seu aplicativo por outro aplicativo ou até mesmo verificar se as informações calculadas no seu componente estão dentro das restrições da sua fonte de dados e dos requisitos de aplicativo.

Você pode validar os dados de várias maneiras:

  • Na camada de negócios, adicionando o código ao seu aplicativo para validar os dados. O conjunto de dados é um local em que você pode fazer isso. O conjunto de dados fornece algumas das vantagens da validação de back-end, como a capacidade de validar alterações à medida que os valores de coluna e linha estão sendo alterados. Para obter mais informações, confira Validar dados em conjuntos de dados.

  • Na camada de apresentação, adicionando a validação aos formulários. Para obter mais informações, confira Validação da entrada de usuário no Windows Forms.

  • No back-end de dados, enviando os dados para a fonte de dados, por exemplo, o banco de dados, e permitindo que aceite ou rejeite os dados. Se você estiver trabalhando com um banco de dados que tenha recursos avançados para validar os dados e fornecer as informações de erro, essa pode ser uma abordagem prática, pois você pode validar os dados, independentemente de onde venham. No entanto, essa abordagem pode não atender aos requisitos de validação específicos do aplicativo. Além disso, fazer com que a fonte de dados valide os dados pode resultar em várias viagens de ida e volta para a fonte de dados, dependendo de como seu aplicativo facilita a resolução de erros de validação gerados pelo back-end.

    Importante

    Ao usar comandos de dados com a propriedade CommandType definida como Text, verifique cuidadosamente as informações enviadas de um cliente antes de passá-las para o banco de dados. Usuários maliciosos podem tentar enviar (injetar) instruções SQL modificadas ou adicionais para obter acesso não autorizado ou para danificar o banco de dados. Antes de transferir a entrada de usuário para um banco de dados, sempre verifique se as informações são válidas. A melhor prática é sempre usar consultas parametrizadas ou procedimentos armazenados, quando possível.

Transmitir atualizações para a fonte de dados

Depois que as alterações forem feitas em um conjunto de dados, você poderá transmitir as alterações para uma fonte de dados. Mais comumente, você faz isso chamando o método Update de um TableAdapter (ou adaptador de dados). O método percorre cada registro em uma tabela de dados, determina qual tipo de atualização é necessário (atualizar, inserir ou excluir), se houver, e então executa o comando apropriado.

Como ilustração de como as atualizações são feitas, suponha que seu aplicativo use um conjunto de dados que contém uma única tabela de dados. O aplicativo busca duas linhas do banco de dados. Após a recuperação, a tabela de dados carregados na memória será assim:

(RowState)     CustomerID   Name             Status
(Unchanged)    c200         Robert Lyon      Good
(Unchanged)    c400         Nancy Buchanan    Pending

O aplicativo altera o status de Nancy Buchanan para "Preferencial". Como resultado dessa alteração, o valor da propriedade RowState dessa linha muda de Unchanged para Modified. O valor da propriedade RowState para a primeira linha permanece Unchanged. Agora a tabela de dados será assim:

(RowState)     CustomerID   Name             Status
(Unchanged)    c200         Robert Lyon      Good
(Modified)     c400         Nancy Buchanan    Preferred

Agora o aplicativo chama o método Update para transmitir o conjunto de dados para o banco de dados. Por sua vez, o método inspeciona cada linha. Para a primeira linha, o método não transmite instruções SQL para o banco de dados, pois essa linha não foi alterada desde que foi originalmente buscada no banco de dados.

No entanto, para a segunda linha, o método Update invoca automaticamente o comando de dados correto e o transmite para o banco de dados. A sintaxe específica da instrução SQL depende do dialeto do SQL compatível com o armazenamento de dados subjacente. No entanto, as seguintes características gerais da instrução SQL transmitida devem ser observadas:

  • A instrução SQL transmitida é uma instrução UPDATE. O adaptador sabe usar uma instrução UPDATE, pois o valor da propriedade RowState é Modified.

  • A instrução SQL transmitida inclui uma cláusula WHERE, que indica que o destino da instrução UPDATE é a linha na qual CustomerID = 'c400'. Essa parte da instrução SELECT diferencia a linha de destino de todas as outras, pois o CustomerID é a chave primária da tabela de destino. As informações da cláusula WHERE são derivadas da versão original do registro (DataRowVersion.Original), caso os valores necessários para identificar a linha tenham mudado.

  • A instrução SQL transmitida inclui a cláusula SET para definir os novos valores das colunas modificadas.

    Observação

    Se a propriedade UpdateCommand do TableAdapter tiver sido definida como o nome de um procedimento armazenado, o adaptador não construirá uma instrução SQL. Em vez disso, ele invocará o procedimento armazenado com os parâmetros apropriados passados.

Passar parâmetros

Normalmente, você usa parâmetros para passar os valores dos registros que serão atualizados no banco de dados. Quando o método Update do TableAdapter executa uma instrução UPDATE, ele precisa preencher os valores de parâmetro. Ele obtém esses valores da coleção Parameters para o comando de dados apropriado, nesse caso, o objeto UpdateCommand no TableAdapter.

Se você tiver usado as ferramentas do Visual Studio para gerar um adaptador de dados, o objeto UpdateCommand conterá uma coleção de parâmetros que correspondem a cada espaço reservado de parâmetro na instrução.

A propriedade System.Data.SqlClient.SqlParameter.SourceColumn de cada parâmetro aponta para uma coluna na tabela de dados. Por exemplo, a propriedade SourceColumn para os parâmetros au_id e Original_au_id é definida como qualquer coluna na tabela de dados que contenha a ID do autor. Quando o método Update do adaptador é executado, ele lê a coluna ID do autor do registro que está sendo atualizado e preenche os valores na instrução.

Em uma instrução UPDATE, você precisa especificar os novos valores (os que serão gravados no registro), bem como os valores antigos (para que o registro possa ser localizado no banco de dados). Portanto, há dois parâmetros para cada valor: um para a cláusula SET e outro diferente para a cláusula WHERE. Ambos os parâmetros leem dados do registro que está sendo atualizado, mas obtêm versões diferentes do valor da coluna com base na propriedade SourceVersion do parâmetro. O parâmetro da cláusula SET obtém a versão atual, enquanto o parâmetro da cláusula WHERE obtém a versão original.

Observação

Você também pode definir valores na coleção Parameters por conta própria no código, o que normalmente faria em um manipulador de eventos para o evento RowChanging do adaptador de dados.