Introdução
A correcta gestão de acessos concorrentes e transacções são tarefas muito importantes numa aplicação. A arquitectura da DLINQ (LINQ para SQL) já nos fornece uma implementação base preocupada com estes aspectos, mas também permite que o programador personalize essa implementação de modo a adaptá-la às necessidades da sua aplicação.
Não sendo o objectivo deste artigo explicar a base do funcionamento da DLINQ é necessário explicar alguns conceitos mais básicos do seu funcionamento de modo a enquadrar os leitores com menos contacto com esta linguagem/tecnologia. De modo a cumprir esse objectivo iremos falar, ligeiramente mais à frente, sobre a classe DataContext
.
Nesta tecnologia, a técnica utilizada para detectar e resolver conflitos de concorrência tem o nome de concorrência optimista. O nome resulta da crença em que a probabilidade das alterações produzidas por uma transacção interferir com outra transacção é baixa. A DLINQ utiliza um esquema de dados desconectados, onde cada utilizador pode fazer as alterações que quiser na sua cópia dos dados (instância de DataContext
). No momento em que o programa tentar propagar essas alterações para a base de dados é realizada a detecção de conflitos. A detecção de conflitos consiste basicamente em verificar se os dados presentes na base de dados foram modificados desde que a aplicação cliente fez o seu último acesso. Se for detectado algum conflito, o programa necessita de saber como resolvê-lo, nomeadamente se escreve por cima os novos dados, se descarta os novos dados ou se faz alguma operação entre eles. A detecção de conflitos é uma das funcionalidades da classe DataContext
. Quando o programador tenta actualizar a base de dados chamando o método SubmitChanges
da classe DataContext
é automaticamente realizada uma detecção de conflitos.
Sempre que existe algum conflito é lançada a excepção ChangeConflictException
, portanto todas as chamadas ao método SubmitChanges
devem estar protegidas por um bloco try/catch
.
Além da informação sobre a origem e a mensagem, a classe ChangeConflictException
oferece ainda a possibilidade de resolver os conflitos de concorrência através de utilização dos enumerados RefreshMode
e ConflictMode
como veremos mais à frente.
De modo a tornar o código mais breve e legível muitos dos exemplos de DLINQ disponíveis na Internet ou em livros não definem um modo de detectar e resolver conflitos de concorrência, tenha em atenção que em código para produção deverá sempre defini-los.
A classe DataContext
Na base da DLINQ está a classe DataContext
. É esta classe que fornece a maioria dos serviços/funcionalidades e sobre a qual são realizadas a maioria das operações. Como principais funcionalidades disponibilizadas por esta classe temos:
- Gestão da ligação à base de dados.
- Mapeamento entre os objectos e a base de dados.
- Tradução e execução das queries.
- Gestão da identidade dos objectos.
- Gestão de alterações nos objectos.
- Processador de alterações.
- Gestão da integridade transaccional.
Para uma melhor compreensão deste artigo importa saber que as tabelas da base de dados são mapeadas em tipos (classes) do lado da linguagem de programação e que as colunas das tabelas são suportadas através de propriedades. Sempre que forem referidas propriedades será neste contexto, representação de colunas de uma tabela.
Acessos ReadOnly
Vamos começar por falar de uma situação que, apesar de não gerar conflitos de concorrência, pode ocorrer várias vezes na vida de uma aplicação. Muitas vezes pretende-se aceder a uma base de dados apenas para consultar os seus dados sem que se pretenda alterá-los num futuro próximo. Um exemplo disso podem ser os valores a apresentar numa ComboBox
, como por exemplo uma lista de países do mundo. A classe DataContext
controla as modificações feitas aos valores das propriedades que representam os campos das tabelas da base de dados mantendo em memória o valor original e o valor actual. A manutenção desses dois valores e o processo de detectar alterações tem um peso computacional. A classe DataContext
controla ainda a identidade dos objectos, cada vez que é realizada uma consulta á base de dados, o serviço de gestão de identidade da classe DataContext
verifica se um objecto com a mesma identidade já foi devolvido numa consulta anterior. Se assim for, a DataContext
irá retornar o valor armazenado na cache interna em vez de o obter da base de dados. Esta gestão de identidade também tem um peso computacional. No caso de pretender apenas dados para leitura pode suprimir estes pesos computacionais e obter daí um aumento de performance na aplicação. Para obter este efeito deverá ser afectada a propriedade ObjectTrackingEnabled
da DataContext
com falso. Esta afectação indica à framework que não precisa de detectar alterações nos dados nem fazer gestão de identidade.
DatabaseDataContext dataContext = new DatabaseDataContext(); dataContext.ObjectTrackingEnabled = false;
O leitor repare que se fizer uma chamada ao método SubmitChanges
será gerada uma excepção visto que não existem alterações a submeter. Será ainda lançada uma excepção caso a propriedade seja afectada com falso após a execução de uma query.
Ao afectar ObjectTrackingEnabled
com falso o valor da propriedade DeferredLoadingEnabled
será ignorado e inferido como falso.
Detecção de Conflitos
Existem dois métodos para realizar a detecção de conflitos de concorrência. Se existir alguma propriedade marcada com o atributo IsVersion
a verdadeiro, a detecção de conflitos é realizada apenas com base na chave primária da tabela e no valor dessa coluna. Se não existir nenhuma propriedade com o atributo IsVersion
como verdadeiro, a DLINQ permite ao programador definir quais as colunas que participam na detecção de conflitos através do atributo UpdateCheck
das propriedades.
O atributo IsVersion
especifica se uma propriedade que representa uma coluna da base de dados contém um número de versão ou um timestamp do registo. As definições de UpdateCheck
da classe serão ignoradas na presença de uma propriedade marcada com IsVersion
a verdadeiro.
De modo a percebermos melhor a forma como os conflitos são detectados vamos tentar perceber como está implementado. Quando é realizada a chamada ao método SubmitChanges
é gerado código SQL para realizar a persistência dos dados na base de dados. Quando for necessário realizar uma actualização dos dados na base de dados não é enviado apenas a chave primária da tabela na cláusula where
mas também todas as colunas que irão participar na detecção de conflitos. A detecção de conflitos é realizada enviando na cláusula where
os valores originais das colunas, detectando-se assim eventuais mudanças. Nada melhor que um exemplo para ficarmos realmente a perceber o comportamento. Vamos imaginar que temos uma tabela Cliente com dois campos: Nome (chave primária) e Cidade.
Como operação iremos actualizar o valor de Cidade
do cliente Vítor
para Torres Vedras
. Como podemos observar, a detecção de conflitos é realizada enviando na cláusula where
os valores originais:
UPDATE [dbo].[Cliente] SET [Cidade] = @p2 WHERE ([Nome] = @p0) AND ([Cidade] = @p1) -- @p0: Input NChar (Size = 10; Prec = 0; Scale = 0) [Vítor] -- @p1: Input NChar (Size = 10; Prec = 0; Scale = 0) [Lisboa] -- @p2: Input NChar (Size = 10; Prec = 0; Scale = 0) [Torres Vedras]
Para exemplificar a detecção de conflitos usando uma coluna de versão/timestamp foi adicionado um campo com o nome Version
à tabela e definido o atributo IsVersion
como verdadeiro na propriedade da DataContext
que o representa.
Como podemos observar no exemplo seguinte a detecção é agora realizada utilizando apenas na cláusula where
a chave primária e a propriedade marcada como IsVersion
.
UPDATE [dbo].[Cliente] SET [Cidade] = @p2 WHERE ([Nome] = @p0) AND ([Version] = @p1) SELECT [t1].[Version] FROM [dbo].[Cliente] AS [t1] WHERE ((@@ROWCOUNT) > 0) AND ([t1].[Nome] = @p3) -- @p0: Input NChar (Size = 10; Prec = 0; Scale = 0) [Vítor] -- @p1: Input Int (Size = 0; Prec = 0; Scale = 0) [1] -- @p2: Input NChar (Size = 10; Prec = 0; Scale = 0) [Torres Vedras] -- @p3: Input NChar (Size = 10; Prec = 0; Scale = 0) [Vítor]
O atributo UpdateCheck
Como já vimos, podemos realizar a detecção de conflitos através do atributo UpdateCheck
. Exemplo:
[Column(DbType = "nvarchar(50)",UpdateCheck = UpdateCheck.WhenChanged)] public string Nome;
Apesar de, na maioria das situações, a melhor opção ser informar o utilizador que existe um conflito de concorrência e disponibilizar mecanismos para a resolver, podem existir cenários em que a concorrência não é uma preocupação. Neste caso podemos simplesmente ignorar qualquer alteração concorrente e efectuar sempre a actualização dos registos, ficando gravado na base de dados a ultima actualização submetida. Esta opção pode implementada atribuindo UpdateCheck.Never
em todas as propriedades. Existem casos em que estarem dois utilizadores a alterar colunas diferentes da mesma linha não levanta problemas. Para implementar esta situação deverá definir o atributo UpdateCheck
como WhenChanged
.
Por omissão as propriedades são consideradas com estando marcadas como UpdateCheck.Always
, o que significa que irão sempre participar na detecção de conflitos, independentemente de terem sofrido alterações após a última leitura da base de dados. Ter todas as colunas a participar nessa detecção pode ser bastante penalizador para o desempenho da aplicação. O programador deverá rever este atributo em todas as propriedades de modo a apenas deixar marcadas as propriedades vitais para o correcto funcionamento da aplicação. Lembra-se de termos falado há pouco que as detecções eram feitas enviando os valores originais na cláusula where
? Ora bem, se a propriedade tiver o atributo UpdateCheck
definido como Always
, o valor original da mesma irá sempre fazer parte da cláusula where
. Se o valor de UpdateCheck
estiver definido como WhenChanged
essa propriedade irá fazer parte da cláusula where
apenas se o valor corrente for diferente do original, ou seja, tiver sido modificado. No caso do atributo UpdateCheck
estar definido como Never
, essa coluna não estará presente na cláusula where
, ou seja, não fará parte da detecção de conflitos .
O parâmetro ConflictMode
O método SubmitChanges
aceita como parâmetro uma opção do enumerado ConflictMode
, que tem dois valores possíveis: ContinueOnConflict
e FailOnFirstConflict
. A opção ContinueOnConflict
, define que todas as actualizações serão tentadas, os conflitos que ocorrerem serão reunidos numa colecção e retornados no fim do processo. A opção FailOnFirstConflict
, que é a opção por omissão, define que as actualizações deverão parar imediatamente após o primeiro conflito.
try { dataContext.SubmitChanges(ConflictMode.ContinueOnConflict); } catch (ChangeConflictException){ (...) }
O leitor tenha em atenção que ao utilizar o modo FailOnFirstConflict
seria de esperar que todas as alterações á base de dados realizadas antes do primeiro conflito acorrer tivessem sucesso. Este comportamento poderia deixar a base de dados inconsistente dado que apenas parte dos dados teriam sido actualizados. Falaremos em transacções mais à frente neste artigo mas é importante perceber nesta fase que, por omissão, o DataContext
cria uma transacção sempre que o SubmitChanges
é chamado. Se for lançada uma excepção ocorre um rollback automático da transacção.