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.
@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)
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 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 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-