Sitemap

Spring Data JPA Basics

9 min readNov 14, 2024

--

  • Object relation mapping (ORM) is the process of persisting any java object directly into a database table.
  • Usually, the name of the object being persisted becomes the name of the table, and each field within that object becomes a column. With the table setup, each row corresponds to a record in the application.
  • Hibernate is one example of ORM.
  • JPA stands for Java Persistence API which provides a specification for persisting, reading, and managing data from your java object to your relational tables in the database.
  • JPA is just guidelines to implement ORM and there is no underlying code for the implementation. In short, JPA is the interface while hibernate is the implementation.
  • Spring Data JPA is part of the spring framework. The goal of spring data repository abstraction is to significantly reduce the amount of boilerplate code required to implement a data access layer for various persistence stores.
  • Spring Data JPA is not a JPA provider, it is a library/framework that adds an extra layer of abstraction on the top of our JPA provider line Hibernate.

Spring Data Commons

  • Spring Data JPA is actually built on top of the functionality provided by the Spring Data Commons.
  • Spring Data Commons defines the Repository interface, which acts as a common interface for working with data stores.
  • This interface has extensions for CRUD (Create, Read, Update, Delete) operations and and their batch analogue.
  • Developers can extend this interface and create custom repositories for their specific data models.
Press enter or click to view image in full size
Spring Data Commons Repository Interfaces
  • We even have Repositories for non-relational databases like mongoDB, redis, cassandra, dynamoDB etc. which are again built on top of Spring Data Commons
Press enter or click to view image in full size

Setup

spring:
datasource:
url: 'jdbc:postgresql://db-url'
driverClassName: 'org.postgresql.Driver'
username: 'username'
password: 'password'
jpa:
show-sql: true # show sql in console
properties:
hibernate:
format_sql: true # format sql
use_sql_comments: true # add comments to the generated SQL

CRUD Operations with Spring Data JPA

Entity

  • An entity is a persistent domain object that represents a table in a relational database.
  • Each instance of an entity corresponds to a row in the table.
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.UuidGenerator;
import java.util.UUID;

@Data
@Entity //marks the class as a JPA entity
@Table(name = "student", schema = "schema") //(Optional) Specifies the table nameif it differs from the class name
public class Student {
@Id //marks the primary key field
@UuidGenerator // automatically generates a UUID whenever a new row is saved
private UUID id;
@NotNull
private String name;
@NotNull
@Column(name = "enrollment_id", unique = true) //(Optional) Specifies the column name if it differs from the field name.
private Long enrollmentId;
}
  • Id value for an entity can be automatically generated using several strategies whenever a new row is being saved.
  • In the above example, we have used @UuidGenerator which will generate a UUID.
  • We can also use @GeneratedValue(strategy = AUTO/IDENTITY/SEQUENCE/TABLE)

Repository

import com.personal.project.spring.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;

public interface StudentRepo extends JpaRepository<Student, UUID> {
}

CRUD operations

@Service
public class StudentService {

@Autowired
private StudentRepo studentRepo;

public Student getStudent(UUID studentId){
Student student = studentRepo.findById(studentId)
.orElseThrow(() -> new IllegalArgumentException("Invalid studentId"));
return student;
}
public Student createStudent(){
Student student = new Student(); //detached entity (not associated with any DB row or not managed by persistent context)
student.setName("Joe Smith");
student.setEnrollmentId(1001L);
student = studentRepo.save(student); //persisted entity (uuid set, DB insert and returned the persisted entity)
return student;
}
public Student updateStudent(){
Student student = getStudent("8f2fed8b-ea31-491b-8656-0e772cb7d7bd"); //persisted entity
student.setName("Joe Clarke");
student = studentRepo.save(student); //persisted entity updated
return student;
}

public Student deleteStudent(UUID studentId){
studentRepo.deleteById(studentId);
return student;
}
}

Composite Primary Key

  • When primary key of a table consists of more than one columns then it is called a composite primary key.
  • Let’s say in our Student entity we have combination of name and enrollmentId as primary key, then following are the changes we’d need to make.
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Embeddable
public class StudentId implements Serializable {
@NotNull
private String name;
@NotNull
@Column(name = "enrollment_id", unique = true)
private Long enrollmentId;
}
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

import java.util.UUID;
@Data
@Entity
@Table(name = "student", schema = "schema")
public class Student {
@EmbeddedId
private StudentId id;

//other column fields
}
import com.personal.project.spring.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.UUID;
public interface StudentRepo extends JpaRepository<Student, StudentId> {
}

Derived Queries

  • Note that in above examples, we have used Repository methods that are already provided by JpaRepository which internally generates SQL for us.
  • But what if we want to use a query for which method is not provided by JpaRepository.
  • Derived queries in Spring Data JPA are a powerful feature that allows you to define query methods in your repository interfaces based on the naming conventions of the method names.
  • Spring Data JPA automatically generates the necessary SQL or JPQL queries based on these method names.
public interface StudentRepo extends JpaRepository<Student, UUID> {

//find by single attribute
//SELECT * FROM student WHERE name = ?
List<Student> findByName(String name);
//ignore case
//SELECT * FROM student WHERE LOWER(name) = LOWER(:name);
List<Student> findByNameIgnoreCase(String name);
//find by multiple attributes
//SELECT * FROM student WHERE name = ? AND enrollment_id = ?
List<Student> findByNameAndEnrollmentId(String name, Long enrollmentId);
//using comparison keywords
//SELECT * FROM student WHERE enrollment_id > ?
List<Student> findByEnrollmentIdGreaterThan(Long enrollmentId);
//using order for sorting
//SELECT * FROM student WHERE name = ? ORDER BY enrollment_id ASC
List<Student> findByNameOrderByEnrollmentIdAsc(String name);
//partial matches
//SELECT * FROM student WHERE name like %?%
List<Student> findByNameContaining(String nameFragment);
//SELECT * FROM student WHERE name like ?%
List<Student> findByNameStartsWith(String nameFragment);
//SELECT * FROM student WHERE name like %?
List<Student> findByNameEndsWith(String nameFragment);
//range queries
//SELECT * FROM student WHERE enrollment_id BETWEEN ? AND ?
List<Student> findByEnrollmentIdBetween(Long start, Long end);
// using IN
//SELECT * FROM student WHERE enrollment_id in (?, ?, ....)
List<Student> findByEnrollmentIdIn(List<Long> enrollmentIds);
//limit
//SELECT * FROM student WHERE name = ? limit 3
List<Student> findFirst3ByName(String name);
Optional<Student> findFirstByNameOrderByEnrollmentIdAsc(String name);
Optional<Student> findTopByNameOrderByEnrollmentIdAsc(String name);
//SELECT * FROM student ORDER BY enrollment_id ASC limit 1
List<Student> findFirstByOrderByEnrollmentIdAsc();
}

JPQL (Java Persistence Query Language)

  • Note that above are just a few examples of how we can frame method names to generate specific SQL.
  • These are good if we want to generate simple SQL but these method name can become long and complicated for complex SQLs.
  • For those cases, we can write our own custom queries using JPQL.
  • JPQL, or Java Persistence Query Language, is a query language used in Java Persistence API (JPA) to perform database operations on entities.
  • It is similar to SQL but operates on the entity objects rather than directly on the database tables.
public interface StudentRepository extends JpaRepository<Student, UUID> {

@Query("SELECT s FROM Student s WHERE s.name = :name")
List<Student> findByName(@Param("name") String name);
@Query("SELECT s FROM Student s WHERE s.name LIKE %:namePattern%")
List<Student> findByNamePattern(@Param("namePattern") String namePattern);
@Query("SELECT COUNT(s) FROM Student s WHERE s.name = :name")
long countByName(@Param("name") String name);
@Modifying
@Query("UPDATE Student s SET s.name = :name WHERE s.enrollmentId = :enrollmentId")
int updateNameByEnrollmentId(@Param("name") String name, @Param("enrollmentId") Long enrollmentId);
@Modifying
@Query("DELETE FROM Student s WHERE s.enrollmentId = :enrollmentId")
void deleteByEnrollmentId(@Param("enrollmentId") Long enrollmentId);
}
  • Note that if we want to write plain SQL, we can write that as well, passing nativeQuery = true
public interface StudentRepository extends JpaRepository<Student, UUID> {

@Query(value = "SELECT * FROM student WHERE name = :name", nativeQuery = true)
List<Student> findByName(@Param("name") String name);
}

Get Lavish Jain’s stories in your inbox

Join Medium for free to get updates from this writer.

Modification Queries

  • Queries that perform INSERT, UPDATE, or DELETE are modification queries.
  • First of all note that all derived or JPQL queries are not Transactional by default, so it is a good practice to annotate them with @Transactional.
  • Now let’s take an example, we will have a Repository method to delete Student by name
public interface StudentRepo extends JpaRepository<Student, UUID> {
@Transactional
int deleteByName(String name);
}
  • Let’s say there are three records with name ‘Samuel and ’we want to delete all of them, when we call this repo method following queries are executed-
Hibernate: 
select
s1_0.id,
s1_0.enrollment_id,
s1_0.name
from
student s1_0
where
s1_0.name=?
Hibernate:
delete
from
student
where
id=?
Hibernate:
delete
from
student
where
id=?
Hibernate:
delete
from
student
where
id=?
  • Note that there is one select query followed by n number of delete queries where n is number of records having name ‘Samuel’.
  • This is quite inefficient as we are hitting n number of queries where one query could do the work.
  • So the best practice is- when you need to do bulk modification, you should use JPQL query with @Modifying.
public interface StudentRepo extends JpaRepository<Student, UUID> {

@Transactional
@Modifying
@Query("DELETE from Student s where s.name = :name")
int deleteByName(@Param("name") String name);
}
  • Only one delete query get executed in this case-
Hibernate: 
delete
from
student s1_0
where
s1_0.name=?
  • @Modifying annotation is used in conjunction with the @Query annotation to indicate that the query is not a SELECT query but rather an INSERT, UPDATE, or DELETE query.
  • The @Modifying annotation is particularly useful for bulk operations where you need to update or delete multiple records in a single query. This is more efficient than iterating over a collection of entities and performing individual updates or deletes.

Projections

  • Projections are a way to retrieve a subset of attributes from an entity, rather than fetching the entire entity.
  • This can be particularly useful when you need to optimize performance by reducing the amount of data transferred from the database.

Interface Based Projections

  • You define an interface with getter methods for the attributes you want to retrieve say Id and name.
import java.util.UUID;
public interface StudentNameProjection {
UUID getId();
String getName();
}
  • Now in Repository we can define same methods with return type as this interface and only the fields defined in the interface will be fetched from the database.
import com.personal.project.spring.entity.Student;
import com.personal.project.spring.projection.StudentNameProjection;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;public interface StudentRepository extends JpaRepository<Student, UUID> {
List<StudentNameProjection> findAll();
}

DTO Based Projections

  • You can define a DTO class with required fields and these fields will be fetched from database and mapped to this DTO.
  • The DTO must have an constructor that matches the fields you want to project.
import java.util.UUID;
@Data
public class StudentDTO {
private UUID id;
private String name;
public StudentDTO(UUID id, String name) {
this.id = id;
this.name = name;
}
}
import com.personal.project.spring.entity.Student;
import com.personal.project.spring.dto.StudentDTO;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;public interface StudentRepository extends JpaRepository<Student, UUID> {
@Query("SELECT new com.example.project.spring.dto.StudentDTO(s.id, s.name) FROM Student s")
List<StudentDTO> findAllStudentDTOs();
}

Pagination

  • Paging and sorting are essential features in Spring Data JPA that help manage large datasets efficiently.
  • If we want to show all students, then it is difficult if there are large number of students.
  • So we can sort them by any property (say name) and then split them into groups of some smaller size called page and return that group of students at once.
  • We can pass number of students each page should have and can request a particular page by page number.
  • We can pass Pageable as one of the argument in any repo method and JPA will automatically return paginated response.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class StudentController {
@Autowired
private StudentService studentService;
@GetMapping("/students")
public Page<Student> getStudents(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "name") String sortBy,
@RequestParam(defaultValue = "asc") String sortDir) {
return studentService.getStudents(page, size, sortBy, sortDir);
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.UUID;@Service
public class StudentService {
@Autowired
private StudentRepository studentRepository;
public Page<Student> getStudents(int page, int size, String sortBy, String sortDir) {
Sort sort = sortDir.equalsIgnoreCase(Sort.Direction.ASC.name()) ? Sort.by(sortBy).ascending() : Sort.by(sortBy).descending();
Pageable pageable = PageRequest.of(page, size, sort);
return studentRepository.findAll(pageable);
}
}
  • If we have 7 students in total, and we pass page=0 and size=2, then total 4 pages will be there and we will get 1st page with 2 students.
    Below is the response-
{
"content": [
{
"id": "6e0913e6-4e92-4388-9fe6-ffa9c2368272",
"name": "Alex Carey",
"enrollmentId": 1006
},
{
"id": "5da6f4eb-15cf-408e-a864-65589ebd0566",
"name": "Jack Doe",
"enrollmentId": 1004
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 2,
"sort": {
"empty": false,
"sorted": true,
"unsorted": false
},
"offset": 0,
"paged": true,
"unpaged": false
},
"last": false,
"totalPages": 4,
"totalElements": 7,
"size": 2,
"number": 0,
"sort": {
"empty": false,
"sorted": true,
"unsorted": false
},
"first": true,
"numberOfElements": 2,
"empty": false
}

--

--