Tìm nạp dữ liệu với Spring Data JPA DTO 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.
DTOKhá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" và "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
- Cài đặt Spring Tool Suite cho Eclipse
- Hướng dẫn lập trình Spring cho người mới bắt đầu
- Hướng dẫn lập trình Spring Boot cho người mới bắt đầu
- Các thuộc tính thông dụng của Spring Boot
- Hướng dẫn sử dụng Spring Boot và Thymeleaf
- Hướng dẫn sử dụng Spring Boot và FreeMarker
- Hướng dẫn sử dụng Spring Boot và Groovy
- Hướng dẫn sử dụng Spring Boot và Mustache
- Hướng dẫn sử dụng Spring Boot và JSP
- Hướng dẫn sử dụng Spring Boot, Apache Tiles, JSP
- Sử dụng Logging trong Spring Boot
- Giám sát ứng dụng với Spring Boot Actuator
- Tạo ứng dụng web đa ngôn ngữ với Spring Boot
- Sử dụng nhiều ViewResolver trong Spring Boot
- Sử dụng Twitter Bootstrap trong Spring Boot
- Hướng dẫn và ví dụ Spring Boot Interceptor
- Hướng dẫn sử dụng Spring Boot, Spring JDBC và Spring Transaction
- Hướng dẫn và ví dụ Spring JDBC
- Hướng dẫn sử dụng Spring Boot, JPA và Spring Transaction
- Hướng dẫn sử dụng Spring Boot và Spring Data JPA
- Hướng dẫn sử dụng Spring Boot, Hibernate và Spring Transaction
- Tương tác Spring Boot, JPA và cơ sở dữ liệu H2
- Hướng dẫn sử dụng Spring Boot và MongoDB
- Sử dụng nhiều DataSource với Spring Boot và JPA
- Sử dụng nhiều DataSource với Spring Boot và RoutingDataSource
- Tạo ứng dụng Login với Spring Boot, Spring Security, Spring JDBC
- Tạo ứng dụng Login với Spring Boot, Spring Security, JPA
- Tạo ứng dụng đăng ký tài khoản với Spring Boot, Spring Form Validation
- Ví dụ OAuth2 Social Login trong Spring Boot
- Chạy các nhiệm vụ nền theo lịch trình trong Spring
- Ví dụ CRUD Restful Web Service với Spring Boot
- Ví dụ Spring Boot Restful Client với RestTemplate
- Ví dụ CRUD với Spring Boot, REST và AngularJS
- Bảo mật Spring Boot RESTful Service sử dụng Basic Authentication
- Bảo mật Spring Boot RESTful Service sử dụng Auth0 JWT
- Ví dụ Upload file với Spring Boot
- Ví dụ Download file với Spring Boot
- Ví dụ Upload file với Spring Boot và jQuery Ajax
- Ví dụ Upload file với Spring Boot và AngularJS
- Tạo ứng dụng Web bán hàng với Spring Boot, Hibernate
- Hướng dẫn và ví dụ Spring Email
- Tạo ứng dụng Chat đơn giản với Spring Boot và Websocket
- Triển khai ứng dụng Spring Boot trên Tomcat Server
- Triển khai ứng dụng Spring Boot trên Oracle WebLogic Server
- Cài đặt chứng chỉ SSL miễn phí Let's Encrypt cho Spring Boot
- Cấu hình Spring Boot chuyển hướng HTTP sang HTTPS
- Tìm nạp dữ liệu với Spring Data JPA DTO Projections
Show More