La mejor manera de usar la anotación Spring Transactional

Introducción

En este artículo, le mostraré la mejor manera de usar la anotación Spring Transactional.

Esta es una de las mejores prácticas que apliqué al desarrollar RevoGain , una aplicación web que le permite calcular las ganancias que obtuvo al operar con acciones, materias primas o criptomonedas usando Revolut .

Anotación transaccional de primavera

Desde la versión 1.0, Spring ofreció soporte para la gestión de transacciones basada en AOP que permitió a los desarrolladores definir los límites de las transacciones de forma declarativa. 

Muy poco después, en la versión 1.2, Spring agregó soporte para la 

anotación , lo que facilitó aún más la configuración de los límites de transacción de las unidades de trabajo comerciales.

La 

@Transactional
anotación proporciona los siguientes atributos:

  • value
    transactionManager
    – estos atributos se pueden usar para proporcionar una 
    TransactionManager
    referencia que se utilizará al manejar la transacción para el bloque anotado
  • propagation
    – define cómo se propagan los límites de la transacción a otros métodos que serán llamados directa o indirectamente desde dentro del bloque anotado. La propagación predeterminada es 
    REQUIRED
    y significa que se inicia una transacción si ya no hay ninguna transacción disponible. De lo contrario, la transacción en curso será utilizada por el método de ejecución actual.
  • timeout
    timeoutString
    – definir el número máximo de segundos que el método actual puede ejecutar antes de lanzar un
    TransactionTimedOutException
  • readOnly
    – define si la transacción actual es de solo lectura o de lectura y escritura.
  • rollbackFor
    rollbackForClassName
    – definir una o más 
    Throwable
    clases para las cuales se retrotraerá la transacción actual. De forma predeterminada, una transacción se retrotrae si arroja un 
    RuntimException
    o un 
    Error
    , pero no si arroja un marcado 
    Exception
    .
  • noRollbackFor
    noRollbackForClassName
    – definir una o más 
    Throwable
    clases para las que no se revertirá la transacción actual. Normalmente, usaría estos atributos para una o más 
    RuntimException
    clases para las que no desea revertir una transacción determinada.

¿A qué capa pertenece la anotación Spring Transactional?

La 

@Transactional
anotación pertenece a la capa de servicio porque es responsabilidad de la capa de servicio definir los límites de la transacción.

No lo use en la capa web porque esto puede aumentar el tiempo de respuesta de la transacción de la base de datos y hacer que sea más difícil proporcionar el mensaje de error correcto para un error de transacción de base de datos determinado (p. ej., consistencia, interbloqueo, adquisición de bloqueo, bloqueo optimista).

La capa DAO (Objeto de acceso a datos) o Repositorio requiere una transacción a nivel de aplicación, pero esta transacción debe propagarse desde la capa de Servicio.

La mejor manera de usar la anotación Spring Transactional

En la capa de servicio, puede tener servicios relacionados con la base de datos y no relacionados con la base de datos. Si un caso de uso comercial determinado necesita mezclarlos, como cuando tiene que analizar una declaración determinada, crear un informe y guardar algunos resultados en la base de datos, es mejor si la transacción de la base de datos se inicia lo más tarde posible.

Por esta razón, podría tener un servicio de puerta de enlace no transaccional, como el siguiente 

RevolutStatementService
:

123456789101112131415dieciséis1718192021222324252627282930313233343536373839404142
@Service
public
class
RevolutStatementService {
 
    
@Transactional
(propagation = Propagation.NEVER)
    
public
TradeGainReport processRevolutStocksStatement(
            
MultipartFile inputFile,
            
ReportGenerationSettings reportGenerationSettings) {
        
return
processRevolutStatement(
            
inputFile,
            
reportGenerationSettings,
            
stocksStatementParser
        
);
    
}
    
 
    
private
TradeGainReport processRevolutStatement(
            
MultipartFile inputFile,
            
ReportGenerationSettings reportGenerationSettings,
            
StatementParser statementParser
    
) {
        
ReportType reportType = reportGenerationSettings.getReportType();
        
String statementFileName = inputFile.getOriginalFilename();
        
long
statementFileSize = inputFile.getSize();
 
        
StatementOperationModel stocksStatementModel = statementParser.parse(
            
inputFile,
            
reportGenerationSettings.getFxCurrency()
        
);
        
int
statementChecksum = stocksStatementModel.getStatementChecksum();
        
TradeGainReport report = generateReport(stocksStatementModel);
 
        
if
(!operationService.addStatementReportOperation(
            
statementFileName,
            
statementFileSize,
            
statementChecksum,
            
reportType.toOperationType()
        
)) {
            
triggerInsufficientCreditsFailure(report);
        
}
 
        
return
report;
    
}
}

El 

processRevolutStocksStatement
método no es transaccional y, por esta razón, podemos usar la 
Propagation.NEVER
estrategia para asegurarnos de que este método nunca se llame desde una transacción activa.

Por lo tanto, 

statementParser.parse
el 
generateReport
método y el se ejecutan en un contexto no transaccional, ya que no queremos adquirir una conexión de base de datos y mantenerla necesariamente cuando solo tenemos que ejecutar el procesamiento a nivel de aplicación.

Solo el 

operationService.addStatementReportOperation
requiere ejecutarse en un contexto transaccional, y por esta razón, el 
addStatementReportOperation
usa la 
@Transactional
anotación:

1234567891011121314
@Service
@Transactional
(readOnly = 
true
)
public
class
OperationService {
 
    
@Transactional
(isolation = Isolation.SERIALIZABLE)
    
public
boolean
addStatementReportOperation(
        
String statementFileName,
        
long
statementFileSize,
        
int
statementChecksum,
        
OperationType reportType) {
        
 
        
...
    
}
}

Tenga en cuenta que 

addStatementReportOperation
anula el nivel de aislamiento predeterminado y especifica que este método se ejecuta en una 
SERIALIZABLE
transacción de base de datos.

Otra cosa que vale la pena señalar es que la clase está anotada con 

@Transactional(readOnly = true)
, lo que significa que, de manera predeterminada, todos los métodos de servicio usarán esta configuración y se ejecutarán en una transacción de solo lectura a menos que el método anule la configuración transaccional usando su propia 
@Trsnactional
definición.

Para los servicios transaccionales, es una buena práctica establecer el 

readOnly
atributo 
true
en el nivel de clase y anularlo por método para los métodos de servicio que necesitan escribir en la base de datos.

Por ejemplo, 

UserService
utiliza el mismo patrón:

123456789101112131415
@Service
@Transactional
(readOnly = 
true
)
public
class
UserService 
implements
UserDetailsService {
 
    
@Override
    
public
UserDetails loadUserByUsername(String username)
        
throws
UsernameNotFoundException {
        
...
    
}
    
 
    
@Transactional
    
public
void
createUser(User user) {
        
...
    
}
}

El 

loadUserByUsername
utiliza una transacción de sólo lectura, y ya que estamos utilizando Hibernate, Spring realiza algunas optimizaciones de sólo lectura, así .

Por otro lado, 

createUser
tiene que escribir en la base de datos. Por lo tanto, anula el 
readOnly
valor del atributo con la configuración predeterminada dada por la 
@Transactional
anotación, que es 
readOnly=false
, por lo tanto, hace que la transacción sea de lectura y escritura.

Otra gran ventaja de dividir los métodos de lectura-escritura y solo lectura es que podemos enrutarlos a diferentes nodos de la base de datos, como se explica en este artículo .

Enrutamiento de transacciones de lectura, escritura y solo lectura con Spring

De esta forma, podemos escalar el tráfico de solo lectura aumentando la cantidad de nodos de réplica.

Impresionante, ¿verdad?

Conclusión

La anotación Spring Transactional es muy útil cuando se trata de definir los límites de transacción de los métodos comerciales.

Si bien los valores de atributos predeterminados se eligieron correctamente, es una buena práctica proporcionar configuraciones de nivel de clase y nivel de método para dividir los casos de uso entre casos de uso no transaccionales, transaccionales, de solo lectura y de lectura y escritura.