web-dev-qa-db-fra.com

Faites reculer A si B tourne mal. botte de printemps, jdbctemplate

J'ai une méthode, 'databaseChanges', qui appelle 2 opérations: A, B de manière itérative. "A" en premier, "B" en dernier. 'A' et 'B' peuvent être C reate, U pdate - D supprimer les fonctionnalités de mon stockage persistant, Oracle Database 11g.

Disons,

'A' met à jour un enregistrement dans la table Users, attribut Zip, où id = 1.

"B" insère un enregistrement dans les loisirs de table.

Scénario: La méthode databaseChanges a été appelée, 'A' fonctionne et met à jour l'enregistrement. 'B' fonctionne et essaie d'insérer un enregistrement, quelque chose se produit, une exception est levée, l'exception se propage à la méthode databaseChanges.

Attendu: 'A' et 'B' n'ont rien changé. la mise à jour effectuée par "A" sera annulée. 'B' n'a rien changé, eh bien ... il y avait une exception.

Réel: La mise à jour 'A' ne semble pas avoir été annulée. 'B' n'a rien changé, eh bien ... il y avait une exception.


n certain code

Si j'avais la connexion, je ferais quelque chose comme:

private void databaseChanges(Connection conn) {
   try {
          conn.setAutoCommit(false);
          A(); //update.
          B(); //insert
          conn.commit();
   } catch (Exception e) { 
        try {
              conn.rollback();
        } catch (Exception ei) {
                    //logs...
        }
   } finally {
          conn.setAutoCommit(true);
   }
}

Le problème: Je n'ai pas la connexion (voir les Tags qui postent avec la question)

J'ai essayé de:

@Service
public class SomeService implements ISomeService {
    @Autowired
    private NamedParameterJdbcTemplate jdbcTemplate;
    @Autowired
    private NamedParameterJdbcTemplate npjt;

    @Transactional
    private void databaseChanges() throws Exception {   
        A(); //update.
        B(); //insert
    }
}

Ma classe AppConfig:

import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

@Configuration
public class AppConfig {    
    @Autowired
    private DataSource dataSource;

    @Bean
    public NamedParameterJdbcTemplate namedParameterJdbcTemplate() {
        return new NamedParameterJdbcTemplate(dataSource);
    }   
}

'A' fait la mise à jour. de 'B' une exception est levée. La mise à jour effectuée par "A" n'est pas annulée.

D'après ce que j'ai lu, je comprends que je n'utilise pas correctement @Transactional. J'ai lu et essayé plusieurs articles de blog et Q&A stackverflow sans succès pour résoudre mon problème.

Aucune suggestion?


MODIFIER

Il existe une méthode qui appelle la méthode databaseChanges ()

public void changes() throws Exception {
    someLogicBefore();
    databaseChanges();
    someLogicAfter();
}

Quelle méthode doit être annotée avec @Transactional,

changements()? databaseChanges ()?

10
lolo

L'annotation @Transactional Au printemps fonctionne en encapsulant votre objet dans un proxy qui, à son tour, encapsule les méthodes annotées avec @Transactional Dans une transaction. À cause de cette annotation ne fonctionnera pas sur les méthodes privées (comme dans votre exemple) car les méthodes privées ne peuvent pas être héritées => elles ne peuvent pas être encapsulées (ce n'est pas vrai si vous utilisez déclarative transactions avec aspectj , alors les mises en garde liées au proxy ci-dessous ne s'appliquent pas).

Voici une explication de base du fonctionnement de la magie du printemps @Transactional.

Tu as écrit:

class A {
    @Transactional
    public void method() {
    }
}

Mais c'est ce que vous obtenez réellement lorsque vous injectez un bean:

class ProxiedA extends A {
   private final A a;

   public ProxiedA(A a) {
       this.a = a;
   }

   @Override
   public void method() {
       try {
           // open transaction ...
           a.method();
           // commit transaction
       } catch (RuntimeException e) {
           // rollback transaction
       } catch (Exception e) {
           // commit transaction
       }
   }
} 

Cela a des limites. Ils ne fonctionnent pas avec les méthodes @PostConstruct Car ils sont appelés avant que l'objet ne soit mandaté. Et même si vous les avez tous configurés correctement, les transactions ne sont annulées que par défaut non vérifiées. Utilisez @Transactional(rollbackFor={CustomCheckedException.class}) si vous avez besoin d'une restauration sur une exception vérifiée.

Une autre mise en garde fréquemment rencontrée que je connais:

La méthode @Transactional Ne fonctionnera que si vous l'appelez "de l'extérieur", dans l'exemple suivant b() ne sera pas encapsulé dans la transaction:

class X {
   public void a() {
      b();
   }

   @Transactional
   public void b() {
   }
}

C'est aussi parce que @Transactional Fonctionne en mandatant votre objet. Dans l'exemple ci-dessus, a() appellera X.b() pas une méthode améliorée "proxy de printemps" b() donc il n'y aura pas de transaction. Pour contourner ce problème, vous devez appeler b() depuis un autre bean.

Lorsque vous avez rencontré l'une de ces mises en garde et que vous ne pouvez pas utiliser une solution de contournement suggérée (rendre la méthode non privée ou appeler b() à partir d'un autre bean), vous pouvez utiliser TransactionTemplate au lieu de transactions déclaratives:

public class A {
    @Autowired
    TransactionTemplate transactionTemplate;

    public void method() {
        transactionTemplate.execute(status -> {
            A();
            B();
            return null;
        });
    }

...
} 

Mise à jour

Répondre à la question mise à jour OP en utilisant les informations ci-dessus.

Quelle méthode doit être annotée avec @Transactional: changes ()? databaseChanges ()?

@Transactional(rollbackFor={Exception.class})
public void changes() throws Exception {
    someLogicBefore();
    databaseChanges();
    someLogicAfter();
}

Assurez-vous que changes() est appelée "de l'extérieur" d'un bean, pas de la classe elle-même et après l'instanciation du contexte (par exemple, ce n'est pas la méthode annotée afterPropertiesSet() ou @PostConstruct) . Comprenez que la transaction de restauration de printemps uniquement pour les exceptions non contrôlées par défaut (essayez d'être plus précis dans la liste des exceptions de restauration pour les cases cochées).

19
frenzykryger

Tout RuntimeException déclenche la restauration, et aucune exception vérifiée ne le fait.

Il s'agit d'un comportement courant dans toutes les API de transaction Spring. Par défaut, si un RuntimeException est jeté depuis le code transactionnel, la transaction sera annulée. Si une exception vérifiée (c'est-à-dire pas un RuntimeException) est levée, la transaction ne sera pas annulée.

Cela dépend de l'exception que vous obtenez dans la fonction databaseChanges. Donc, pour intercepter toutes les exceptions, il vous suffit d'ajouter rollbackFor = Exception.class

Le changement censé être sur la classe de service, le code sera comme ça:

@Service
public class SomeService implements ISomeService {
    @Autowired
    private NamedParameterJdbcTemplate jdbcTemplate;
    @Autowired
    private NamedParameterJdbcTemplate npjt;

    @Transactional(rollbackFor = Exception.class)
    private void databaseChanges() throws Exception {   
        A(); //update
        B(); //insert
    }
}

De plus, vous pouvez faire quelque chose de bien avec lui donc pas tout le temps vous devrez écrire rollbackFor = Exception.class. Vous pouvez y parvenir en écrivant votre propre annotation personnalisée:

@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(rollbackFor = Exception.class)
@Documented
public @interface CustomTransactional {  
}

Le code final sera comme ça:

@Service
public class SomeService implements ISomeService {
    @Autowired
    private NamedParameterJdbcTemplate jdbcTemplate;
    @Autowired
    private NamedParameterJdbcTemplate npjt;

    @CustomTransactional
    private void databaseChanges() throws Exception {   
        A(); //update
        B(); //insert
    }
}
5
choop

Le premier code que vous présentez est pour UserTransactions, c'est-à-dire que l'application doit gérer les transactions. Habituellement, vous voulez que le conteneur s'en occupe et utilise l'annotation @Transactional. Je pense que le problème dans votre cas pourrait être que vous avez l'annotation sur une méthode privée. Je déplacerais l'annotation au niveau de la classe

@Transactional
public class MyFacade {

public void databaseChanges() throws Exception {   
    A(); //update.
    B(); //insert
}

Ensuite, il devrait revenir en arrière correctement. Vous pouvez trouver plus de détails ici L'attribut Spring @Transactional fonctionne-t-il sur une méthode privée?

1
Guenther

Essaye ça:

@TransactionManagement(TransactionManagementType.BEAN)
public class MyFacade {

@TransactionAttribute(TransactionAttribute.REQUIRES_NEW)
public void databaseChanges() throws Exception {   
    A(); //update.
    B(); //insert
}
0
user3805841

Ce que vous semblez manquer est un TransactionManager. Le but de TransactionManager est de pouvoir gérer les transactions de la base de données. Il existe 2 types de transactions, programmatiques et déclaratives. Ce que vous décrivez est le besoin d'une transaction déclarative via des annotations.

Donc, ce que vous devez être en place pour votre projet est le suivant:

Dépendance des transactions Spring (en utilisant Gradle comme exemple)

compile("org.springframework:spring-tx")

Définir un gestionnaire de transactions dans la configuration Spring Boot

Quelque chose comme ça

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource)
{
    return new DataSourceTransactionManager(dataSource);
}

Vous devrez également ajouter l'annotation @EnableTransactionManagement (Vous ne savez pas si c'est gratuit dans les nouvelles versions de Spring Boot.

@EnableTransactionManagement
public class AppConfig {
...
}

Ajouter @Transactional

Ici, vous ajouteriez l'annotation @Transactional Pour la méthode à laquelle vous souhaitez participer à la transaction

@Transactional
public void book(String... persons) {
    for (String person : persons) {
        log.info("Booking " + person + " in a seat...");
        jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", person);
    }
};

Notez que cette méthode doit être publique et non privée. Vous voudrez peut-être envisager de mettre @Transactional Sur la méthode publique appelant databaseChanges().

Il y a aussi des sujets avancés sur où @Transactional Devrait aller et comment il se comporte, il vaut donc mieux commencer par faire fonctionner quelque chose, puis explorer ce domaine un peu plus tard :)

Une fois que tous ces éléments sont en place (dépendance + configuration de transactionManager + annotation), les transactions doivent alors fonctionner en conséquence.

Références

Documentation de référence Spring sur les transactions

Spring Guide for Transactions using Spring Boot - Ceci a un exemple de code avec lequel vous pouvez jouer

0
Shiraaz.M