Concorrência em LINQ para SQL

Resolução de Conflitos

Os objectos em memória guardam o valor original (o valor obtido da base de dados) e o valor actual (novo valor atribuído pelo utilizador). Como já terá percebido, um conflito ocorre quando o valor original do objecto não corresponde ao valor presente na base de dados, ou seja, na cláusula where vai um parâmetro onde o seu valor não corresponde ao valor presente na base de dados. O processo de resolução de conflitos resume-se basicamente em actualizar os valores dos objectos em memória de modo a deixarem de existir estes conflitos. Os métodos que permitem realizar estas actualizações recebem como parâmetro um RefreshMode, que permite ao programador definir a forma como estes objectos serão actualizados.

Antes de falarmos sobre as formas de resolver os conflitos iremos tentar perceber o que é o enumerado RefreshMode e quais as opções que nos oferece. Iremos falar ainda sobre a propriedade ChangeConflicts, que nos permite obter mais informações sobre os objectos que estão em conflito.

O enumerado RefreshMode

O enumerado RefreshMode permite ao programador definir como os objectos serão actualizados de modo aos conflitos serem resolvidos. Este enumerado disponibiliza 3 opções, KeepChanges, KeepCurrentValues ou OverwriteCurrentValues. Ao utilizarmos RefreshMode.KeepChanges estamos a definir que queremos obter da base de dados para as propriedades da classe todos os dados que foram alterados, excepto se essas propriedades tiverem sido previamente alteradas pelo utilizador. Obteremos da base de dados os dados alterados pelo outro utilizador que não foram alterados por nós. (Serão persistidos por esta ordem de prioridade: os dados alterados pelo utilizador, as alterações obtidas da base de dados ou os valores originalmente obtidos.)

A opção RefreshMode.KeepCurrentValues define que as alterações feitas na base de dados por outros utilizadores serão descartadas e que os dados a serem persistidos são os que estão presentes nos nossos objectos da aplicação. (Serão persistidos por esta ordem de prioridade: os dados alterados pelo utilizador, os valores originalmente obtidos.)

Por fim a utilização de RefreshMode.OverwriteCurrentValues define que os valores a persistir são os valores correntes que estão presentes na base de dados e não os valores que nós temos nos nossos objectos. (Serão persistidos por esta ordem de prioridade: as alterações recebidos da base de dados, os valores originalmente obtidos.)

A Colecção ChangeConflicts

A classe DataContext dispõe de um propriedade que nos permite obter os objectos em conflitos. Essa propriedade tem o nome de ChangeConflicts e retorna uma colecção de objectos do tipo ObjectChangeConflict e representa todas as tabelas onde existem conflitos. Cada instância de ObjectChangeConflict dispões de uma colecção que representa todas as colunas dessa tabela onde existem conflitos. Essa propriedade tem o nome de MemberConflicts e armazena objectos do tipo MemberChangeConflict. Sobre objectos deste tipo podemos consultar informações tais como o valor original, o valor corrente e o valor presente na base de dados.

try
{
  db.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException)
{
  foreach (ObjectChangeConflict tabela in db.ChangeConflicts)
  {
    foreach (MemberChangeConflict coluna in tabela.MemberConflicts)
    {
      object ValorOriginal = coluna.OriginalValue;
      object ValorActual = coluna.CurrentValue;
      object ValorNaBaseDados = coluna.DatabaseValue;
    }
  }
}

Resolver os Conflitos

Agora que já temos mais conhecimentos sobre o enumerado RefreshMode e sobre a colecção ChangeConflicts iremos falar sobre três formas de resolver os conflitos disponíveis na DLINQ. A primeira e a mais simples de todas é usar o método ResolveAll sobre a colecção ChangeConflicts. Desta forma podemos resolver todos usando um RefreshMode como parâmetro. Todos os objectos em conflito serão actualizados usando o mesmo RefreshMode.

try
{
  db.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException)
{
  db.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges);
}

Se desejarmos um comportamento diferente para cada tabela podemos usar o método Resolve sobre cada ObjectChangeConflict. Como já foi referido, a classe DataContext guarda numa colecção os objectos em conflito. Podemos percorrer essa colecção através da propriedade ChangeConflicts e definir o RefreshMode para cada um desses objectos. Cada ObjectChangeConflict representa uma tabela da base de dados em conflito.

try
{
  db.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException)
{
  foreach (ObjectChangeConflict tabela in db.ChangeConflicts)
  {
    tabela.Resolve(RefreshMode.KeepChanges); 
  }
}

Se desejarmos refinar ainda mais o método de resolução podemos usar o método Resolve sobre cada MemberChangeConflict e definir o RefreshMode para cada coluna da tabela. Cada objecto da classe ObjectChangeConflict dispõe de uma propriedade com o nome de MemberChangeConflicts que nos dá acesso a uma colecção de objectos em conflito. Cada objecto desta colecção representa uma coluna de uma tabela da base de dados em conflito.

try
{
  db.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException)
{
  foreach (ObjectChangeConflict tabela in db.ChangeConflicts)
  {
    foreach (MemberChangeConflict coluna in tabela.MemberConflicts)
    {
      coluna.Resolve(RefreshMode.KeepChanges);
      // coluna.Resolve(coluna.OriginalValue);
    }
  }
}

Como o leitor se apercebeu, a forma de resolver os conflitos é bastante simples, podendo o programador decidir até que nível de profundidade pretende ir.

Transacções

O controlo de transacções serve para garantir a integridade dos dados, isso significa que através dele podemos ter a certeza de que todos os dados serão gravados na base de dados. Em caso de eventual falha nalgum momento da gravação todo o processo volta ao estado inicial, sem inserções parciais. Ao ocorrer um erro durante o processo é realizado um rollback e nada é gravado na base de dados. Ao completar o processo com sucesso, a transacção gera um commit e grava os dados na base de dados. Como podemos observar, nos exemplos até agora apresentados, nunca ouve a preocupação de implementar um modelo transaccional de modo a que, na ocorrência de conflitos, os registos gravados antes de o conflito ocorrer voltem ao estado inicial. Esta situação poderia deixar a base de dados inconsistente já que alguns registos seriam gravados e outros não. De modo a resolver este problema, por omissão, a classe DataContext cria uma transacção quando o método SubmitChanges é chamado. Caso ocorra algum conflito é feito rollback automaticamente sobre a transacção.

No modo de transacção implícita é verificado se a chamada ao método SubmitChanges é realizada no contexto de uma transacção ou se a propriedade Transaction da classe DataContext está afectada com uma transacção local iniciada pelo utilizador. Irá utilizar a primeira que encontrar. Caso não encontre nenhuma é iniciada uma nova transacção para executar os comandos SQL gerados. Se todos os comandos correrem com sucesso a DLINQ faz commit à transacção e retorna. Caso exista algum conflito faz rollback.

Como o leitor já percebeu a classe DataContext tem uma propriedade com o nome Transaction. Esta propriedade permite ao programador definir qual a transacção a utilizar nas comunicações com a base de dados. O programador é responsável pela criação, commit, rollback e libertação dos recursos dessa transacção e só pode utiliza-la no contexto da própria instância de DataContext.

try
{
  dataContext.Connection.Open();
  dataContext.Transaction = dataContext.Connection.BeginTransaction();
  dataContext.SubmitChanges(ConflictMode.ContinueOnConflict);
  dataContext.Transaction.Commit();
}
catch (ChangeConflictException)
{
  dataContext.Transaction.Rollback();
}

Outra opção e a mais recomendada é utilizar um objecto da classe TransactionScope. Este tipo de transacções adapta-se automaticamente aos objectos que manipula. Se fizer apenas acesso a uma base de dados utilizará uma transacção simples. Caso faça acesso a mais do que uma base de dados utilizará uma transacção distribuída. Outra vantagem é que não necessário iniciar ou fazer rollback a transacções deste tipo, apenas é necessário fazer commit. A operação de commit é realizada chamado o método Complete da classe TransactionScope e será feito o rollback automático da transacção caso o método Complete não seja chamado.

using (TransactionScope scope = new TransactionScope())
{
  dataContext.SubmitChanges(ConflictMode.ContinueOnConflict);
  scope.Complete();
}

O leitor repare que se existir algum conflito de concorrência o método SubmitChanges irá lançar uma excepção fazendo com que o método Complete não seja chamado e a transacção sofrerá um rollback automático.

Implementar Concorrência Pessimista

Por vezes poderá ser necessário utilizar um esquema de concorrência pessimista. Um esquema de concorrência pessimista bloqueia os recursos durante a edição. Neste esquema de concorrência, os recursos permanecem bloqueados até ao fim da edição dos mesmos. A principal vantagem deste esquema é a garantia de que nenhum outro utilizador tem acesso de escrita aos recursos bloqueados. Como desvantagens temos principalmente duas. Para manter os recursos bloqueados é necessária uma ligação permanente à base de dados. Esta ligação permanente pode ser um recurso crítico como, por exemplo, aplicações web com muitos utilizadores. O facto de os recursos ficarem bloqueados pode ser uma grande vantagem mas também poderá ser uma grande desvantagem. Imaginemos que um utilizador faz uma pausa para café ou para almoço. Os recursos ficam bloqueados durante todo esse tempo, mesmo que não tenham sido alterados ainda. A DLINQ fornece-nos uma forma muito simples de implementar este esquema de concorrência. Basta juntar a leitura e a escrita dos dados na mesma transacção. Para realizar esta tarefa vamos usar a classe TransactionScope e um bloco using, onde no fim do mesmo será feita a chamada ao método Complete da TransacionScope.

DatabaseDataContext dc = new DatabaseDataContext();
using (TransactionScope t = new TransactionScope())
{
  Cliente cliente = (from c in dc.Clientes
                     where c.Nome == "Vitor"
                     select c).Single();
  //ou
  //Cliente cliente = dc.Clientes.Single(c => c.Nome == "Vitor");
 
  cliente.Cidade = "Torres Vedras";
  dc.SubmitChanges();
  t.Complete();
}

Usando o esquema de concorrência pessimista não existem conflitos para resolver visto que os recursos são bloqueado durante a transacção e portanto nenhum outro utilizador tem permissão os modificar.

Bibliografia