Conhecer os princípios de Unit of Work
Objetivos de aprendizagem
Após concluir esta unidade, você estará apto a:
- Gerenciar eficazmente suas operações DML e evitar atualizações parciais de banco de dados.
- Entender os recursos e as vantagens da implementação do Apex do padrão.
- Aplicar o padrão Unit of Work ao método de serviço applyDiscount da unidade anterior.
Acompanhar com o Trail Together
Deseja acompanhar um especialista enquanto trabalha nesta etapa? Veja este vídeo que faz parte da série Trail Together.
(Este clipe começa na marca dos 54:05 minutos, caso você queira retroceder e ver o início da etapa novamente.)
Princípios da Unit of Work
Unit of Work é um padrão de design que reduz código repetitivo ao implementar gerenciamento de transações e as sobrecargas de códigos de adesão à massificação da DML por meio do uso extensivo de mapas e listas. Não é um requisito para implementar uma camada de serviço, mas pode ajudar. Vamos mostrar a você um exemplo de antes e depois para explicar como isso funciona.
O padrão Unit of Work usado neste módulo está baseado no padrão descrito por Martin Fowler: “Mantém uma lista de objetos afetada por uma transação de negócios e coordena a escrita de alterações e a resolução de problemas de simultaneidade.”
Na Salesforce Platform, isso quer dizer que o padrão lida com os seguintes casos de uso:
- Registrar atualizações de registros, inserções e exclusões para implementar um requisito de negócios específico
- Registrar relacionamentos de registro para facilitar a inserção de registros filhos ou registros relacionados com menos codificação
- Quando solicitado para escrever (ou confirmar) no banco de dados, massifica todos os registros capturados
- Fazer wrapping de DML em SavePoint, liberando o desenvolvedor de implementar isso todas as vezes para cada método de serviço escrito.
Como implementar um método de serviço sem uma Unit of Work
Para entender melhor o que o padrão Unit of Work tem para oferecer, vamos primeiro analisar o código que precisamos escrever sem usar o padrão Unit of Work em cada método de serviço, aderindo mesmo assim às melhores práticas de design discutidas anteriormente. Precisamos escrever código para um requisito de negócios específico, mas também tê-lo como código clichê para implementar o seguinte:
-
Massificação e otimização de DML – O código pode atualizar alguns ou todos os registros Oportunity ou OpportunityLineItem, dependendo do fluxo de lógica. Ele cria e popula duas listas para manter apenas a leitura de registros que precisam ser atualizados.
-
Tratamento de erros e gerenciamento de transação – Segundo os princípios de design da camada de serviço, deve confirmar todas as alterações ou nenhuma se ocorrer um erro, independentemente se o chamador capturar quaisquer exceções geradas. Lembre-se que a plataforma reverte automaticamente apenas se as exceções não forem tratadas, o que não é desejável desde uma perspectiva de exceção de usuário. É uma boa prática para o código da camada de serviço gerenciar um escopo de transação usando o recurso SavePoint e a semântica try/catch.
O exemplo a seguir usa um recurso SavePoint
para encapsular e fazer wrapping das operações de banco de dados em um método de serviço, conforme as considerações de design. Por quê? Imagine um cenário em que a segunda operação DML falhe. Sem um recurso SavePoint
no método:
- Se o autor da chamada não tratar a exceção, toda a transação, incluindo a primeira operação DML, será revertida, uma vez que esse é o comportamento transacional padrão do Apex.
- Se o chamador capturar a exceção, sem permitir que ela se propague ou sem restaurar um recurso SavePoint, o tempo de execução do Apex confirmará as atualizações nas linhas de oportunidade (primeira instrução DML), causando uma atualização parcial no banco de dados.
Quando não estiver usando o padrão Unit of Work, a melhor prática é lidar com várias operações DML, conforme demonstrado no exemplo. No entanto, como você verá nas próximas seções, o Unit of Work pode tratar disso para você.
public static void applyDiscounts(Set<Id> opportunityIds, Decimal discountPercentage) { // Validate parameters // ... // Query Opportunities and Lines // ... // Update Opportunities and Lines (if present) List<Opportunity> oppsToUpdate = new List<Opportunity>(); List<OpportunityLineItem> oppLinesToUpdate = new List<OpportunityLineItem>(); // Do some work... Decimal factor = 1 - (discountPercentage==null ? 0 : discountPercentage / 100); for(Opportunity opportunity : opportunities) { // Apply to Opportunity Amount if(opportunity.OpportunityLineItems!=null && opportunity.OpportunityLineItems.size()>0) { for(OpportunityLineItem oppLineItem : opportunity.OpportunityLineItems) { oppLineItem.UnitPrice = oppLineItem.UnitPrice * factor; oppLinesToUpdate.add(oppLineItem); } } else { opportunity.Amount = opportunity.Amount * factor; oppsToUpdate.add(opportunity); } } // Update the database SavePoint sp = Database.setSavePoint(); try { update oppLinesToUpdate; update oppsToUpdate; } catch (Exception e) { // Rollback Database.rollback(sp); // Throw exception on to caller throw e; } }
Implementação do Apex do padrão Unit of Work
O restante desta unidade faz referência a uma biblioteca de código aberto do Apex que contém uma implementação do padrão Unit of Work de Martin Fowler. É implementado por meio de uma única classe, fflib_SObjectUnitOfWork, então abra isso em uma outra guia. Na próxima unidade, iremos trabalhar com esta classe mas, por enquanto, vamos entender um pouco mais sobre seus métodos-chave.
Esta classe expõe métodos para permitir que uma instância da classe fflib_SObjectUnitOfWork
capture registros que precisam ser criados, atualizados ou excluídos conforme o código de serviço é executado por meio de métodos de registro. Além disso, o método commitWork encapsula o SavePoint e a convenção try/catch.
A atualização do banco de dados com DML só ocorre quando o método commitWork é chamado. Então, o código de serviço pode chamar os métodos de registro sempre que for preciso, mesmo em loops. Essa abordagem permite que o desenvolvedor se concentre na lógica de negócios e não no código para gerenciar várias listas e mapas.
Por fim, como mostrado no diagrama a seguir, o escopo da Unit of Work é determinado pelo início e fim do seu código de método de serviço. Somente chame o método commitWork quando no escopo do método de serviço.
Para incluir a Unit of Work em seus métodos de código de serviço, siga estas etapas.
- Inicialize uma Unit of Work e use-a para definir o escopo de todo o trabalho pelo método de serviço.
- Garanta que a lógica da camada de serviço registre registros com a Unit of Work quando é executada.
- Chame o método commitWork da Unit of Work para massificar e executar o DML.
O diagrama a seguir mostra as etapas acima e reforça o escopo de cada etapa com relação à execução do código do método de serviço.
Para usar a classe Unit of Work, você precisa construí-la com uma lista dos objetos com que seu código está interagindo. Os objetos precisam estar em ordem de dependência para garantir que os registros pai e filho registrados sejam inseridos pelo método commitWork na ordem correta. Vamos explorar mais sobre como lidar com o relacionamento pai-filho de fflib_SObjectUnitWork na próxima unidade.
fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork( new List<SObjectType> { OpportunityLineItem.SObjectType, Opportunity.SObjectType } );
Como implementar um método de serviço com a Unit of Work
O exemplo a seguir aplica o padrão Unit of Work ao serviço que criamos na unidade anterior. O código que não mudou não é mostrado. Observe que as listas desapareceram e que o SavePoint não tem o código clichê try/catch em seu redor porque é tudo tratado pela classe fflib_SObjectUnitOfWork
public static void applyDiscounts(Set<Id> opportunityIds, Decimal discountPercentage) { // Unit of Work fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork( new List<SObjectType> { OpportunityLineItem.SObjectType, Opportunity.SObjectType } ); // Validate parameters // ... // Query Opportunities and Lines // ... // Update Opportunities and Lines (if present) // ... for(Opportunity opportunity : opportunities) { // Apply to Opportunity Amount if(opportunity.OpportunityLineItems!=null && opportunity.OpportunityLineItems.size()>0) { for(OpportunityLineItem oppLineItem : opportunity.OpportunityLineItems) { oppLineItem.UnitPrice = oppLineItem.UnitPrice * factor; uow.registerDirty(oppLineItem); } } else { opportunity.Amount = opportunity.Amount * factor; uow.registerDirty(opportunity); } } // Commit Unit of Work uow.commitWork(); }
A classe fflib_SObjectUnitOfWork agrega operações DML e faz o wrapping das mesmas em um SavePoint quando o método commitWork é chamado.
Em códigos mais complexos, com várias profundidades e classes, você pode optar por transmitir SObjectUnitOfWork (ou usar um estático). O código chamado pode continuar a registrar suas próprias atualizações de banco de dados, sabendo que o proprietário da Unit of Work, neste caso a camada de serviço, realiza uma única fase de confirmação ou reversão em seu nome.
Recursos
- Padrões corporativos do Apex – repositório GitHub
- Separação de preocupações (Wikipedia)
- Padrões de arquitetura empresarial de Martin Fowler
- Padrões Unit of Work de Martin Fowler
- Como gerenciar sua DML e transações com uma Unit of Work
- Produzir mais com a Unit of Work