A razão pela qual digo que as transações não pertencem à camada de modelo é basicamente esta:
Os modelos podem chamar métodos em outros modelos.
Se um modelo tenta iniciar uma transação, mas não sabe se seu chamador já iniciou uma transação, então o modelo deve condicionalmente iniciar uma transação, conforme mostrado no exemplo de código em @Bubba's answer . Os métodos do modelo têm que aceitar um sinalizador para que o chamador possa dizer se ele tem permissão para iniciar sua própria transação ou não. Ou então o modelo precisa ter a capacidade de consultar o estado "em uma transação" de seu chamador.
public function setPrivacy($privacy, $caller){
if (! $caller->isInTransaction() ) $this->beginTransaction();
$this->privacy = $privacy;
// ...action code..
if (! $caller->isInTransaction() ) $this->commit();
}
E se o chamador não for um objeto? Em PHP, pode ser um método estático ou simplesmente um código não orientado a objetos. Isso fica muito confuso e leva a muitos códigos repetidos nos modelos.
Também é um exemplo de Acoplamento de controle , que é considerado ruim porque o chamador precisa saber algo sobre o funcionamento interno do objeto chamado. Por exemplo, algumas dos métodos do seu modelo podem ter um parâmetro $transactional, mas outros métodos podem não ter esse parâmetro. Como o chamador deve saber quando o parâmetro é importante?
// I need to override method's attempt to commit
$video->setPrivacy($privacy, false);
// But I have no idea if this method might attempt to commit
$video->setFormat($format);
A outra solução que vi sugerida (ou mesmo implementada em alguns frameworks como o Propel) é fazer
beginTransaction()
e commit()
no-ops quando o DBAL sabe que já está em uma transação. Mas isso pode levar a anomalias se o seu modelo tentar fazer o commit e descobrir que ele não está realmente confirmado. Ou tenta reverter e essa solicitação é ignorada. Já escrevi sobre essas anomalias antes. O compromisso que sugeri é que Os modelos não sabem sobre transações . O modelo não sabe se sua solicitação para
setPrivacy()
é algo que deve ser confirmado imediatamente ou é parte de um quadro maior, uma série mais complexa de alterações que envolvem vários modelos e deve somente ser confirmado se todas essas mudanças forem bem-sucedidas. Esse é o objetivo das transações. Portanto, se os Modelos não sabem se podem ou devem iniciar e confirmar sua própria transação, quem sabe? GRASP inclui um padrão de controlador que é uma classe não UI para um caso de uso, e a ela é atribuída a responsabilidade de criar e controlar todas as partes para realizar esse caso de uso. Os controladores sabem sobre as transações porque esse é o lugar onde todas as informações são acessíveis sobre se o caso de uso completo é complexo e requer que várias alterações sejam feitas em Modelos, em uma transação (ou talvez em várias transações).
O exemplo sobre o qual escrevi antes, é iniciar uma transação no
beforeAction()
método de um controlador MVC e confirme-o no afterAction()
método, é uma simplificação . O Controlador deve ser livre para iniciar e confirmar quantas transações forem necessárias logicamente para concluir a ação atual. Ou, às vezes, o Controlador pode abster-se do controle de transação explícito e permitir que os Modelos autorizem cada alteração. Mas o ponto é que a informação sobre quais transacções são necessárias é algo que os Modelos não sabem - eles têm que ser informados (na forma de um parâmetro $transacional) ou então consultar o chamador, o que teria que delegar a questão até a ação do Controlador de qualquer maneira.
Você também pode criar uma Camada de serviço de classes que cada uma sabe como executar casos de uso tão complexos e se deve incluir todas as alterações em uma única transação. Dessa forma, você evita muito código repetido. Mas não é comum que aplicativos PHP incluam uma camada de serviço distinta; a ação do Controlador geralmente coincide com uma Camada de Serviço.