26 tháng 2 năm 2024 Công nghệ thông tin
JPA (Jakarta Persistence API) là một tiêu chuẩn Java EE dựa trên công nghệ ORM (Object-Relational Mapping), được sử dụng để duy trì, truy cập và quản lý dữ liệu giữa ứng dụng Java và cơ sở dữ liệu quan hệ. Tiêu chuẩn JPA cung cấp một loạt các chú thích và API để ánh xạ đối tượng Java vào bảng cơ sở dữ liệu, định nghĩa mối quan hệ giữa các thực thể và thực hiện các thao tác cơ sở dữ liệu, từ đó đơn giản hóa việc phát triển lớp dữ liệu bền vững trong ứng dụng Java.
Spring Data JPA là một mô-đun của khung Spring, nó cung cấp thêm cách tiếp cận thông qua giao diện kho lưu trữ (Repository Interface) để đơn giản hóa hơn nữa việc phát triển lớp dữ liệu bền vững. Khi sử dụng Spring Data JPA, nhà phát triển chỉ cần định nghĩa một giao diện và kế thừa giao diện Repository của Spring Data, sau đó đặt tên phương thức theo quy tắc, vậy là Spring Data JPA sẽ tự động tạo ra câu lệnh truy vấn cơ sở dữ liệu tương ứng dựa trên tên phương thức. Spring Data JPA cũng hỗ trợ sử dụng chú thích @Query
để tùy chỉnh câu lệnh truy vấn và sử dụng Specification
để viết điều kiện truy vấn động nhằm đáp ứng các yêu cầu truy vấn phức tạp. Ngoài ra, Spring Data JPA còn tích hợp quản lý giao dịch của Spring Framework và có thể tích hợp liền mạch với các tính năng khác của khung Spring.
Bài viết này sẽ giới thiệu về Spring Data Repository trước tiên; sau đó chuẩn bị dữ liệu thử nghiệm và giới thiệu ví dụ về dự án; cuối cùng trình bày cách sử dụng các chú thích và đặc điểm của Spring Data JPA thông qua mã nguồn minh họa.
1 game 123win Giới thiệu về Spring Data Repository
Để sử dụng khả năng truy cập cơ sở dữ liệu của Spring Data JPA, cách trực tiếp nhất là định nghĩa một giao diện Repository
(ví dụ: UserRepository
), sau đó cho giao diện này mở rộng giao diện org.springframework.data.repository.Repository
và chỉ định lớp Model tương ứng cùng kiểu dữ liệu của trường ID, như vậy có thể bắt đầu viết phương thức theo quy tắc đặt tên trong giao diện đã định nghĩa. Hơn nữa, giao diện tùy chỉnh cũng có thể mở rộng các giao diện dẫn xuất từ Repository
, chẳng hạn như CrudRepository
, để có thể sử dụng trực tiếp các phương thức mà nó cung cấp.
Những giao diện nào thường được mở rộng từ Repository
? Chúng có gì khác nhau? Liệt kê dưới đây:
Repository
Giao diện lõi nền tảng do Spring Data cung cấp, chỉ cần mở rộng giao diện này và định nghĩa phương thức theo quy tắc đặt tên là đã có khả năng truy cập cơ sở dữ liệu.
Định nghĩa giao diện Repository
như sau:
public interface Repository<T, ID> {}
CrudRepository
vàListCrudRepository
CrudRepository
bao gồm các phương thức cơ bản cho thao tác thêm, sửa, xóa và đọc dữ liệu.ListCrudRepository
mở rộng từCrudRepository
, hai giao diện này có chức năng tương tự nhưng khác biệt ở loại dữ liệu trả về:CrudRepository
sử dụng kiểuIterable<T>
cònListCrudRepository
sử dụng kiểuList<T>
.
Định nghĩa giao diện CrudRepository
như sau:
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
long count();
void deleteById(ID id);
// ...
}
Định nghĩa giao diện ListCrudRepository
như sau:
@NoRepositoryBean
public interface ListCrudRepository<T, ID> extends CrudRepository<T, ID> {
<S extends T> List<S> saveAll(Iterable<S> entities);
List<T> findAll();
List<T> findAllById(Iterable<ID> ids);
}
PagingAndSortingRepository
vàListPagingAndSortingRepository
PagingAndSortingRepository
hỗ trợ phân trang và sắp xếp kết quả tập hợp thực thể.ListPagingAndSortingRepository
mở rộng từPagingAndSortingRepository
, hai giao diện này có chức năng tương tự nhưng khác biệt ở loại dữ liệu trả về:PagingAndSortingRepository
sử dụng kiểuIterable<T>
cònListPagingAndSortingRepository
sử dụng kiểuList<T>
.
Định nghĩa giao diện PagingAndSortingRepository
như sau:
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends Repository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
Định nghĩa giao diện ListPagingAndSortingRepository
như sau:
@NoRepositoryBean
public interface ListPagingAndSortingRepository<T, ID> extends PagingAndSortingRepository<T, ID> {
List<T> findAll(Sort sort);
}
Ví dụ dưới đây định nghĩa một UserRepository
cho Model User
để truy cập cơ sở dữ liệu, đồng thời mở rộng CrudRepository
và thêm một số phương thức bổ sung theo quy tắc đặt tên:
public interface UserRepository extends CrudRepository<User, Long> {
boolean existsByNameAndEmail(String name, String email);
List<User> findByNameIgnoreCase(String name);
int countByName(String name);
int deleteByName(String name);
}
2 Chuẩn bị dữ liệu thử nghiệm và giới thiệu dự án ví dụ
Dự án ví dụ trong bài viết này là một dự án Spring Boot được quản lý bởi Maven, sử dụng cơ sở dữ liệu MySQL cục bộ (phiên bản 8.1.0).
Liệt kê phiên bản của JDK, Maven, Spring Boot, Spring Data JPA và Hibernate Core được sử dụng trong ví dụ này:
JDK: Amazon Corretto 17.0.8
Maven: 3.9.5
Spring Boot: 3.2.2
Spring Data JPA: 3.2.2
Hibernate Core: 6.4.1.Final
2.1 Chuẩn bị dữ liệu thử nghiệm
Thực thi các câu lệnh DDL sau trong cơ sở dữ liệu MySQL cục bộ để chuẩn bị dữ liệu thử nghiệm (bao gồm: lệnh tạo cơ sở dữ liệu, lệnh tạo bảng và dữ liệu thử nghiệm):
CREATE DATABASE test DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
DROP TABLE IF EXISTS user;
CREATE TABLE user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
age INT NOT NULL,
email VARCHAR(20) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT '2024-01-01 00:00:00',
updated_at TIMESTAMP NOT NULL DEFAULT '2024-01-01 00:00:00'
);
INSERT INTO user (name, age, email, created_at, updated_at)
VALUES ('Larry', 18, 'larry@larry.com', '2024-01-01 08:00:00', '2024-01-01 08:00:00'),
('Jacky', 28, 'jacky@jacky.com', '2024-02-01 08:00:00', '2024-02-01 08:00:00'),
('Lucy', 20, 'lucy@lucy.com', '2024-03-01 08:00:00', '2024-03-01 08:00:00');
2.2 Giới thiệu dự án ví dụ
Dự án ví dụ spring-data-jpa-demo sử dụng các phụ thuộc sau:
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Nội dung file cấu hình application.yaml của dự án ví dụ như sau (chủ yếu cấu hình thông tin kết nối cơ sở dữ liệu và bật in câu lệnh SQL):
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: root
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
Như vậy, dữ liệu thử nghiệm và khung sườn dự án đã sẵn sàng. Tiếp theo sẽ trình bày cách sử dụng Spring Data JPA thông qua mã nguồn ví dụ.
3 Sử dụng Spring Data JPA
Phần này sẽ minh họa cách sử dụng Spring Data JPA thông qua thiết kế các giao diện tăng-xóa-sửa-truy vấn cho User.
3.1 Định nghĩa lớp Model
Trước tiên cần định nghĩa một lớp Model User.java
:
// src/main/java/com/example/demo/model/User.java
package com.example.demo.model;
@Data
@Entity
public class User [f8bet72](/post/leetcode-construct-binary-search-tree-from-preorder-traversa/) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
private String email;
private Date createdAt;
}
Như có thể thấy, chúng ta sử dụng chú thích @Entity
của tiêu chuẩn JPA để đánh dấu User
là một thực thể, sau đó đánh dấu thuộc tính id
là khóa chính và chỉ định chiến lược sinh giá trị.
3.2 Định nghĩa giao diện Repository và thêm phương thức thông dụng
Tiếp theo, định nghĩa một kho lưu trữ UserRepository.java
và cho nó mở rộng từ giao diện Repository
cơ bản nhất.
Thêm các phương thức thông dụng cho thao tác tăng-xóa-sửa-truy vấn vào giao diện UserRepository
theo quy tắc đặt tên (hỗ trợ: find...By
, exists...By
, count...By
, delete...By
, v.v.):
// src/main/java/com/example/demo/repository/UserRepository.java
package com.example.demo.repository;
public interface UserRepository extends Repository<User, Long> {
// Tìm một User theo id
User findById(Long id);
// Truy vấn tập hợp User với phân trang và sắp xếp
Page<User> findAll(Pageable pageable);
// Kiểm tra sự tồn tại của User theo nhiều thuộc tính
boolean existsByNameAndEmail(String name, String email);
// Tìm tập hợp User theo tên và bỏ qua chữ hoa/thường
List<User> findByNameIgnoreCase(String name);
// Tìm tập hợp User theo tên và trả về theo thứ tự thời gian tạo giảm dần
List<User> findByNameOrderByCreatedAtDesc(String name);
// Thêm hoặc cập nhật một User
User save(User user);
// Đếm số lượng User theo tên
long countByName(String name);
// Xóa User theo id
void deleteById(Long id);
}
Các phương thức trên đều dễ hiểu về cách truyền tham số và cách sử dụng, ngoại trừ Page<User> findAll(Pageable pageable);
.
Viết một lớp kiểm thử đơn vị UserRepositoryTest.java
để minh họa cách sử dụng Page<User> findAll(Pageable pageable);
:
// src/test/java/com/example/demo/repository/UserRepositoryTest.java
package com.example.demo.repository;
@SpringBootTest
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
public void testFindAll() {
// Phân trang với pageNumber là 1 (bắt đầu từ 0), pageSize là 2 và sắp xếp theo createdAt giảm dần
Pageable pageable = PageRequest.of(1, 2, Sort.by("createdAt").descending());
Page<User> page = userRepository.findAll(pageable);
// page.getContent() trả về kiểu List<User>
assertEquals(1, page.getContent().size());
}
}
3.3 Sử dụng chú thích @Query
Ngoài việc sử dụng quy tắc đặt tên để thêm phương thức thông dụng, có thể sử dụng chú thích @Query
để tùy chỉnh câu lệnh truy vấn:
// src/main/java/com/example/demo/repository/UserRepository.java
package com.example.demo.repository;
public interface UserRepository extends Repository<User, Long> {
@Query("select u from User u where u.name = :name and u.age = :age")
User findByNameAndAge(@Param("name") String name, @Param("age") Integer age);
@Query("select u from User u where u.name = ?1 and u.age = ?2")
User findByNameAndAgeAnotherWay(String name, Integer age);
@Query(value = "select * from user where name = ?1 and age = ?2", nativeQuery = true)
User findByNameAndAgeWithNativeSQL(String name, Integer age);
}
Các phương thức findByNameAndAge
, findByNameAndAgeAnotherWay
, findByNameAndAgeWithNativeSQL
lần lượt sử dụng cách truy vấn với tham số được đặt tên, tham số placeholder và SQL gốc.
3.4 Sử dụng chú thích @Modifying
Nếu câu lệnh @Query
trong phương thức cần cập nhật cơ sở dữ liệu, thì cần thêm chú thích @Modifying
:
// src/main/java/com/example/demo/repository/UserRepository.java
package com.example.demo.repository;
public interface UserRepository extends Repository<User, Long> {
@Transactional
@Modifying
@Query("update User u set u.name = :name where u.id = :id")
void updateNameById(@Param("name") String name, @Param("id") Long id);
@Transactional
@Modifying
@Query("delete from User u where u.age > :age")
void deleteByAgeGreaterThan(@Param("age") Integer age);
}
Chú ý rằng phương thức cũng sử dụng chú thích @Transactional
, vì kiểm soát giao dịch của Spring Data chỉ áp dụng cho thao tác truy vấn trên Repository, thao tác ghi cần thêm chú thích này để được chấp nhận.
3.5 Gọi thủ tục lưu trữ
Tạo thủ tục lưu trữ get_md5_email_by_id
bằng cách sử dụng câu lệnh SQL sau:
DELIMITER //
DROP PROCEDURE IF EXISTS get_md5_email_by_id //
CREATE PROCEDURE get_md5_email_by_id(IN user_id BIGINT, OUT md5_email VARCHAR(32))
BEGIN
SELECT MD5(email) INTO md5_email FROM user WHERE id = user_id;
END //
DELIMITER ;
Thủ tục này dùng để truy vấn chuỗi MD5 của Email dựa trên ID của User.
Tiếp theo, cấu hình metadata của thủ tục này trên Model User
bằng chú thích @NamedStoredProcedureQuery
:
// src/main/java/com/example/demo/model/User.java
package com.example.demo.model;
@Entity
@NamedStoredProcedureQuery(
name = "User.getMd5EmailById",
procedureName = "get_md5_email_by_id",
parameters = {
@StoredProcedureParameter(mode = ParameterMode.IN, type = Long.class),
@StoredProcedureParameter(mode = ParameterMode.OUT, type = String.class)
}
)
public class User {}
Sau đó, tạo một phương thức mới trong UserRepository
và thêm chú thích @Procedure
với tên được cấu hình trước đó:
// src/main/java/com/example/demo/repository/UserRepository.java
package com.example.demo.repository;
public interface UserRepository extends Repository<User, Long> {
@Transactional
@Procedure(name = "User.getMd5EmailById")
String getMd5EmailUsingProcedure(@Param("user_id") Long id);
}
3.6 Sử dụng Specification để truy vấn động
Trong các tình huống kinh doanh thực tế, có thể cần tạo câu lệnh truy vấn động dựa trên điều kiện, Specification
được sử dụng để ghép nối điều kiện truy vấn động.
Để cho phép một Repository hỗ trợ truy vấn động theo Specification
, cần mở rộng giao diện JpaSpecificationExecutor<T>
:
// src/main/java/com/example/demo/repository/UserRepository.java
package com.example.demo.repository;
public interface UserRepository extends Repository<User, Long>, JpaSpecificationExecutor<User> {}
Như vậy, UserRepository
đã hỗ trợ truy vấn động theo Specification
. Tiếp theo, sử dụng phương thức List<T> findAll(Specification<T> spec)
được mở rộng từ JpaSpecificationExecutor
.
Tạo một Specification
để ghép nối điều kiện WHERE:
// Tương đương với: WHERE age > 18 AND name LIKE '%La%';
Specification<User> spec = (root, query, criteriaBuilder) -> {
Predicate ageGreaterThanCondition = criteriaBuilder.greaterThan(root.get("age"), 18);
Predicate nameLikeCondition = criteriaBuilder.like(root.get("name"), "%La%");
return criteriaBuilder.and(ageGreaterThanCondition, nameLikeCondition);
};
Cuối cùng, gọi phương thức findAll(Specification<T> spec)
của UserRepository
và truyền Specification
vào để lấy kết quả mong muốn:
List<User> users = userRepository.findAll(spec);
3.7 Sử dụng chú thích @Transactional
Chú thích org.springframework.transaction.annotation.Transactional
được sử dụng để hỗ trợ giao dịch. Giao dịch là một nhóm các thao tác cơ sở dữ liệu, tất cả phải thành công hoặc thất bại hoàn toàn; nếu bất kỳ thao tác nào thất bại, tất cả sẽ được hồi ngược về trạng thái ban đầu.
Tạo một UserServiceImpl
, viết một phương thức sử dụng UserRepository
để thực hiện thao tác xóa, nhưng phương thức này ném ra một ngoại lệ sau khi xóa:
// src/main/java/com/example/demo/service/impl/UserServiceImpl.java
package com.example.demo.service.impl;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Transactional(rollbackFor = Exception.class)
@Override
public void deleteUserByAgeGreaterThanWithException(Integer age) {
userRepository.deleteByAgeGreaterThan(age);
throw new RuntimeException("transaction test");
}
}
Do phương thức này có chú thích @Transactional(rollbackFor = Exception.class)
, khi gọi phương thức, Spring sẽ phát hiện ngoại lệ và thực hiện hồi ngược, nên dữ liệu không thực sự bị xóa:
// src/test/java/com/example/demo/service/UserServiceTest.java
package com.example.demo.service;
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testDeleteUserByAgeGreaterThanWithException() {
assertThrows(RuntimeException.class, () -> userService.deleteUserByAgeGreaterThanWithException(1));
}
}
Như vậy, bài viết đã giới thiệu về Spring Data Repository, chuẩn bị dữ liệu thử nghiệm và dự án ví dụ, cuối cùng trình bày cách sử dụng các chú thích và đặc điểm của Spring Data JPA thông qua mã nguồn minh họa. Mã nguồn của ví dụ này đã được đẩy lên GitHub cá nhân, mời theo dõi hoặc Fork.
[1] Spring Framework: Spring Data JPA | Spring - spring.io [2] Một bài viết giúp bạn hiểu rõ Spring Data JPA | Chuyên mục Zhihu - zhuanlan.zhihu.com