web-dev-qa-db-fra.com

L'annotation transactionnelle ne fonctionne pas dans Spring Boot

@Transactional ne fonctionne pas dans Spring Boot.

Application.Java:

@EnableTransactionManagement(proxyTargetClass=true)
@SpringBootApplication(exclude = {ErrorMvcAutoConfiguration.class})
public class Application {

    @Autowired
    private EntityManagerFactory entityManagerFactory;


    public static void main(String[] args) {
        System.out.println("--------------------------- Start Application ---------------------------");
        ApplicationContext ctx = SpringApplication.run(Application.class, args);
    }

    @Bean
    public SessionFactory getSessionFactory() {
        if (entityManagerFactory.unwrap(SessionFactory.class) == null) {
            throw new NullPointerException("factory is not a hibernate factory");
        }
        return entityManagerFactory.unwrap(SessionFactory.class);
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource());
        em.setPackagesToScan(new String[] { "com.buhryn.interviewer.models" });

        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(additionalProperties());

        return em;
    }

    @Bean
    public DataSource dataSource(){
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("org.postgresql.Driver");
        dataSource.setUrl("jdbc:postgresql://localhost:5432/interviewer");
        dataSource.setUsername("postgres");
        dataSource.setPassword("postgres");
        return dataSource;
    }

    @Bean
    @Autowired
    public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) {
        HibernateTransactionManager txManager = new HibernateTransactionManager();
        txManager.setSessionFactory(sessionFactory);

        return txManager;
    }

    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslation(){
        return new PersistenceExceptionTranslationPostProcessor();
    }

    Properties additionalProperties() {
        Properties properties = new Properties();
        properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
        properties.setProperty("hibernate.show_sql", "false");
        properties.setProperty("hibernate.format_sql", "false");
        properties.setProperty("hibernate.hbm2ddl.auto", "create");
        properties.setProperty("hibernate.current_session_context_class", "org.hibernate.context.internal.ThreadLocalSessionContext");
        return properties;
    }
}

CandidateDao.Java

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class CandidateDao implements ICandidateDao{

    @Autowired
    SessionFactory sessionFactory;

    protected Session getCurrentSession(){
        return sessionFactory.getCurrentSession();
    }

    @Override
    @Transactional
    public CandidateModel create(CandidateDto candidate) {
        CandidateModel candidateModel = new CandidateModel(candidate.getFirstName(), candidate.getLastName(), candidate.getEmail(), candidate.getPhone());
        getCurrentSession().save(candidateModel);
        return candidateModel;
    }

    @Override
    public CandidateModel show(Long id) {
        return new CandidateModel(
                "new",
                "new",
                "new",
                "new");
    }

    @Override
    public CandidateModel update(Long id, CandidateDto candidate) {
        return new CandidateModel(
                "updated",
                candidate.getLastName(),
                candidate.getEmail(),
                candidate.getPhone());
    }

    @Override
    public void delete(Long id) {

    }
}

Classe de service

@Service
public class CandidateService implements ICandidateService{

    @Autowired
    ICandidateDao candidateDao;

    @Override
    public CandidateModel create(CandidateDto candidate) {
        return candidateDao.create(candidate);
    }

    @Override
    public CandidateModel show(Long id) {
        return candidateDao.show(id);
    }

    @Override
    public CandidateModel update(Long id, CandidateDto candidate) {
        return candidateDao.update(id, candidate);
    }

    @Override
    public void delete(Long id) {
        candidateDao.delete(id);
    }
}

Controller.class

@RestController
@RequestMapping(value = "/api/candidates")
public class CandidateController {

    @Autowired
    ICandidateService candidateService;

    @RequestMapping(value="/{id}", method = RequestMethod.GET)
    public CandidateModel show(@PathVariable("id") Long id) {
        return candidateService.show(id);
    }

    @RequestMapping(method = RequestMethod.POST)
    public CandidateModel create(@Valid @RequestBody CandidateDto candidate, BindingResult result) {
        RequestValidator.validate(result);
        return candidateService.create(candidate);
    }

    @RequestMapping(value="/{id}", method = RequestMethod.PUT)
    public CandidateModel update(@PathVariable("id") Long id, @Valid @RequestBody CandidateDto candidate, BindingResult result) {
        RequestValidator.validate(result);
        return candidateService.update(id, candidate);
    }

    @RequestMapping(value="/{id}", method = RequestMethod.DELETE)
    public void delete(@PathVariable("id") Long id) {
        candidateService.delete(id);
    }
}

Lorsque j'appelle la méthode create dans le système DAO throw exception :

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.orm.jpa.JpaSystemException: save is not valid without active transaction; nested exception is org.hibernate.HibernateException: save is not valid without active transaction
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.Java:978)
    org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.Java:868)
    javax.servlet.http.HttpServlet.service(HttpServlet.Java:644)
    org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.Java:842)
    javax.servlet.http.HttpServlet.service(HttpServlet.Java:725)
    org.Apache.Tomcat.websocket.server.WsFilter.doFilter(WsFilter.Java:52)
    org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration$ApplicationContextHeaderFilter.doFilterInternal(EndpointWebMvcAutoConfiguration.Java:291)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.Java:107)
    org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.Java:77)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.Java:107)
    org.springframework.boot.actuate.trace.WebRequestTraceFilter.doFilterInternal(WebRequestTraceFilter.Java:102)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.Java:107)
    org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.Java:85)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.Java:107)
    org.springframework.boot.actuate.autoconfigure.MetricFilterAutoConfiguration$MetricsFilter.doFilterInternal(MetricFilterAutoConfiguration.Java:90)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.Java:107)

Mon fichier Gradle :

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE")
    }
}

apply plugin: 'Java'
apply plugin: 'idea'
apply plugin: 'spring-boot'

jar {
    baseName = 'interviewer'
    version =  '0.1.0'
}

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-actuator")
    compile("org.codehaus.jackson:jackson-mapper-asl:1.9.13")
    compile("com.google.code.gson:gson:2.3.1")
    compile("org.springframework.data:spring-data-jpa:1.8.0.RELEASE")
    compile("org.hibernate:hibernate-entitymanager:4.3.10.Final")
    compile("postgresql:postgresql:9.1-901-1.jdbc4")
    compile("org.aspectj:aspectjweaver:1.8.6")

    testCompile("org.springframework.boot:spring-boot-starter-test")

}

task wrapper(type: Wrapper) {
    gradleVersion = '2.3'
}

Et un lien vers le référentiel git: https://github.com/Yurii-Buhryn/interviewer

17
Yurii Buhryn

Vous utilisez d'abord Spring Boot, puis utilisez Spring Boot et laissez-le configurer automatiquement les choses pour vous. Il configurera une source de données, une entité entitymanagerfactory, un gestionnaire de transactions, etc.

Ensuite, vous utilisez le mauvais gestionnaire de transactions, vous utilisez JPA, vous devez donc utiliser le JpaTransactionManager au lieu du HibernateTransactionManager car il est déjà configuré pour vous, vous pouvez simplement supprimer la définition du bean pour cela.

Second votre hibernate.current_session_context_class gâche une bonne intégration tx, supprimez-le.

Utiliser la configuration automatique

Lorsque vous prenez tout cela en compte, vous pouvez essentiellement réduire votre classe Application comme suit.

@SpringBootApplication(exclude = {ErrorMvcAutoConfiguration.class})
@EntityScan("com.buhryn.interviewer.models")
public class Application {

    public static void main(String[] args) {
        System.out.println("--------------------------- Start Application ---------------------------");
        ApplicationContext ctx = SpringApplication.run(Application.class, args);
    }

    @Bean
    public SessionFactory sessionFactory(EntityManagerFactory emf) {
        if (emf.unwrap(SessionFactory.class) == null) {
            throw new NullPointerException("factory is not a hibernate factory");
        }
        return emf.unwrap(SessionFactory.class);
    }
}

Ajoutez ensuite un application.properties dans src/main/resources contenant les éléments suivants.

# DataSource configuration
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.url=jdbc:postgresql://localhost:5432/interviewer

# General JPA properties
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.show-sql=false

# Hibernate Specific properties
spring.jpa.properties.hibernate.format_sql=false
spring.jpa.hibernate.ddl-auto=create

Cela configurera correctement la source de données et JPA.

Utilisez JPA au lieu de Hibernate ordinaire

Une autre astuce au lieu d'utiliser l'API hibernate simple consiste simplement à utiliser JPA de cette façon, vous pouvez également supprimer le bean pour le SessionFactory. Changez simplement votre dao pour utiliser un EntityManager au lieu d'un SessionFactory.

@Repository
public class CandidateDao implements ICandidateDao{

    @PersistenceContext
    private EntityManager em;

    @Override
    @Transactional
    public CandidateModel create(CandidateDto candidate) {
        CandidateModel candidateModel = new CandidateModel(candidate.getFirstName(), candidate.getLastName(), candidate.getEmail(), candidate.getPhone());
        return em.persist(candidateModel);
    }

    @Override
    public CandidateModel show(Long id) {
        return new CandidateModel(
                "new",
                "new",
                "new",
                "new");
    }

    @Override
    public CandidateModel update(Long id, CandidateDto candidate) {
        return new CandidateModel(
                "updated",
                candidate.getLastName(),
                candidate.getEmail(),
                candidate.getPhone());
    }

    @Override
    public void delete(Long id) {

    }
}

Ajout de Spring Data JPA

Et si vous voulez vraiment en bénéficier, ajoutez Spring Data JPA dans le mix et supprimez complètement votre DAO et ne laissez qu'une interface. Ce que vous avez maintenant serait déplacé vers une classe de service (à laquelle il appartient à mon humble avis).

L'ensemble du référentiel

public interface ICandidateDao extends JpaRepository<CandidateModel, Long> {}

Le service modifié (qui est maintenant aussi transactionnel comme il se doit et toute la logique métier est dans le service).

@Service
@Transactional
public class CandidateService implements ICandidateService{

    @Autowired
    ICandidateDao candidateDao;

    @Override
    public CandidateModel create(CandidateDto candidate) {
        CandidateModel candidateModel = new CandidateModel(candidate.getFirstName(), candidate.getLastName(), candidate.getEmail(), candidate.getPhone());
        return candidateDao.save(candidate);
    }

    @Override
    public CandidateModel show(Long id) {
        return candidateDao.findOne(id);
    }

    @Override
    public CandidateModel update(Long id, CandidateDto candidate) {
        CandidateModel cm = candidateDao.findOne(id);
        // Update values.
        return candidateDao.save(cm);
    }

    @Override
    public void delete(Long id) {
        candidateDao.delete(id);
    }
}

Maintenant, vous pouvez également supprimer la définition de bean pour le SessionFactory en réduisant votre Application à une méthode main uniquement.

@SpringBootApplication(exclude = {ErrorMvcAutoConfiguration.class})
@EntityScan("com.buhryn.interviewer.models")
public class Application {

    public static void main(String[] args) {
        System.out.println("--------------------------- Start Application ---------------------------");
        ApplicationContext ctx = SpringApplication.run(Application.class, args);
    }
}

Je suggère donc fortement de travailler avec le cadre au lieu d'essayer de contourner le cadre. Comme cela simplifiera vraiment votre développeur en direct.

Dépendances

Pour terminer, je suggère de supprimer le spring-data-jpa dépendance de vos dépendances et utilisez plutôt le démarreur. La même chose vaut pour AspectJ utiliser le démarreur AOP pour cela. De plus, jackson 1 n'est plus pris en charge, donc l'ajout de cette dépendance n'ajoute rien

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-actuator")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.springframework.boot:spring-boot-starter-aop")
    compile("com.google.code.gson:gson:2.3.1")
    compile("org.hibernate:hibernate-entitymanager:4.3.10.Final")
    compile("postgresql:postgresql:9.1-901-1.jdbc4")

    testCompile("org.springframework.boot:spring-boot-starter-test")
}
58
M. Deinum