openplanning

Tìm nạp dữ liệu với Spring Data JPA DTO Projections

  1. Entity Classes
  2. Ví dụ cơ bản
  3. Open Projections
  4. Nested Projections
  5. Phương thức Repository với tham số
  6. Quy ước đặt tên phương thức Repository
  7. Dynamic Projections
Khi sử dụng Spring Data JPA để tìm nạp (fetch) dữ liệu bạn thường sử dụng các câu truy vấn với toàn bộ các thuộc tính của một Entity, chẳng hạn:
package org.o7planning.spring_jpa_projections.repository;

import org.o7planning.spring_jpa_projections.entity.Employee;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

public interface EmployeeRepository extends CrudRepository<Employee, Long> {

	@Query("SELECT e FROM Employee e " //
			+ " where e.empNo = :empNo")
	public Employee getByEmpNo(String empNo);

}
Mỗi Entity đại diện cho một bảng trong cơ sở dữ liệu, nó có rất nhiều thuộc tính, mỗi thuộc tính tương ứng với một cột trong bảng. Đôi khi bạn chỉ cần truy vấn để lấy thông tin một vài cột thay vì tất cả, điều này làm tăng hiệu suất của ứng dụng.
Employee table.
Trong bài viết này tôi hướng dẫn bạn sử dụng Spring Data JPA DTO Projections để tạo ra các truy vấn tuỳ chỉnh, chỉ bao gồm các thuộc tính mà bạn quan tâm.
Về mặt ngữ nghĩa "Projection" (Phép chiếu) ám chỉ một ánh xạ giữa một truy vấn JPA tới các thuộc tính của một Java DTO tuỳ biến.
DTO
Khái niệm về DTO (Data Transfer Object) được giải thích một cách rườm rà trên mạng, nhưng nó đơn giản là một lớp với các thuộc tính để chứa dữ liệu, dùng để chuyển dữ liệu từ nơi này tới nơi khác. Trong trường hợp Spring Data JPA, dữ liệu được chuyển từ Database đến ứng dụng của bạn.
Trong Spring Data JPA, bạn sẽ dùng interface để mô tả một DTO với các thuộc tính mà bạn quan tâm. Phần còn lại sẽ do Spring Data JPA thực hiện, nó sẽ tạo ra lớp DTO uỷ quyền (proxy) thi hành interface này trong thời gian chạy của ứng dụng. Lớp DTO uỷ quyền sẽ chứa dữ liệu thực sự.
package org.o7planning.spring_jpa_projections.view;

public interface EmployeeView {

	public String getEmpNo();

	public String getFullName();

	public String getEmail();
}

1. Entity Classes

Trong bài viết này tôi sẽ sử dụng 2 lớp Entity dưới đây để minh hoạ trong các ví dụ:
Department.java
package org.o7planning.spring_jpa_projections.entity;

import java.io.Serializable;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;

@Entity
@Table(name = "Department", uniqueConstraints = { //
		@UniqueConstraint(name = "DEPARTMENT_UK", columnNames = { "Dept_No" }), })
public class Department implements Serializable {

	private static final long serialVersionUID = 2091523073676133566L;

	@Id
	@GeneratedValue(generator = "my_seq")
	@SequenceGenerator(name = "my_seq", //
			sequenceName = "main_seq", allocationSize = 1)
	private Long id;

	@Column(name = "Dept_No", length = 32, nullable = false)
	private String deptNo;

	@Column(name = "Name", length = 128, nullable = false)
	private String name;

	// Getters & Setters

}
Employee.java
package org.o7planning.spring_jpa_projections.entity;

import java.io.Serializable;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;

@Entity
@Table(name = "Employee", uniqueConstraints = { //
		@UniqueConstraint(name = "EMPLOYEE_UK", columnNames = { "Emp_No" }), })
public class Employee implements Serializable {

	private static final long serialVersionUID = 8195147871240380311L;

	@Id
	@GeneratedValue(generator = "my_seq")
	@SequenceGenerator(name = "my_seq", //
			sequenceName = "main_seq", allocationSize = 1)
	private Long id;

	@Column(name = "Emp_No", length = 32, nullable = false)
	private String empNo;

	@Column(name = "First_Name", length = 64, nullable = false)
	private String firstName;

	@Column(name = "Last_Name", length = 64, nullable = false)
	private String lastName;

	@Column(name = "Phone_Number", length = 32, nullable = true)
	private String phoneNumber;

	@Column(name = "Email", length = 64, nullable = false)
	private String email;

	@ManyToOne
	@JoinColumn(name = "Department_Id", nullable = false, //
			foreignKey = @ForeignKey(name = "Dept_Emp_Fk"))
	private Department department;

	// Getters & Setters
}

2. Ví dụ cơ bản

Các bước triển khai Spring Data JPA Projections rất đơn giản, bạn chỉ cần một interface mô tả một DTO với các thuộc tính (properties) mà bạn quan tâm. Sau đó viết câu truy vấn JPA để ánh xạ các cột tới các thuộc tính của DTO. Phần còn lại sẽ do Spring Data JPA thực hiện, nó sẽ tạo ra lớp DTO uỷ quyền (proxy class) thi hành interface này trong thời gian chạy của ứng dụng. Lớp DTO uỷ quyền là thứ chứa dữ liệu thực sự.
Trong ví dụ này chúng ta tạo một interface mô tả một DTO, với một vài thuộc tính, tương ứng với một vài cột dữ liệu mà chúng ta quan tâm.
EmployeeView.java
package org.o7planning.spring_jpa_projections.view;

public interface EmployeeView {

	public String getEmpNo();

	public String getFullName();

	public String getEmail();
}
Sau đó viết câu truy vấn để ánh xạ các cột dữ liệu tới các thuộc tính của DTO đã tạo ở bước trên.
EmployeeRepository.java (*)
package org.o7planning.spring_jpa_projections.repository;

import java.util.List;

import org.o7planning.spring_jpa_projections.entity.Employee;
import org.o7planning.spring_jpa_projections.view.EmployeeView;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

public interface EmployeeRepository extends CrudRepository<Employee, Long> {
	
	//
	// IMPORTANT: Required: "as empNo", "as fullName", "as email".
	//
	@Query("SELECT e.empNo as empNo, " //
			+ " concat(e.firstName, ' ', e.lastName) as fullName, " //
			+ " e.email as email " //
			+ " FROM Employee e")
	public List<EmployeeView> listEmployeeViews();

	// Other methods ..
}

3. Open Projections

Trong ví dụ này thuộc tính "fullName" của DTO sẽ không xuất hiện trong câu truy vấn JPA. Nhưng giá trị của nó vẫn được xác định một cách chính xác dựa trên chú thích @Value.
EmployeeView2.java
package org.o7planning.spring_jpa_projections.view;

import org.springframework.beans.factory.annotation.Value;

public interface EmployeeView2 {

	public String getEmpNo();

	@Value("#{target.firstName + ' ' + target.lastName}")
	public String getFullName();

	public String getEmail();
}
Câu truy vấn JPA dưới đây không bao gồm "fullName", nhưng nó cung cấp "firstName""lastName" để tính toán ra "fullName" cho DTO.
EmployeeRepository.java (** ex2)
package org.o7planning.spring_jpa_projections.repository;

import java.util.List;

import org.o7planning.spring_jpa_projections.entity.Employee;
import org.o7planning.spring_jpa_projections.view.EmployeeView;
import org.o7planning.spring_jpa_projections.view.EmployeeView2;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

public interface EmployeeRepository extends CrudRepository<Employee, Long> { 

	//
	// IMPORTANT: Required: "as empNo", "as firstName", "as lastName", "as email".
	//
	@Query("SELECT e.empNo as empNo, " //
			+ " e.firstName as firstName, " // (***)
			+ " e.lastName as lastName, " // (***)
			+ " e.email as email " //
			+ " FROM Employee e")
	public List<EmployeeView2> listEmployeeView2s();

	// Other methods ..
}

4. Nested Projections

Tiếp theo là một ví dụ với các Projections lồng nhau.
EmployeeView3.java
package org.o7planning.spring_jpa_projections.view;

public interface EmployeeView3 {

	public String getEmpNo();

	public String getFullName();

	public String getEmail();

	public DepartmentView3 getDepartment();
}
DepartmentView3.java
package org.o7planning.spring_jpa_projections.view;

import org.springframework.beans.factory.annotation.Value;

public interface DepartmentView3 {

	// Map to "deptNo" property of Department entity.
	// Same property names --> No need @Value
	public String getDeptNo();

	// Map to "name" property of Department entity.
	// Different property names --> NEED @Value
	@Value("#{target.name}")
	public String getDeptName();
}
EmployeeRepository.java (** ex3)
package org.o7planning.spring_jpa_projections.repository;

import java.util.List;

import org.o7planning.spring_jpa_projections.entity.Employee;
import org.o7planning.spring_jpa_projections.view.EmployeeView3;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

public interface EmployeeRepository extends CrudRepository<Employee, Long> {

	//
	// IMPORTANT: Required: "as empNo", "as fullName", "as email", "as department".
	//
	@Query("SELECT e.empNo as empNo, " //
			+ " concat(e.firstName, ' ', e.lastName) as fullName, " //
			+ " e.email as email, " //
			+ " d as department " //
			+ " FROM Employee e " //
			+ " Left join e.department d ")
	public List<EmployeeView3> listEmployeeView3s();

	// Other methods ..
}

5. Phương thức Repository với tham số

EmployeeRepository.java (** ex4)
package org.o7planning.spring_jpa_projections.repository;

import org.o7planning.spring_jpa_projections.entity.Employee;
import org.o7planning.spring_jpa_projections.view.EmployeeView;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

public interface EmployeeRepository extends CrudRepository<Employee, Long> {

	//
	// IMPORTANT: Required: "as empNo", "as fullName", "as email".
	//
	@Query("SELECT e.empNo as empNo, " //
			+ " concat(e.firstName, ' ', e.lastName) as fullName, " //
			+ " e.email as email " //
			+ " FROM Employee e " //
			+ " WHERE e.empNo = :empNo")
	public EmployeeView findByEmpNo(String empNo);

	// Other methods ..
}

6. Quy ước đặt tên phương thức Repository

Trong một vài trường hợp bạn có thể không cần viết câu truy vấn JPA nếu tên phương thức Repository và các tham số của nó phù hợp với các quy ước của Spring Data JPA. Điều này có nghĩa là bạn không cần sử dụng annotation @Query.
Ví dụ, chúng ta tạo một phương thức với tên theo quy tắc sau:
  • "find" + "By" + "PropertyName"
  • "find" + "Xxx" + "By" + "PropertyName"
EmployeeRepository.java (** ex5)
package org.o7planning.spring_jpa_projections.repository;

import org.o7planning.spring_jpa_projections.entity.Employee;
import org.o7planning.spring_jpa_projections.view.EmployeeView5;
import org.springframework.data.repository.CrudRepository;

public interface EmployeeRepository extends CrudRepository<Employee, Long> {

	//
	// Without @Query annotation. (Ex5)
	// - "find" + "By" + "PropertyName".
	// - "find" + "Xxx" + "By" + "PropertyName".
	//
	public EmployeeView5 findEmployeeView5ByEmpNo(String empNo);

	// Other methods ..
}
Chú ý, các thuộc tính của Interface cũng phải giống với các thuộc tính của Entity gốc.
EmployeeView5.java
package org.o7planning.spring_jpa_projections.view;

public interface EmployeeView5 { 
 
	public String getEmpNo();

	public String getFirstName();

	public String getLastName();

	public String getEmail();
}
Một ví dụ khác, không cần sử dụng annotation @Query:
EmployeeRepository.java (** ex6)
package org.o7planning.spring_jpa_projections.repository;

import org.o7planning.spring_jpa_projections.entity.Employee;
import org.o7planning.spring_jpa_projections.view.EmployeeView6;
import org.springframework.data.repository.CrudRepository;

public interface EmployeeRepository extends CrudRepository<Employee, Long> {

	//
	// Without @Query annotation. (Ex6)
	// - "find" + "By" + "PropertyName".
	// - "find" + "Xxx" + "By" + "PropertyName".
	//
	public EmployeeView6 findView6ByEmpNo(String empNo);

	// Other methods ..
}
EmployeeView6.java
package org.o7planning.spring_jpa_projections.view;

import org.springframework.beans.factory.annotation.Value;

public interface EmployeeView6 {

	public String getEmpNo();

	@Value("#{target.firstName + ' ' + target.lastName}")
	public String getFullName();

	public String getEmail();
}
Với trường hợp phương thức không có tham số, quy tắc đặt tên là:
  • "find" + "By"
  • "find" + "Xxx" + "By"
EmployeeRepository.java (** ex7)
package org.o7planning.spring_jpa_projections.repository;

import java.util.List;

import org.o7planning.spring_jpa_projections.entity.Employee;
import org.o7planning.spring_jpa_projections.view.EmployeeView6;
import org.springframework.data.repository.CrudRepository;

public interface EmployeeRepository extends CrudRepository<Employee, Long> {

	//
	// Without @Query annotation. (Ex7)
	// - "find" + "By"
	// - "find" + "Xxx" + "By"
	//
	public List<EmployeeView6> findView6By();

	// Other methods ..
}
Tóm lại, để tránh phải sử dụng annotation @Query bạn cần phải tuân thủ các quy tắc đặt tên cho phương thức Repository. Với tôi, tôi thích sử dụng @Query để mọi thứ rõ ràng hơn và có thể đặt tên phương thức một cách tuỳ ý, mặc dù phải viết code nhiều hơn một chút.
  • "find" + "By" + "PropertyNames" + "Keyword"
  • "find" + "Xxx" + "By" + "PropertyNames" + "Keyword"
Keyword
Method Name
JPA Query
GreaterThan
findByAgeGreaterThan(int age)
Select {properties} from Person e where e.age > :age
LessThan
findByAgeLessThan(int age)
Select {properties} from Person e where e.age < :age
Between
findByAgeBetween(int from, int to)
Select {properties} from Person e where e.age between :from and :to
IsNotNull, NotNull
findByFirstnameNotNull()
Select {properties} from Person e where e.firstname is not null
IsNull, Null
findByFirstnameNull()
Select {properties} from Person e where e.firstname is null
Like
findByFirstnameLike(String name)
Select {properties} from Person e where e.firstname like :name
(No Keyword)
findByFirstname(String name)
Select {properties} from Person e where e.firstname = :name
Not
findByFirstnameNot(String name)
Select {properties} from Person e where e.firstname <> :name
...

7. Dynamic Projections

Một Entity có thể có một hoặc nhiều Projections, tuỳ thuộc vào logic ứng dụng của bạn. Bạn có thể cân nhắc sử dụng phương thức Repository với tham số Class như sau:
EmployeeRepository.java (** ex8)
package org.o7planning.spring_jpa_projections.repository;

import java.util.List;

import org.o7planning.spring_jpa_projections.entity.Employee;
import org.springframework.data.repository.CrudRepository;

public interface EmployeeRepository extends CrudRepository<Employee, Long> {

	// (Ex8)
	// - "find" + "By"
	// - "find" + "Xxx" + "By"
	//
	public <T> List<T> findViewByEmpName(String empNo, Class<T> type);

	// Other methods ..
}

Các hướng dẫn Spring Boot

Show More