Spring Data Jpa Transactionality

A database transaction can end up either in commit or rollback. there can be multiple scenarios in either commit or rollback, programing each scenario can be error-prone, hence spring data Jpa provides @Transactional annotation.

drop database if exists AppleDb;
create database AppleDb;
use AppleDb;
create TABLE apple ( apple_id BIGINT NOT NULL, apple_name VARCHAR(255) DEFAULT NULL, taste VARCHAR(255) DEFAULT NULL, PRIMARY KEY (apple_id)) ENGINE=INNODB;
create TABLE hibernate_sequence ( next_val BIGINT) ENGINE=INNODB;
insert into hibernate_sequence values ( 1 );
package com.sujan.example.jpa.controller;

import com.sujan.example.jpa.entity.Apple;
import com.sujan.example.jpa.entity.AppleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class AppleController {
    @Autowired
    private AppleService appleService;

    @PostMapping
    void transactional(@RequestBody List<Apple> appleList) {
        appleService.saveAll(appleList);
    }
}
package com.sujan.example.jpa.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Apple {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long appleId;
    private String appleName;
    private String taste;
}
package com.sujan.example.jpa.repository;

import com.sujan.example.jpa.entity.Apple;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AppleRepository extends JpaRepository<Apple, Long> {
}
package com.sujan.example.jpa.entity;

import com.sujan.example.jpa.repository.AppleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.IntStream;

@Service
public class AppleService {
    @Autowired
    private AppleRepository appleRepository;

    @Transactional
    public void saveAll(List<Apple> appleList) {
        IntStream.range(0, appleList.size()).forEach(i -> {
            Apple save = appleRepository.save(appleList.get(i));
            System.out.println("Apple Save, Generated Id is = " + save.getAppleId());
            if (i == appleList.size() - 1) {
                throw new RuntimeException();
            }
        });
    }
}
package com.sujan.example.jpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(JpaApplication.class, args);
    }
}
spring.datasource.url=jdbc:mysql://localhost:3306/AppleDb?autoReconnect=true&useSSL=false&createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.datasource.initialization-mode=always
plugins {
    id 'org.springframework.boot' version '2.3.3.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}
group = 'com.sujan'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
repositories {
    mavenCentral()
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    annotationProcessor 'org.projectlombok:lombok'
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}
test {
    useJUnitPlatform()
}
curl --location --request POST 'http://localhost:8080/' --header 'Content-Type: application/json' --data-raw '[
    {
        "appleName": "Macintosh",
        "taste": "tangy"
    },
    {
        "appleName": "Fuji",
        "taste": "sweet"
    }
]'
select next_val as id_val from hibernate_sequence for update
update hibernate_sequence set next_val= ? where next_val=?
Apple Save, Generated Id is = 1
select next_val as id_val from hibernate_sequence for update
update hibernate_sequence set next_val= ? where next_val=?
Apple Save, Generated Id is = 2
java.lang.RuntimeException with root cause

Transaction Time Out

Some times a method has to do multiple task to provide the functionality withing precisbed time limit, and tasks are dependent on each order, if any one of them fails, the entire functionality should fail.
Spring Data Jpa @Transaction allows to combine multiple different functionalities under one transaction. in below example, method saveAllApple and findAllApple have totally differnent functionality but is brought under saveAll method.
saveAllApple method is executed successfully but findAllApple fails to complete execution withing time, hence entire saveAll method will be marked as failure and all the records inserted by saveAllApple method will be reverted.

drop database if exists AppleDb;
create database AppleDb;
use AppleDb;
create TABLE apple ( apple_id BIGINT NOT NULL, apple_name VARCHAR(255) DEFAULT NULL, taste VARCHAR(255) DEFAULT NULL, PRIMARY KEY (apple_id)) ENGINE=INNODB;
create TABLE hibernate_sequence ( next_val BIGINT) ENGINE=INNODB;
insert into hibernate_sequence values ( 1 );
package com.sujan.example.jpa.controller;

import com.sujan.example.jpa.entity.Apple;
import com.sujan.example.jpa.entity.AppleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class AppleController {
    @Autowired
    private AppleService appleService;

    @PostMapping
    List<Apple> transactional(@RequestBody List<Apple> appleList) throws InterruptedException {
        List<Apple> all = appleService.saveAll(appleList);
        return all;
    }
}
package com.sujan.example.jpa.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Apple {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long appleId;
    private String appleName;
    private String taste;
}
package com.sujan.example.jpa.repository;

import com.sujan.example.jpa.entity.Apple;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AppleRepository extends JpaRepository<Apple, Long> {
}
package com.sujan.example.jpa.entity;

import com.sujan.example.jpa.repository.AppleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class AppleService {
    @Autowired
    private AppleRepository appleRepository;

    @Transactional(timeout = 5)
    public List<Apple> saveAll(List<Apple> appleList) throws InterruptedException {
        saveAllApple(appleList);
        TimeUnit.SECONDS.sleep(5);
        return findAllApple();
    }

    private List<Apple> findAllApple() {
        return appleRepository.findAll();
    }

    private void saveAllApple(List<Apple> appleList) {
        appleRepository.saveAll(appleList);
    }
}
package com.sujan.example.jpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(JpaApplication.class, args);
    }
}
spring.datasource.url=jdbc:mysql://localhost:3306/AppleDb?autoReconnect=true&useSSL=false&createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.datasource.initialization-mode=always
plugins {
    id 'org.springframework.boot' version '2.3.3.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}
group = 'com.sujan'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
repositories {
    mavenCentral()
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    annotationProcessor 'org.projectlombok:lombok'
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}
test {
    useJUnitPlatform()
}
curl --location --request POST 'http://localhost:8080/' --header 'Content-Type: application/json' --data-raw '[
    {
        "appleName": "Macintosh",
        "taste": "tangy"
    },
    {
        "appleName": "Fuji",
        "taste": "sweet"
    }
]'
select next_val as id_val from hibernate_sequence for update
update hibernate_sequence set next_val= ? where next_val=?
select next_val as id_val from hibernate_sequence for update
update hibernate_sequence set next_val= ? where next_val=?
TransactionTimedOutException: Transaction timed out: deadline was Thu Mar 18 23:41:48 IST 2021]

Transactional query methods

To make repository methods transactional, One way is to add @Transactional annotations on top of every method or else declare entire User Defined Repository as Transactional and modify methods as per requirement.

drop database if exists AppleDb;
create database AppleDb;
use AppleDb;
create TABLE apple ( apple_id BIGINT NOT NULL, apple_name VARCHAR(255) DEFAULT NULL, taste VARCHAR(255) DEFAULT NULL, PRIMARY KEY (apple_id)) ENGINE=INNODB;
create TABLE hibernate_sequence ( next_val BIGINT) ENGINE=INNODB;
insert into hibernate_sequence values ( 1 );
insert into apple (apple_name, apple_id, taste) values ('Macintosh', 1, 'tangy');
insert into apple (apple_name, apple_id, taste) values ('Fuji', 2, 'sweet');
insert into apple (apple_name, apple_id, taste) values ('Gala', 3, 'juicy');
insert into apple (apple_name, apple_id, taste) values ('Jonagold', 4, 'tart');
insert into apple (apple_name, apple_id, taste) values ('GrannySmith', 5, 'sharp');
package com.sujan.example.jpa.controller;

import com.sujan.example.jpa.entity.Apple;
import com.sujan.example.jpa.repository.AppleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class AppleController {
    @Autowired
    private AppleRepository appleRepository;

    @GetMapping
    void transactional() {
        List<Apple> all = appleRepository.findAll();
        all.forEach(System.out::println);
        appleRepository.deleteByTaste("sweet");
        all = appleRepository.findAll();
        all.forEach(System.out::println);
    }
}
package com.sujan.example.jpa.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Apple {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long appleId;
    private String appleName;
    private String taste;
}
package com.sujan.example.jpa.repository;

import com.sujan.example.jpa.entity.Apple;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Repository
@Transactional(readOnly = true)
public interface AppleRepository extends CrudRepository<Apple, Long> {
    @Override
    @Transactional(timeout = 10)
    public List<Apple> findAll();

    @Modifying
    @Transactional
    @Query("delete from Apple a where a.taste = :taste")
    void deleteByTaste(String taste);
}
package com.sujan.example.jpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(JpaApplication.class, args);
    }
}
spring.datasource.url=jdbc:mysql://localhost:3306/AppleDb?autoReconnect=true&useSSL=false&createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.datasource.initialization-mode=always
plugins {
    id 'org.springframework.boot' version '2.3.3.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}
group = 'com.sujan'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
repositories {
    mavenCentral()
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    annotationProcessor 'org.projectlombok:lombok'
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}
test {
    useJUnitPlatform()
}
curl --location --request GET 'http://localhost:8080/'
select apple0_.apple_id as apple_id1_0_, apple0_.apple_name as apple_na2_0_, apple0_.taste as taste3_0_ from apple apple0_
Apple(appleId=1, appleName=Macintosh, taste=tangy)
Apple(appleId=2, appleName=Fuji, taste=sweet)
Apple(appleId=3, appleName=Gala, taste=juicy)
Apple(appleId=4, appleName=Jonagold, taste=tart)
Apple(appleId=5, appleName=GrannySmith, taste=sharp)
Hibernate: delete from apple where taste=?
Hibernate: select apple0_.apple_id as apple_id1_0_, apple0_.apple_name as apple_na2_0_, apple0_.taste as taste3_0_ from apple apple0_
Apple(appleId=1, appleName=Macintosh, taste=tangy)
Apple(appleId=3, appleName=Gala, taste=juicy)
Apple(appleId=4, appleName=Jonagold, taste=tart)
Apple(appleId=5, appleName=GrannySmith, taste=sharp)

@Transaction annotation with readOnly flag set to true, defined on top of interface which will make every method defined in it as read Only, but Transactional annotation defined on deleteByTaste method overrides the Transactional annotation defined on interface with default value of readOnly flag which is false.

CRUD methods defined in the Spring Data Jpa repository interface are transactions. ie traction will wither commit properly or rollback. For reading API transaction is configured as read-only. Spring Data Jpa allows changing the transaction configurations of Repository methods by overriding in custom repository interface

drop database if exists AppleDb;
create database AppleDb;
use AppleDb;
create TABLE apple ( apple_id BIGINT NOT NULL, apple_name VARCHAR(255) DEFAULT NULL, taste VARCHAR(255) DEFAULT NULL, PRIMARY KEY (apple_id)) ENGINE=INNODB;
create TABLE hibernate_sequence ( next_val BIGINT) ENGINE=INNODB;
insert into hibernate_sequence values ( 1 );
insert into apple (apple_name, apple_id, taste) values ('Macintosh', 1, 'tangy');
insert into apple (apple_name, apple_id, taste) values ('Fuji', 2, 'sweet');
insert into apple (apple_name, apple_id, taste) values ('Gala', 3, 'juicy');
insert into apple (apple_name, apple_id, taste) values ('Jonagold', 4, 'tart');
insert into apple (apple_name, apple_id, taste) values ('GrannySmith', 5, 'sharp');
@RestController
public class AppleController {
    @Autowired
    private AppleRepository appleRepository;

    @GetMapping
    void transactional() {
        List<Apple> all = appleRepository.findAll();
        all.forEach(System.out::println);
    }
}
package com.sujan.example.jpa.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Apple {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long appleId;
    private String appleName;
    private String taste;
}
package com.sujan.example.jpa.repository;

import com.sujan.example.jpa.entity.Apple;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Repository
public interface AppleRepository extends CrudRepository<Apple, Long> {
    @Override
    @Transactional(timeout = 10)
    public List<Apple> findAll();
}
package com.sujan.example.jpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(JpaApplication.class, args);
    }
}
spring.datasource.url=jdbc:mysql://localhost:3306/AppleDb?autoReconnect=true&useSSL=false&createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.datasource.initialization-mode=always
plugins {
    id 'org.springframework.boot' version '2.3.3.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}
group = 'com.sujan'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
repositories {
    mavenCentral()
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    annotationProcessor 'org.projectlombok:lombok'
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}
test {
    useJUnitPlatform()
}
curl --location --request GET 'http://localhost:8080/'
select apple0_.apple_id as apple_id1_0_, apple0_.apple_name as apple_na2_0_, apple0_.taste as taste3_0_ from apple apple0_
Apple(appleId=1, appleName=Macintosh, taste=tangy)
Apple(appleId=2, appleName=Fuji, taste=sweet)
Apple(appleId=3, appleName=Gala, taste=juicy)
Apple(appleId=4, appleName=Jonagold, taste=tart)
Apple(appleId=5, appleName=GrannySmith, taste=sharp)
Hibernate: delete from apple where taste=?
Hibernate: select apple0_.apple_id as apple_id1_0_, apple0_.apple_name as apple_na2_0_, apple0_.taste as taste3_0_ from apple apple0_
Apple(appleId=1, appleName=Macintosh, taste=tangy)
Apple(appleId=3, appleName=Gala, taste=juicy)
Apple(appleId=4, appleName=Jonagold, taste=tart)
Apple(appleId=5, appleName=GrannySmith, taste=sharp)

@Transaction annotation with readOnly flag set to true, defined on top of interface which will make every method defined in it as read Only, but Transactional annotation defined on deleteByTaste method overrides the Transactional annotation defined on interface with default value of readOnly flag which is false.

follow us on