EntityManager.persist() not working with @Transactional

1.3k Views Asked by At

I'm trying to learn how to use the @Transactional annotation in Spring (I'm not using Spring Boot). But when I swich from my - working - method using begin() and commit() ton the one with @Transactional, my entities aren't persisted. Can you help me understand what I'm doing wrong?

Without transactional/working method:

public <S extends Message> S save(S entity) {
    EntityManager em = pu.getEntityManager();
    try {
        logger.debug("Trying to save " + ((Message) entity).toString());
        em.getTransaction().begin();
        em.persist(entity);
        em.getTransaction().commit();
        logger.debug("MessageRepository.save() - after .commit()");
    }
    catch (PersistenceException e) {
        logger.error("Entity already exists");
        e.printStackTrace();
    }
    finally {
        System.out.println();
        em.close();
    }

    return entity;
}

With @Transactional/not working:

@Transactional
@Override
public <S extends Message> S save(S entity) {
    EntityManager em = pu.getEntityManager();
    em.persist(entity);
    return entity;
}

And here is my PersustanceUtil (pu) class:

package com.cypherf.repository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.annotation.PreDestroy;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
@Service
public class PersistenceUtil {
    private static EntityManagerFactory emf = null;

    @Bean
    public PlatformTransactionManager txManager() {
        return new JpaTransactionManager(getEntityManagerFactory());
    }

    /**
     * Creates entity manager factory as singleton instance and returns it
     *
     * @return the EntityManagerFactory
     */
    public EntityManagerFactory getEntityManagerFactory() {
        if (emf == null) {
            synchronized (EntityManagerFactory.class) {
                if (emf == null) {
                    final StandardServiceRegistry sr = new StandardServiceRegistryBuilder()
                            .configure() // Configures setting from hibernate.cfg.xml
                            .build();
                    try {
                        emf = new MetadataSources(sr).buildMetadata().buildSessionFactory();
                    }
                    catch (Exception e) {
                        StandardServiceRegistryBuilder.destroy(sr);
                        throw e;
                    }
                }
            }
        }
        return emf;
    }

    /**
     * Closes the entity manager factory
     */
    @PreDestroy
    public static void closeEntityManagerFactory() {
        System.out.println("PersistenceUtil.closeEntityManagerFactory()");
        if (emf != null) {
            System.out.println("Closing emf");
            emf.close();
        }
    }

    /**
     * Returns a new EntityManager instance
     *
     * @return the new EntityManager instance
     */
    public EntityManager getEntityManager() {
        return getEntityManagerFactory().createEntityManager();
    }
}

Main class:

package com.cypherf;
import com.cypherf.model.Message;
import com.cypherf.repository.MessageRepository;
import com.cypherf.service.MessageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@ComponentScan
@EnableTransactionManagement
public class TestSpringApplication {
    @Autowired
    private ApplicationContext context;

    @Autowired
    private CrudRepository<Message, Long> messageRepository;

    @Autowired
    private MessageService messageService;

    final static Logger logger = LoggerFactory.getLogger(TestSpringApplication.class);

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(TestSpringApplication.class);
        TestSpringApplication app = ctx.getBean(TestSpringApplication.class);
        logger.info("Running app...");
        app.run(args);
        //TestSpringApplication app = context.getBean(TestSpringApplication.class);
        //TestSpringApplication app = new TestSpringApplication();
        //app.run(args);

    }

    public void run(String... args) {
        messageRepository.save(new Message("Message one"));
        messageRepository.save(new Message("Message two"));
        messageService.printAllMessages();

        messageRepository.save(new Message("Message two"));
        messageService.printAllMessages();

//        System.out.println("BEANS:");
//        List<String> beans = Arrays.asList(context.getBeanDefinitionNames());
//        beans.forEach(bean -> System.out.println(bean));
    }
}

Here is the output:

[...]
2419 [main] DEBUG org.hibernate.event.internal.EntityCopyObserverFactoryInitiator  - Configured EntityCopyObserver strategy: disallow
2518 [main] DEBUG org.hibernate.boot.internal.ClassLoaderAccessImpl  - Not known whether passed class name [com.cypherf.model.Message] is safe
2518 [main] DEBUG org.hibernate.boot.internal.ClassLoaderAccessImpl  - No temp ClassLoader provided; using live ClassLoader for loading potentially unsafe class : com.cypherf.model.Message
2790 [main] DEBUG org.hibernate.bytecode.internal.bytebuddy.BytecodeProviderImpl  - HHH000513: Unable to create the ReflectionOptimizer for [com.cypherf.model.Message]: private accessor [text]
2853 [main] DEBUG org.hibernate.orm.model.mapping.creation  - Starting post-init callbacks
2853 [main] DEBUG org.hibernate.orm.model.mapping.creation  - Starting PostInitCallbackEntry : Entity(com.cypherf.model.Message) `staticFetchableList` generator
2853 [main] DEBUG org.hibernate.orm.model.mapping.creation  - Starting PostInitCallbackEntry : Entity(com.cypherf.model.Message) `sqmMultiTableInsertStrategy` interpretation
2946 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  - Static SQL for entity: com.cypherf.model.Message
2946 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Version select: select id from Message where id=?
2946 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Insert (0): insert into Message (text,id) values (?,?)
2946 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Update (0): update Message set text=? where id=?
2946 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Delete (0): delete from Message where id=?
2962 [main] DEBUG org.hibernate.orm.sql.ast.create  - Created new SQL alias : m1_0
2962 [main] DEBUG org.hibernate.orm.sql.ast.create  - Registration of TableGroup [StandardTableGroup(com.cypherf.model.Message)] with identifierForTableGroup [com.cypherf.model.Message] for NavigablePath [com.cypherf.model.Message] 
2993 [main] DEBUG org.hibernate.orm.results.graph.AST  - DomainResult Graph:
 \-EntityResultImpl [com.cypherf.model.Message]
 |  \-BasicFetch [com.cypherf.model.Message.text]

2993 [main] DEBUG org.hibernate.orm.sql.ast.tree  - SQL AST Tree:
    SelectStatement {
      FromClause {
        StandardTableGroup (m1 : com.cypherf.model.Message) {
          primaryTableReference : Message as m1_0
        }
      }
    }

3040 [main] DEBUG org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator  - No JtaPlatform was specified, checking resolver
3040 [main] DEBUG org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformResolverInitiator  - No JtaPlatformResolver was specified, using default [org.hibernate.engine.transaction.jta.platform.internal.StandardJtaPlatformResolver]
3056 [main] DEBUG org.hibernate.engine.transaction.jta.platform.internal.StandardJtaPlatformResolver  - Could not resolve JtaPlatform, using default [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
3056 [main] INFO  org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator  - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
3056 [main] DEBUG org.hibernate.type.spi.TypeConfiguration$Scope  - Scoping TypeConfiguration [org.hibernate.type.spi.TypeConfiguration@4d27d9d] to SessionFactoryImplementor [org.hibernate.internal.SessionFactoryImpl@3a209918]
3056 [main] DEBUG org.hibernate.query.named.NamedObjectRepository  - Checking 0 named HQL queries
3056 [main] DEBUG org.hibernate.query.named.NamedObjectRepository  - Checking 0 named SQL queries
3071 [main] DEBUG org.hibernate.SQL  - 
    drop table if exists Message cascade 
Hibernate: 
    drop table if exists Message cascade 
3087 [main] INFO  org.hibernate.orm.connections.access  - HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@5170bc02] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode.
3088 [main] DEBUG org.hibernate.SQL  - 
    drop sequence if exists Message_SEQ
Hibernate: 
    drop sequence if exists Message_SEQ
3088 [main] DEBUG org.hibernate.SQL  - 
    create sequence Message_SEQ start with 1 increment by 50
Hibernate: 
    create sequence Message_SEQ start with 1 increment by 50
3088 [main] INFO  org.hibernate.orm.connections.access  - HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@4601047] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode.
3103 [main] DEBUG org.hibernate.SQL  - 
    create table Message (
        id bigint not null,
        text varchar(255) unique,
        primary key (id)
    )
Hibernate: 
    create table Message (
        id bigint not null,
        text varchar(255) unique,
        primary key (id)
    )
3119 [main] DEBUG org.hibernate.internal.SessionFactoryRegistry  - Initializing SessionFactoryRegistry : org.hibernate.internal.SessionFactoryRegistry@40dd552c
3119 [main] DEBUG org.hibernate.internal.SessionFactoryRegistry  - Registering SessionFactory: 0c3c67af-87cc-4b0a-bfd9-a5e6498e66fc (<unnamed>)
3119 [main] DEBUG org.hibernate.internal.SessionFactoryRegistry  - Not binding SessionFactory to JNDI, no JNDI name configured
3119 [main] DEBUG org.hibernate.internal.SessionFactoryImpl  - Instantiated SessionFactory
3176 [main] DEBUG org.hibernate.stat.internal.StatisticsInitiator  - Statistics initialized [enabled=false]
3333 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory  - Creating shared instance of singleton bean 'messageService'
3333 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory  - Creating shared instance of singleton bean 'txManager'
3402 [main] INFO  com.cypherf.TestSpringApplication  - Running app...
3409 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Creating new transaction with name [com.cypherf.repository.MessageRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
3409 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Opened new EntityManager [SessionImpl(1809269661<open>)] for JPA transaction
3409 [main] DEBUG org.hibernate.engine.transaction.internal.TransactionImpl  - On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
3409 [main] DEBUG org.hibernate.engine.transaction.internal.TransactionImpl  - begin
3409 [main] DEBUG com.cypherf.repository.MessageRepository  - Trying to save Message [id: 0; text:"Message one"]
3425 [main] DEBUG org.hibernate.SQL  - 
    select
        next value for Message_SEQ
Hibernate: 
    select
        next value for Message_SEQ
3425 [main] DEBUG org.hibernate.id.enhanced.SequenceStructure  - Sequence value obtained: 1
3440 [main] DEBUG org.hibernate.event.internal.AbstractSaveEventListener  - Generated identifier: 1, using strategy: org.hibernate.id.enhanced.SequenceStyleGenerator
3440 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Initiating transaction commit
3440 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Committing JPA transaction on EntityManager [SessionImpl(1809269661<open>)]
3440 [main] DEBUG org.hibernate.engine.transaction.internal.TransactionImpl  - committing
3456 [main] DEBUG org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl  - Initiating JDBC connection release from afterTransaction
3456 [main] DEBUG org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl  - Initiating JDBC connection release from afterTransaction
3456 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Closing JPA EntityManager [SessionImpl(1809269661<open>)] after transaction
3456 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Creating new transaction with name [com.cypherf.repository.MessageRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
3456 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Opened new EntityManager [SessionImpl(1553616699<open>)] for JPA transaction
3456 [main] DEBUG org.hibernate.engine.transaction.internal.TransactionImpl  - On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
3456 [main] DEBUG org.hibernate.engine.transaction.internal.TransactionImpl  - begin
3456 [main] DEBUG com.cypherf.repository.MessageRepository  - Trying to save Message [id: 0; text:"Message two"]
3456 [main] DEBUG org.hibernate.SQL  - 
    select
        next value for Message_SEQ
Hibernate: 
    select
        next value for Message_SEQ
3456 [main] DEBUG org.hibernate.id.enhanced.SequenceStructure  - Sequence value obtained: 51
3456 [main] DEBUG org.hibernate.event.internal.AbstractSaveEventListener  - Generated identifier: 2, using strategy: org.hibernate.id.enhanced.SequenceStyleGenerator
3456 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Initiating transaction commit
3456 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Committing JPA transaction on EntityManager [SessionImpl(1553616699<open>)]
3456 [main] DEBUG org.hibernate.engine.transaction.internal.TransactionImpl  - committing
3456 [main] DEBUG org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl  - Initiating JDBC connection release from afterTransaction
3456 [main] DEBUG org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl  - Initiating JDBC connection release from afterTransaction
3456 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Closing JPA EntityManager [SessionImpl(1553616699<open>)] after transaction
3456 [main] INFO  com.cypherf.service.MessageService  - MessageService.printAllMessages()
3456 [main] DEBUG org.hibernate.orm.query.hql  - HQL : from Message
3724 [main] DEBUG org.hibernate.orm.query.sqm.ast  - SqmStatement Tree :
    -> [select]
      -> [query-spec]
        -> [select]
          -> [selection]
            -> [root] - `com.cypherf.model.Message(136820536837300)`
            <- [root] - `com.cypherf.model.Message(136820536837300)`
          <- [selection]
        <- [select]
        -> [from]
          -> [root] - `com.cypherf.model.Message(136820536837300)`
          <- [root] - `com.cypherf.model.Message(136820536837300)`
        <- [from]
      <- [query-spec]
    <- [select]

3771 [main] DEBUG org.hibernate.orm.sql.ast.create  - Created new SQL alias : m1_0
3771 [main] DEBUG org.hibernate.orm.sql.ast.create  - Registration of TableGroup [StandardTableGroup(com.cypherf.model.Message(136820536837300))] with identifierForTableGroup [com.cypherf.model.Message] for NavigablePath [com.cypherf.model.Message] 
3787 [main] DEBUG org.hibernate.orm.results.graph.AST  - DomainResult Graph:
 \-EntityResultImpl [com.cypherf.model.Message(136820536837300)]
 |  \-BasicFetch [com.cypherf.model.Message(136820536837300).text]

3787 [main] DEBUG org.hibernate.orm.sql.ast.tree  - SQL AST Tree:
    SelectStatement {
      FromClause {
        StandardTableGroup (m1 : com.cypherf.model.Message(136820536837300)) {
          primaryTableReference : Message as m1_0
        }
      }
    }

3803 [main] DEBUG org.hibernate.orm.sql.exec  - Skipping reading Query result cache data: cache-enabled = false, cache-mode = NORMAL
3819 [main] DEBUG org.hibernate.orm.results  - Initializer list
3819 [main] DEBUG org.hibernate.orm.results  -     com.cypherf.model.Message(136820536837300) -> EntityResultInitializer(com.cypherf.model.Message(136820536837300))@107577149 (SingleTableEntityPersister(com.cypherf.model.Message))
3819 [main] DEBUG org.hibernate.SQL  - 
    select
        m1_0.id,
        m1_0.text 
    from
        Message m1_0
Hibernate: 
    select
        m1_0.id,
        m1_0.text 
    from
        Message m1_0
3819 [main] DEBUG org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl  - Initiating JDBC connection release from afterTransaction
3819 [main] DEBUG org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl  - Initiating JDBC connection release from afterTransaction
3819 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Creating new transaction with name [com.cypherf.repository.MessageRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
3819 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Opened new EntityManager [SessionImpl(225465790<open>)] for JPA transaction
3819 [main] DEBUG org.hibernate.engine.transaction.internal.TransactionImpl  - On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
3819 [main] DEBUG org.hibernate.engine.transaction.internal.TransactionImpl  - begin
3819 [main] DEBUG com.cypherf.repository.MessageRepository  - Trying to save Message [id: 0; text:"Message two"]
3834 [main] DEBUG org.hibernate.event.internal.AbstractSaveEventListener  - Generated identifier: 3, using strategy: org.hibernate.id.enhanced.SequenceStyleGenerator
3834 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Initiating transaction commit
3834 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Committing JPA transaction on EntityManager [SessionImpl(225465790<open>)]
3834 [main] DEBUG org.hibernate.engine.transaction.internal.TransactionImpl  - committing
3834 [main] DEBUG org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl  - Initiating JDBC connection release from afterTransaction
3834 [main] DEBUG org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl  - Initiating JDBC connection release from afterTransaction
3834 [main] DEBUG org.springframework.orm.jpa.JpaTransactionManager  - Closing JPA EntityManager [SessionImpl(225465790<open>)] after transaction
3834 [main] INFO  com.cypherf.service.MessageService  - MessageService.printAllMessages()
3834 [main] DEBUG org.hibernate.orm.sql.exec  - Skipping reading Query result cache data: cache-enabled = false, cache-mode = NORMAL
3834 [main] DEBUG org.hibernate.orm.results  - Initializer list
3834 [main] DEBUG org.hibernate.orm.results  -     com.cypherf.model.Message(136820536837300) -> EntityResultInitializer(com.cypherf.model.Message(136820536837300))@1174086484 (SingleTableEntityPersister(com.cypherf.model.Message))
3834 [main] DEBUG org.hibernate.SQL  - 
    select
        m1_0.id,
        m1_0.text 
    from
        Message m1_0
Hibernate: 
    select
        m1_0.id,
        m1_0.text 
    from
        Message m1_0
3834 [main] DEBUG org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl  - Initiating JDBC connection release from afterTransaction
3834 [main] DEBUG org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl  - Initiating JDBC connection release from afterTransaction

Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/8.3/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 5s
3 actionable tasks: 2 executed, 1 up-to-date
13:29:27: Execution finished ':TestSpringApplication.main()'.
3

There are 3 best solutions below

9
Anish B. On BEST ANSWER

I finally made your code working on my local by fixing your code. :)

Now, it's persisting the data in the database. I have used mysql database for testing.

For testing, hibernate.cfg.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
    <session-factory>
        <property name="show_sql">true</property>
        <property name="format_sql">true</property>
        <property name="dialect">org.hibernate.dialect.MySQLDialect</property>
        <property name="connection.driver_class">com.mysql.cj.jdbc.Driver</property>
        <property name="connection.url">jdbc:mysql://localhost:3306/test</property>
        <property name="connection.username">root</property>
        <property name="connection.password">Anish@123</property>
        <property name="hibernate.hbm2ddl.auto">create-drop</property>
        <property name="hibernate.show_sql">true</property>
        <mapping class="com.example.demo.Message"/>
    </session-factory>
</hibernate-configuration>

Remove @EnableTransactionManagement from the MessageRepository class, put @Transactional(transactionManager="txManager") on the save method and put @PersistenceContext on the entityManager instead of @Autowired:

package com.example.demo;

import jakarta.persistence.EntityManager;
import jakarta.persistence.NoResultException;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.repository.CrudRepository;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Repository
public class MessageRepository implements CrudRepository<Message, Long> {

    @PersistenceContext
    private EntityManager em;

    private final static Logger logger = LoggerFactory.getLogger(MessageRepository.class);

    @Transactional(transactionManager = "txManager")
    @Override
    public @NonNull <S extends Message> S save(@NonNull S message) {
        //em.getTransaction().begin();
        em.persist(message);
        //em.getTransaction().commit();
        return message;
    }

     ....
}

Add @EnableTransactionManagement on the PersistenceUtil and @Bean over the EntityManagerFactory so that Spring is able to find the bean and inject in EntityManager via @PersistenceContext:

package com.example.demo;

import jakarta.annotation.PreDestroy;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
@Service
public class PersistenceUtil {
    final static Logger logger = LoggerFactory.getLogger(PersistenceUtil.class);

    private static EntityManagerFactory emf = null;

    @Bean
    public PlatformTransactionManager txManager() {
        return new JpaTransactionManager(getEntityManagerFactory());
    }

    @Bean
    public EntityManagerFactory getEntityManagerFactory() {
        if (emf == null) {
            synchronized (EntityManagerFactory.class) {
                if (emf == null) {
                    final StandardServiceRegistry sr = new StandardServiceRegistryBuilder()
                            .configure() // Configures setting from hibernate.cfg.xml
                            .build();
                    try {
                        emf = new MetadataSources(sr).buildMetadata().buildSessionFactory();
                    }
                    catch (Exception e) {
                        StandardServiceRegistryBuilder.destroy(sr);
                        throw e;
                    }
                }
            }
        }
        return emf;
    }

    @PreDestroy
    public static void closeEntityManagerFactory() {
        System.out.println("PersistenceUtil.closeEntityManagerFactory()");
        if (emf != null) {
            emf.close();
        }
    }

    @Bean
    public EntityManager getEntityManager() {
        return getEntityManagerFactory().createEntityManager();
    }
}

TestSpringApplication:

@Configuration
@ComponentScan
public class TestSpringApplication {
    @Autowired
    private ApplicationContext context;

    @Autowired
    private CrudRepository<Message, Long> messageRepository;

    final static Logger logger = LoggerFactory.getLogger(TestSpringApplication.class);

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(TestSpringApplication.class);
        TestSpringApplication app = ctx.getBean(TestSpringApplication.class);
        app.run(args);
    }

    public void run(String... args) {
        // SAVE
        logger.debug("saving message 1/one...");
        Message messageOne = messageRepository.save(new Message("Message one"));

        logger.debug("saving message 2/two...");
        Message messageTwo = messageRepository.save(new Message("Message two"));
        printAllMessages();


        // SAVE ALL
        logger.debug("saving message 2/two...");
        Message messageThree = new Message("Message three");
        Message messageFour = new Message("Message four");
        Message messageFive = new Message("Message five");
        Message messageSix = new Message("Message six");
        Message messageSeven = new Message("Message seven");
        Message messageEight = new Message("Message eight");
        List<Message> messages = Arrays.asList(messageThree, messageFour, messageFive, messageSix, messageSeven, messageEight);
        messageRepository.saveAll(messages);

        // FIND ALL
        printAllMessages();
    }

    private void printAllMessages() {
        logger.debug("finding all messages...");
        messageRepository.findAll().forEach(m -> {
            logger.debug("Message: " + m.toString());
        });
    }
}
 

Successful data persistence with the screenshot:

enter image description here

4
wi2ard On

Check this guide for an in-depth and clear explanation about Spring transactionality:

Your UserService (ne. in your example using main it is CrudRepository) gets proxied on the fly, and the proxy manages transactions for you. But it is not the proxy itself handling all this transactional state (open, commit, close), the proxy delegates that work to a transaction manager.

You should instantiate the transaction manager bean, use for eg. JpaTransactionManager to reuse your entityManagerFactory.

I see you have some logging framework put in place so you can enable logging of the transaction handling to better understand what's going on.

Edit after comment:

When I use my other save method with begin and commit instead of Transactional it logs them properly

You could look at your flushing. Because you're using the default flushing strategy, Jpa will only flush when needed - that is when a new query is made that needs up to date records

6
Ken Chan On

It is because in your not working example, the entityManager that you used to persist the entity is not the same entityManager instance that is used to begin the transaction.

The aspect that is backing the @Transactional will automatically create an entityManager and start the transaction on it before it actually executing the @Transactional method . You can think that the following codes actually executes behind the scene in this transactional aspect before it really executes the @Transactional method :

EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();

In your @Transactional method , you need to use the @PersistenceContext to access the entityManager instance that is created in the transactional aspect , but not creating another new entityManager by yourself which you never start any transaction on it and hence the entity your persist with it will never get commit.

Codewise , you have to do something likes :

@Repository
public class SomeRepository {

    @PersistenceContext
    private EntityManager em;
    
    @Transactional
    public <S extends Message> S save(S entity) {;
        em.persist(entity);
        return entity;
    }
}

But before making @PersistenceContext can correctly inject the entityManager , you have to first integrate spring with hibernate correctly first by defining EntityManagerFactory as a spring bean. The recommend option is use LocalContainerEntityManagerFactoryBean (See this for details).

A quick getting started example for you is :

@Bean
public LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean(DataSource ds) {
      LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
      factory.setDataSource(ds);
      factory.setPersistenceProviderClass(HibernatePersistenceProvider.class);

      // configure your factory here ..
      return factory;
}

And your JpaTransactionManager just need to be defined as :

@Bean
public PlatformTransactionManager txManager() {
        return new JpaTransactionManager();
}

It will retrieve EntityManagerFactory from the spring context (i.e the one that is created by the LocalContainerEntityManagerFactoryBean) and to use it. But not using the one that is not managed by Spring.