Unit of Work Concurrency, como é tratado?

5

Estou lendo algumas informações sobre o padrão da unidade de trabalho. Há uma coisa que não está muito clara para mim: o que acontecerá quando você solicitar alguns registros em um thread (1) e outro thread (2) remover um desses registros e confirmar a alteração?

O registro removido no primeiro segmento recebe um estado "removido"? O problema com esta abordagem é que o programa tem que verificar toda vez que ele quiser usar um registro quando ele ainda existir. Ele também tem que bloquear o registro quando ele quiser usá-lo.

Eu não consigo pensar em nenhuma outra abordagem, como esse problema é resolvido na prática?

    
por Laurence 22.11.2014 / 11:48
fonte

1 resposta

5

Em geral, uma unidade de trabalho corresponde a uma transação de banco de dados. Então, a partir dessa perspectiva:

BEGIN TRANSACTION

-- do stuff

COMMIT TRANSACTION

Bancos de dados são muito avançados em como eles lidam com isso. Qualquer dado que você ler depois da instrução BEGIN TRANSACTION é "bloqueado" para qualquer outro segmento (note que você pode sobrescrever este comportamento na sua instrução SELECT com algumas opções de bloqueio explícitas, mas estou ignorando isso aqui). Todos os outros segmentos que desejam acessar os mesmos dados serão bloqueados até que essa transação seja confirmada ou revertida. É por isso que é importante fazer o trabalho dentro da transação o mais breve possível.

Na prática, em um aplicativo que usa o padrão de repositório, há dois cenários típicos:

  1. Ler-modificar-escrever tudo dentro de uma transação
  2. Leia em uma transação somente leitura, modificar-gravar em uma transação posterior de leitura / gravação

Para o cenário 1, imagine que você tenha um botão chamado "Excluir registros antigos". O botão (mais ou menos) faria isso:

using(var uow = GetUnitOfWork())
{
    var repository = uow.GetRepository<MyRecord>();
    var oldRecords = repository.Entities
        .Where(x => x.Old)
        .ToList();
    foreach(var record in oldRecords)
    {
        repository.Delete(record);
    }
    uow.Commit(); // sometimes this is optional (default behavior could be to commit)
}

Como toda a leitura / gravação é feita dentro de uma unidade de trabalho, que está dentro de uma transação de banco de dados, se duas pessoas clicaram nesse botão ao mesmo tempo, não há problema de simultaneidade porque o banco de dados serializará as solicitações .

O cenário 2 é muito mais comum e mais complicado. Imagine que você tenha uma tela de edição, como "editar funcionário". Normalmente, você não deseja manter uma transação aberta o tempo todo em que o usuário tem a tela de edição aberta, portanto, primeiro, você obtém as informações do funcionário e as processa na tela. Em seguida, o usuário faz alterações e clica no botão "Salvar". É razoável assumir que outra pessoa também estava editando esse funcionário ao mesmo tempo, então ambos começaram com a mesma cópia do funcionário, fizeram duas alterações diferentes e depois clicaram em "Salvar" (um deles clicará primeiro).

Se, dentro da rotina de salvamento, você recarregar os dados do funcionário, modificar apenas os dados que foram alterados e depois confirmá-los, contanto que os dois usuários mudem algo diferente (o usuário 1 alterou o nome e o usuário 2 alteraram o sobrenome), então é possível que as duas alterações sejam feitas. No entanto, se você gravar sua rotina de salvaguarda de modo a copiar todos os dados da sua tela de edição para sua entidade de funcionário e salvá-la, as alterações do usuário 2 substituirão as alterações do usuário 1 (porque o usuário 2 ainda possui o nome original em sua tela) ). Além disso, se o usuário 1 e o usuário 2 alterarem o mesmo campo, a alteração do usuário 2 sempre substituirá a alteração do usuário 1. Este não é o caminho certo para fazê-lo.

Na prática, usando algo como Entity Framework ou NHibernate, há dois conceitos: uma sessão e uma transação. Quando sua tela de edição é criada, você também cria uma sessão. A sessão controla todos os dados carregados do banco de dados durante essa sessão (incluindo os valores originais). A sessão é descartada quando a tela é fechada (estou considerando um aplicativo de desktop aqui, não um aplicativo da web). Quando você clica em salvar, ele inicia uma transação, mas, em vez de recarregar os dados do banco de dados, basta alterar a entidade original do funcionário que você carregou do banco de dados e depois confirmar a transação. A estrutura então faz uma declaração UPDATE como essa (suponha que a tabela de funcionários tenha apenas 3 colunas: "id", "first_name", "last_name"):

BEGIN TRANSACTION

UPDATE Employee
SET first_name = 'new first name'
WHERE id = 123
    AND first_name = 'old first name'
    AND last_name = 'last name'

COMMIT TRANSACTION

Isso é chamado de simultaneidade otimista. O que isso significa é que se alguém alterou esse registro entre quando você carregou a tela de edição e quando você clicou em salvar, a atualização falhará (e o banco de dados indicará isso dizendo "0 registros atualizados" em vez de "1 registro atualizado") . A estrutura detectará esse erro e geralmente lançará uma exceção (o NHibernate lança um StaleObjectStateException ). Cabe a você lidar com essa exceção. Honestamente, apenas mostro uma mensagem do tipo "alguém editou este registro ao mesmo tempo, então você terá que fechar essa tela e tentar novamente".

Observe que há uma otimização que você pode fazer. Você pode adicionar uma nova coluna à sua tabela chamada VERSION . Em seguida, você pode configurar sua estrutura para usar essa coluna em vez de verificar todas as colunas da tabela para ver se elas foram alteradas, para que a atualização seja assim:

BEGIN TRANSACTION

UPDATE Employee
SET first_name = 'new first name', 
    version = 2
WHERE id = 123
    AND version = 1

COMMIT TRANSACTION

Então, você está confiando no banco de dados para garantir que as alterações sejam atômicas (indivisíveis), mas você precisa fazer algum trabalho extra para lidar com o elemento humano, já que as pessoas precisam obter uma cópia dos dados, pensar sobre isso um tempo e fazer uma mudança, e os dados podem ter mudado durante esse tempo. Cabe a você como programador como você quer lidar com conflitos como esse.

    
por 22.11.2014 / 13:12
fonte