Open In App

Enabling Transaction Locks in Spring Data JPA

Last Updated : 22 Aug, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

Transaction locks in Spring Data JPA can help manage concurrent data access. It can ensure the data consistency. By controlling how transactions acquire the locks on the database rows or tables, we can prevent issues such as lost updates and ensure that the application maintains data integrity. This article will guide you through enabling the transaction locks using Spring Data JPA with practical examples and explanations.

Transaction locks are crucial for managing concurrent access to the data in the database. It ensures data integrity and consistency when multiple transactions interact with the same data. In Spring Data JPA, two primary types of locks are used: Pessimistic Locking and Optimistic Locking. Each approach has its own use cases, benefits, and drawbacks.

1. Pessimistic Locking

Pessimistic Locking assumes that conflicts will occur and locks the data to prevent the other transactions from accessing it concurrently. This type of locking can be useful in scenarios where you can expect high contention for the same data or when transactions involve complex operations that should not be implemented.

How It Works

1. Acquire Lock:

  • When the transaction reads a record with the pessimistic lock, the database acquires the lock on that record.
  • The lock can be of the different types, such as PESSIMISTIC_READ (prevents other transactions from writing) or PESSIMISTIC_WRITE (prevents other transactions from reading or writing).

2. Block Other Transactions

  • Other transactions trying to access the same record will be blocked until the lock can be released. This prevents data inconsistency caused by the concurrent modifications.

3. Release Lock

  • The lock is held for the duration of the transaction and it can be automatically released when the transaction commits or rolls back.

Implementation in Spring Data JPA

In Spring Data JPA, we can implement the pessimistic locking using @Lock annotation with LockModeType.PESSIMISTIC_WRITE or LockModeType.PESSIMISTIC_READ on the repository method.

Example:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import javax.persistence.LockModeType;
import java.util.Optional;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    Optional<Product> findByIdForUpdate(Long id);
}

Explanation:

  • @Lock(LockModeType.PESSIMISTIC_WRITE): It can acquires the write lock on the record when it is read.
  • findByIdForUpdate(Long id): This method can retrieves the product by its ID and locks it for writing.

Use Case

Support the two users are trying to update the price of the same product simultaneously. Pessimistic locking will ensure that only one user can be modify the record at the time preventing data inconsistency.

2. Optimistic Locking

Optimistic Locking assumes that conflicts are rare and allows the concurrent transactions to proceed without locking the data. It uses the version field to detect the conflicts and ensure that updates do not overwrite the each other.

How It Works

  1. Version Field: Each entity has the version field and typically annotated with @Version. This field can be used to track changes to the entity.
  2. Check Version: When the transaction updates the record, it includes the current version of the record. The database can checks if the version in the transaction matches the version in the database. If they can match then the update proceeds.
  3. Detect Conflicts: If another transaction has updated the record and changed the version, the current transaction with fail with an OptimisticLockException. It indicates the conflict that needs to be resolved.
  4. Handle Exceptions: The application must handle the OptimisticLockException by retrying the transaction or informing the user of the conflict.

Implementation in Spring Data JPA

In Spring Data JPA, we can optimistic locking is implemented using the @Version annotation on the field in the entity class.

Example:

import jakarta.persistence.*;

@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private Double price;

    @Version
    private Long version;
}

Explanation:

  • @Version: It makes the version field used for the optimistic locking.
  • The version field can be automatically updated by the Hibernate on the each update.

Use Case

If two users try to update the price of the same product simultaneously, optimistic locking will be detect the conflict and the prevent one of the updates from being applied, ensuring that the most recent version of the data is used.

Implementation of Enabling Transaction Locks in Spring Data JPA

Step 1: Create a New Spring Boot project

Create a new Spring Boot Project using IntelliJ Idea. Choose the below options:

  • Name: transaction-locks-demo
  • Language: Java
  • Type: Maven
  • Packaging: Jar

Click on the Next button.

Project Metadata

Step 2: Add the dependencies

Add the following dependencies into the Spring Boot project.

Dependencies

Step 3: Project Structure

Once created the project, the file structure looks like the below image.

Project Folder Structure

Step 4: Configure the Application Properties

Open the application.properties file and add the following configuration of MySQL into the Spring Boot Project.

spring.application.name=transaction-locks-demo

spring.datasource.url=jdbc:mysql://localhost:3306/transaction_locks
spring.datasource.username=root
spring.datasource.password=mypassword
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

Step 5: Create the Product Entity

The Product entity class can defines the entity with the both pessimistic and optimistic locking.

Java
package com.gfg.transactionlocksdemo;


import jakarta.persistence.*;

@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private Double price;

    @Version
    private Long version;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public Long getVersion() {
        return version;
    }

    public void setVersion(Long version) {
        this.version = version;
    }
}

Step 6: Create the ProductRepositoy Interface

This repository interface for accessing the Product entities with methods for the pessimistic locking.

Java
package com.gfg.transactionlocksdemo;


import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    Optional<Product> findByIdForUpdate(Long id);
}

Step 7: Create the ProductService Class

This ProductService layer class with methods to demonstrates both the pessimistic and optimistic locking.

Java
package com.gfg.transactionlocksdemo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    @Transactional
    public void updateProductPricePessimistic(Long id, Double newPrice) {
        Optional<Product> product = productRepository.findByIdForUpdate(id);
        product.ifPresent(p -> {
            p.setPrice(newPrice);
            productRepository.save(p);
        });
    }

    @Transactional
    public void updateProductPriceOptimistic(Long id, Double newPrice) {
        Optional<Product> product = productRepository.findById(id);
        product.ifPresent(p -> {
            p.setPrice(newPrice);
            productRepository.save(p);
        });
    }

    @Transactional
    public Product saveProduct(Product product) {
        return productRepository.save(product);
    }
}

Step 8: Create the ProductController Class

Create the controller class named as ProductController, it will handle the HTTP requests to update the product price using the both pessimistic and optimistic locking mechanisms.

Java
package com.gfg.transactionlocksdemo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/products")
public class ProductController {

    @Autowired
    private ProductService productService;

    // Endpoint for saving a new product
    @PostMapping
    public Product saveProduct(@RequestBody Product product) {
        return productService.saveProduct(product);
    }

    // Endpoint for pessimistic locking
    @PutMapping("/pessimistic/{id}")
    public String updateProductPricePessimistic(@PathVariable Long id, @RequestParam Double newPrice) {
        productService.updateProductPricePessimistic(id, newPrice);
        return "Product price updated with pessimistic lock.";
    }

    // Endpoint for optimistic locking
    @PutMapping("/optimistic/{id}")
    public String updateProductPriceOptimistic(@PathVariable Long id, @RequestParam Double newPrice) {
        productService.updateProductPriceOptimistic(id, newPrice);
        return "Product price updated with optimistic lock.";
    }
}

Step 9: Main Class

No changes are required in the main class.

Java
package com.gfg.transactionlocksdemo;

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

@SpringBootApplication
public class TransactionLocksDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(TransactionLocksDemoApplication.class, args);
	}

}

pom.xml file:

XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.3.2</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.gfg</groupId>
	<artifactId>transaction-locks-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>transaction-locks-demo</name>
	<description>transaction-locks-demo</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

Step 10: Run the application

Once completed the project, it will start and run at port 8080.

output console

Step 11: Testing the Application

1. Save the Products

POST http://localhost:8080/products

Output:

postman output

2. Pessimistic Lock Endpoint

PUT http://localhost:8080/products/pessimistic/1?newPrice=1300.0

Output:

postman output

This request will update the price of the product with id 1 to 1300.0 using the pessimistic locking. It ensures that the product record is locked for the duration of the transaction to prevent the concurrent modifications.

3. Optimistic Lock Endpoint

PUT http://localhost:8080/products/optimistic/1?newPrice=1400.0

Output:

postman output

This request will update the price of product with id 1 to 1400.0 using the optimistic locking. It uses the version field to detect concurrent the updates and handle conflicts.


Next Article

Similar Reads