Gestion efficace des transactions au printemps

Bonne journée à tous!

Eh bien, la fin du mois est toujours intense, et il ne nous reste qu'un jour avant le début du deuxième volet du cours «Developer on Spring Framework» - un cours merveilleux et intéressant enseigné par le tout aussi beau et en colère Yuri (comme certains étudiants l'appellent pour le niveau d'exigences en DZ), regardons donc un autre matériel que nous avons préparé pour vous.

Allons-y.

Présentation

La plupart du temps, les développeurs n'attachent pas d'importance à la gestion des transactions. En conséquence, soit la majeure partie du code doit être réécrite ultérieurement, soit le développeur implémente la gestion des transactions sans savoir comment cela devrait réellement fonctionner ni quels aspects devraient être utilisés spécifiquement dans leur cas.

Un aspect important de la gestion des transactions est de déterminer les limites correctes d'une transaction, quand une transaction doit commencer et quand se terminer, quand les données doivent être ajoutées à la base de données et quand elles doivent être pompées (en cas d'exception).



L'aspect le plus important pour les développeurs est de comprendre comment implémenter au mieux la gestion des transactions dans une application. Examinons donc les différentes options.

Méthodes de gestion des transactions

Les transactions peuvent être gérées de la manière suivante:

1. Contrôle du programme en écrivant du code personnalisé

Il s'agit d'une ancienne méthode de gestion des transactions.

EntityManagerFactory factory = Persistence.createEntityManagerFactory("PERSISTENCE_UNIT_NAME"); EntityManager entityManager = entityManagerFactory.createEntityManager(); Transaction transaction = entityManager.getTransaction() try { transaction.begin(); someBusinessCode(); transaction.commit(); } catch(Exception ex) { transaction.rollback(); throw ex; } 

Avantages :

  • Les limites des transactions sont évidentes dans le code.

Inconvénients :

  • Il est répétitif et sujet aux erreurs.
  • Toute erreur peut avoir un très gros impact.
  • Vous devez écrire un grand nombre de modèles. Si vous souhaitez appeler une autre méthode à partir de cette méthode, vous devez à nouveau la contrôler à partir du code.

2. Utilisation de Spring pour la gestion transactionnelle

Spring prend en charge deux types de gestion des transactions

1. Gestion des transactions logicielles : vous devez gérer les transactions via la programmation. Cette méthode est suffisamment flexible, mais difficile à maintenir.

2. Gestion déclarative des transactions : vous séparez la gestion des transactions de la logique métier. Vous utilisez uniquement des annotations dans la configuration basée sur XML pour la gestion des transactions.

Nous vous recommandons fortement d'utiliser des transactions déclaratives. Si vous souhaitez connaître les raisons, poursuivez votre lecture, sinon accédez directement à la section Gestion déclarative des transactions si vous souhaitez implémenter cette option.

Examinons maintenant chaque approche en détail.

2.1. Gestion des transactions programmatiques:

Le framework Spring fournit deux outils pour la gestion des transactions programmatiques.

a. Utilisation de TransactionTemplate (recommandé par l'équipe Spring):

Voyons comment implémenter ce type à l'aide de l'exemple de code ci-dessous (extrait de la documentation Spring avec quelques modifications)

Notez que les extraits de code sont extraits de Spring Docs.

Fichier Xml contextuel:

 <!-- Initialization for data source --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/TEST"/> <property name="username" value="root"/> <property name="password" value="password"/> </bean> <!-- Initialization for TransactionManager --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <!-- Definition for ServiceImpl bean --> <bean id="serviceImpl" class="com.service.ServiceImpl"> <constructor-arg ref="transactionManager"/> </bean> 

Classe de Service :

 public class ServiceImpl implements Service { private final TransactionTemplate transactionTemplate; //       PlatformTransactionManager public ServiceImpl(PlatformTransactionManager transactionManager) { this.transactionTemplate = new TransactionTemplate(transactionManager); } //       ,   ,    //       xml  this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED); this.transactionTemplate.setTimeout(30); //30  ///    public Object someServiceMethod() { return transactionTemplate.execute(new TransactionCallback() { //         public Object doInTransaction(TransactionStatus status) { updateOperation1(); return resultOfUpdateOperation2(); } }); }} 

S'il n'y a pas de valeur de retour, utilisez la classe TransactionCallbackWithoutResult pratique avec une classe anonyme, comme indiqué ci-dessous:

 transactionTemplate.execute(new TransactionCallbackWithoutResult() { protected void doInTransactionWithoutResult(TransactionStatus status) { updateOperation1(); updateOperation2(); } }); 

  • Les instances de la classe TransactionTemplate sont thread-safe, donc tous les états de dialogue ne sont pas pris en charge.
  • TransactionTemplate instances TransactionTemplate prennent néanmoins en charge l'état de configuration, donc si une classe doit utiliser un TransactionTemplate avec des paramètres différents (par exemple, un niveau d'isolement différent), vous devez créer deux instances TransactionTemplate différentes, bien que certaines classes puissent utiliser la même instance TransactionTemplate.

b. Utilisation directe de l'implémentation de PlatformTransactionManager :

Examinons à nouveau cette option dans le code.

 <!-- Initialization for data source --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/TEST"/> <property name="username" value="root"/> <property name="password" value="password"/> </bean> <!-- Initialization for TransactionManager --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> public class ServiceImpl implements Service { private PlatformTransactionManager transactionManager; public void setTransactionManager( PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } DefaultTransactionDefinition def = new DefaultTransactionDefinition(); //     -  ,       def.setName("SomeTxName"); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = txManager.getTransaction(def); try { //   -  } catch (Exception ex) { txManager.rollback(status); throw ex; } txManager.commit(status); } 

Maintenant, avant de passer à la prochaine méthode de gestion des transactions, voyons comment décider du type de gestion des transactions à choisir.

Choisir entre la gestion des transactions programmatique et déclarative :

  • La gestion des transactions par programme n'est un bon choix que si vous avez un petit nombre d'opérations transactionnelles. (Dans la plupart des cas, ce ne sont pas des transactions.)
  • Un nom de transaction ne peut être défini explicitement que dans la gestion des transactions du programme.
  • La gestion des transactions par programmation doit être utilisée lorsque vous souhaitez contrôler explicitement la gestion des transactions.
  • En revanche, si votre application contient de nombreuses opérations transactionnelles, cela vaut la peine d'utiliser la gestion déclarative.
  • La gestion déclarative ne vous permet pas de gérer les transactions dans la logique métier et n'est pas difficile à configurer.

2.2. Transactions déclaratives (généralement utilisées dans presque tous les scénarios d'une application Web)

Étape 1 : définissez le gestionnaire de transactions dans le fichier xml contextuel de votre application Spring.

 <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"/> <tx:annotation-driven transaction-manager="txManager"/> 

Étape 2 : activez la prise en charge des annotations en ajoutant une entrée dans le fichier xml contextuel de votre application Spring.

OU ajoutez @EnableTransactionManagement à votre fichier de configuration, comme indiqué ci-dessous:

 @Configuration @EnableTransactionManagement public class AppConfig { ... } 

Spring recommande d'annoter uniquement des classes spécifiques (et des méthodes de classes spécifiques) avec l'annotation @Transactional par rapport aux interfaces d'annotation.

La raison en est que vous placez l'annotation au niveau de l'interface, et si vous utilisez des classes proxy ( proxy-target-class = «true» ) ou l'aspect entrelacé ( mode = «aspectj» ), alors les paramètres de transaction ne sont pas reconnus par l'infrastructure proxy et les plexus, par exemple, le comportement transactionnel ne s'appliquera pas.

Étape 3 : ajoutez l'annotation @Transactional à la classe (méthode de classe) ou à l'interface (méthode d'interface).

 <tx:annotation-driven proxy-target-class="true"> 

Configuration par défaut: proxy-target-class="false"

  • @Transactional peut être placée avant une définition d'interface, une méthode d'interface, une définition de classe ou une méthode de classe publique.
  • Si vous souhaitez que certaines méthodes de classe (marquées d' @Transactional annotation @Transactional ) aient des paramètres d'attribut différents, tels que le niveau d'isolement ou le niveau de propagation, placez l'annotation au niveau de la méthode pour remplacer les paramètres d'attribut au niveau de la classe.
  • En mode proxy (qui est défini par défaut), seuls les appels de méthode «externes» passant par le proxy peuvent être interceptés. Cela signifie qu'un «appel indépendant», par exemple, une méthode dans la cible qui appelle une autre méthode de la cible, ne conduira pas à une transaction réelle au moment de l'exécution même si la méthode appelée est étiquetée avec @Transactional .

Voyons maintenant @Transactional différence entre les @Transactional annotation @Transactional

@Transactional (isolation=Isolation.READ_COMMITTED)

  • La valeur par défaut est Isolation.DEFAULT
  • Dans la plupart des cas, vous utiliserez les paramètres par défaut jusqu'à ce que vous ayez des exigences particulières.
  • Indique au gestionnaire de transactions ( tx ) que le prochain niveau d'isolement doit être utilisé pour le tx actuel. Il doit être installé au point de départ de tx , car nous ne pouvons pas modifier le niveau d'isolement après le démarrage de tx.

PAR DÉFAUT : utilisez le niveau d'isolement par défaut dans la base de données de base.

READ_COMMITTED (lecture de données fixes): constante indiquant qu'une lecture incorrecte a été empêchée; Une lecture non répétée et une lecture fantôme peuvent se produire.

READ_UNCOMMITTED (lire les données non validées): ce niveau d'isolement indique qu'une transaction peut lire des données qui n'ont pas encore été supprimées par d'autres transactions.

REPEATABLE_READ (lecture répétabilité): une constante indiquant que la lecture sale et la lecture non répétable sont empêchées; une lecture fantôme peut apparaître.

SÉRIALISABLE : permanent, indiquant que la lecture incorrecte , la lecture non répétable et la lecture fantôme sont interdites.

Que signifient ces jargons: lecture «sale», lecture fantôme ou lecture répétée?

  • Sale lecture : la transaction A écrit. Pendant ce temps, la transaction «B» lit le même enregistrement jusqu'à ce que la transaction A soit terminée. Plus tard, la transaction A décide d'annuler, et maintenant nous avons des modifications à la transaction B qui sont incompatibles. C'est une lecture sale. La transaction B fonctionnait au niveau d'isolement READ_UNCOMMITTED, afin qu'elle puisse lire les modifications apportées par la transaction A avant la fin de la transaction.
  • Lecture non répétable : la transaction «A» lit certains enregistrements. La transaction «B» écrit ensuite cet enregistrement et le valide. Plus tard, la transaction A lit à nouveau le même enregistrement et peut recevoir des valeurs différentes, car la transaction B a apporté des modifications à cet enregistrement et les a validées. Il s'agit d'une lecture non répétitive.
  • Lecture fantôme : la transaction «A» lit une série d'enregistrements. Pendant ce temps, la transaction «B» insère un nouvel enregistrement dans la même ligne que la transaction A. Plus tard, la transaction A lit à nouveau la même plage et reçoit également l'enregistrement que la transaction B vient d'insérer. Il s'agit d'une lecture fantôme: la transaction a récupéré plusieurs fois une série d'enregistrements de la base de données et a reçu différents jeux de résultats (contenant des enregistrements fantômes).

@Transactional(timeout=60)

La valeur par défaut est le délai d'expiration par défaut pour le système de transaction sous-jacent.

Informe le gestionnaire tx de la durée d'attente avant que tx soit inactif avant de décider d'annuler ou non les transactions qui ne répondent pas.

@Transactional(propagation=Propagation.REQUIRED)

S'il n'est pas spécifié, le comportement de propagation par défaut est REQUIRED .

Les autres options sont REQUIRES_NEW, MANDATORY, SUPPORTS, NOT_SUPPORTED, NEVER et NESTED .

OBLIGATOIRE

Indique que la méthode cible ne peut pas fonctionner sans un tx actif. Si tx est déjà en cours d'exécution avant d'appeler cette méthode, alors il continuera dans le même tx, ou le nouveau tx commencera peu de temps après l'appel de cette méthode.

REQUIRES_NEW

  • Indique qu'un nouveau tx doit être exécuté à chaque appel de la méthode cible. Si tx est déjà en cours d'exécution, il sera mis en pause avant d'en démarrer un nouveau.

Obligatoire

  • Indique que la méthode cible nécessite un tx actif. Si tx ne continue pas, il n'échouera pas, lançant une exception.

SUPPORTS

  • Indique que la méthode cible peut être exécutée indépendamment de tx. Si tx fonctionne, il participera au même tx. S'il est exécuté sans tx, il sera toujours exécuté s'il n'y a pas d'erreur.

  • Les méthodes qui récupèrent des données sont les meilleures candidates pour cette option.

NOT_SUPPORTED

  • Indique que la méthode cible ne nécessite pas de propagation de contexte de transaction.
  • Fondamentalement, les méthodes qui sont exécutées dans une transaction, mais qui effectuent des opérations avec de la RAM, sont les meilleures candidates pour cette option.

Jamais

  • Indique que la méthode cible lèvera une exception si elle est exécutée dans un processus transactionnel.
  • Cette option n'est dans la plupart des cas pas utilisée dans les projets.

@Transactional (rollbackFor=Exception.class)

Valeur par défaut: rollbackFor=RunTimeException.class

Au printemps, toutes les classes d'API lèvent une RuntimeException, ce qui signifie que si une méthode échoue, le conteneur annule toujours la transaction en cours.

Le problème ne concerne que les exceptions vérifiées. Ainsi, ce paramètre peut être utilisé pour annuler de manière déclarative une transaction si une Checked Exception se produit.

@Transactional (noRollbackFor=IllegalStateException.class)

Indique que la restauration ne doit pas se produire si la méthode cible déclenche cette exception.

Maintenant, la dernière étape, mais la plus importante dans la gestion des transactions, consiste à publier l'annotation @Transactiona l. Dans la plupart des cas, une confusion survient là où l'annotation doit être située: au niveau du service ou au niveau du DAO?

@Transactional : niveau de service ou DAO?

Le service est le meilleur endroit pour placer @Transactional , le niveau de service doit contenir le comportement du cas d'utilisation au niveau de détail pour l'interaction utilisateur, qui va logiquement dans la transaction.

Il existe de nombreuses applications CRUD qui n'ont pas de logique métier significative ayant un niveau de service qui transfère simplement les données entre les contrôleurs et les objets d'accès aux données, ce qui n'est pas utile. Dans ces cas, nous pouvons placer l'annotation de transaction au niveau DAO.

Par conséquent, dans la pratique, vous pouvez les placer n'importe où, c'est à vous.

De plus, si vous mettez @Transactional dans la couche DAO et si votre couche DAO sera réutilisée par différents services, il sera difficile de la placer dans la couche DAO, car différents services peuvent avoir des exigences différentes.

Si votre niveau de service récupère des objets à l'aide de Hibernate, et supposons que vous ayez des initialisations paresseuses dans la définition d'un objet de domaine, vous devez ouvrir la transaction au niveau du service, sinon vous rencontrerez une LazyInitializationException levée par ORM.

Prenons un autre exemple où votre niveau de service peut appeler deux méthodes DAO différentes pour effectuer des opérations de base de données. Si votre première opération DAO a échoué, les deux autres peuvent être transférées et vous mettrez fin à l'état incohérent de la base de données. L'annotation du niveau de service peut vous éviter de telles situations.

J'espère que cet article vous a aidé.

LA FIN

Il est toujours intéressant de voir vos commentaires ou questions.

Source: https://habr.com/ru/post/fr431508/


All Articles