Post

Spring) Spring Boot's JPA @Transactional/SimpleJpaRepository

Spring) Spring Boot's JPA @Transactional/SimpleJpaRepository

This post was migrated from Tistory. You can find the original here.

2023.07.13 - [spring] - Spring) JPA persistence/transactions/entity states (23-07-08)

In the previous post, JPA was handled through hibernate-core.

1
implementation 'org.hibernate:hibernate-core:6.1.7.Final'

This time, let’s look at how Spring Boot handles JPA.

JPA in Spring

1
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

We use data-jpa. In plain Java we configured things via an XML file and created the EntityManager and EntityManagerFactory ourselves, but in a Spring Boot environment, once you provide the DB information in application.properties, the ManagerFactory is created automatically based on it.

1
2
@PersistenceContext
EntityManager em;

By using the @PersistenceContext annotation, you can inject and use the automatically created EntityManager.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@PersistenceContext
EntityManager em;

@Test
@Transactional 
@Rollback(value = false) // When @Transactional is used with @SpringBootTest, the default behavior is to roll back after the test finishes.
@DisplayName("Create memo - success")
void test1() {
    Memo memo = new Memo();
    memo.setUsername("Robbert");
    memo.setContents("Testing @Transactional!");

    em.persist(memo);
}

With core JPA we created/started/ended transactions manually, but in Spring we can easily handle transactions using the @Transactional annotation. When importing the transaction annotation, jakarta’s @Transactional also shows up in autocomplete — in a Spring environment, the jakarta transaction annotation produces the same result.

@Transactional

Lifecycle of the persistence context and the transaction

1
2
3
4
5
6
7
8
9
    @Test
    @DisplayName("Create memo - failure")
    void test2() {
        Memo memo = new Memo();
        memo.setUsername("Robbie");
        memo.setContents("Testing @Transactional!");

        em.persist(memo);
    }

The code above throws an error. This is because in a Spring container environment, the lifecycle of the persistence context and the transaction are one and the same. To use the persistence context, you must use @Transactional.

Transaction propagation

1
@Transactional(propagation = Propagation.REQUIRED)

The default propagation option is REQUIRED, meaning that if a transaction already exists on the parent method, the child method’s transaction joins the parent’s transaction.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    @Test
    @Transactional
    @Rollback(value = false)
    @DisplayName("Transaction propagation test")
    void test3() {
        memoRepository.createMemo(em);
        System.out.println("test3 method finished");
    }

(createMemo)
@Transactional
public Memo createMemo(EntityManager em) {
    Memo memo = em.find(Memo.class, 1);
    memo.setUsername("Robbie");
    memo.setContents("Testing @Transactional propagation!");

    System.out.println("createMemo method finished");
    return memo;
}

If you run the code above, you can confirm that the update is executed on commit only after the parent method finishes. Without the @Transactional annotation, you’d see the update happen on commit once the child method finishes, and only then would the parent method finish.

Spring Data JPA

Spring Data JPA provides a Repository interface that abstracts JPA.
The Repository interface is used through classes implemented with a JPA provider such as Hibernate. (The higher up, the more abstract; the lower down, the more concrete the implementation.)

SimpleJpaRepository

Spring Data JPA automatically generates a class that implements the JpaRepository interface.
When the Spring server starts, any interface that extends JpaRepository is automatically scanned, and based on that interface’s information, a SimpleJpaRepository class is automatically generated and registered as a Spring Bean.
So you don’t need to write an implementation class yourself — you can use JPA’s functionality directly through the JpaRepository interface.

1
2
public interface PostRepository extends JpaRepository<Post, Long> {
}

Simply extending JpaRepository<Entity, Entity ID type> like this automatically generates a class implementing the basic functionality, and it’s managed as a Bean.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(SimpleJpaRepository.java)
    @Transactional
    @Override
    public <S extends T> S save(S entity) {

        Assert.notNull(entity, "Entity must not be null");

        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }

If you look inside the implementation class, you’ll notice it’s similar to the code from the previous post that used hibernate-core to save to the DB.

1
Post savePost = postRepository.save(post);

Now the code that used to require manually using EntityManager can be written in a single line.

1
2
3
4
5
6
7
@Transactional
public Long updateMemo(Long id, MemoRequestDto requestDto) {
    Memo memo = findMemo(id);
    // update the memo contents
    memo.update(requestDto);
    return id;
}

There’s no update method in SimpleJpaRepositorythe update is carried out through the persistence context’s dirty checking. Since the persistence context and the transaction share the same lifecycle, you must add @Transactional for the update to take effect.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    @Override
    @Transactional
    @SuppressWarnings("unchecked")
    public void delete(T entity) {

        Assert.notNull(entity, "Entity must not be null");

        if (entityInformation.isNew(entity)) {
            return;
        }

        Class<?> type = ProxyUtils.getUserClass(entity);

        T existing = (T) em.find(type, entityInformation.getId(entity));

        // if the entity to be deleted doesn't exist, delete is a NOOP
        if (existing == null) {
            return;
        }

        em.remove(em.contains(entity) ? entity : em.merge(entity));
    }

Looking at the delete implementation in SimpleJpaRepository, you can see @Transactional there too. In other words, the implementation classes also rely on the persistence context’s dirty checking for updates and deletes.
However, update isn’t a pre-implemented method, so remember to add @Transactional yourself when writing it directly in a service.

+ case

1
2
3
4
5
6
7
8
9
10
11
12
13
@Transactional
public Long saveMemos(Long id, MemoRequestDto requestDto) {
    Memo memo1 = new Memo();
    Memo memo2 = new Memo();
    Memo memo3 = new Memo();
    ...
    memo.save(memo1);
    ...
    memo.save(memo2);
    ...
    memo.save(memo3);
    return id;
}

save already has @Transactional applied to it, but in a case like the one above, you still need @Transactional at this level.
If a problem occurs partway through the three saves and you need to roll all of them back, you have to use transaction propagation to join the parent’s transaction and process everything as a single transaction.

This post is licensed under CC BY-NC 4.0 by the author.