Cách sử dụng Spring Data JPA để truy cập cơ sở dữ liệu - Link đăng nhập Saigon777 Tặng 50k

| Apr 6, 2025 min read

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> {}
  • CrudRepositoryListCrudRepository 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ểu Iterable<T> còn ListCrudRepository sử dụng kiểu List<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);
}
  • PagingAndSortingRepositoryListPagingAndSortingRepository 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ểu Iterable<T> còn ListPagingAndSortingRepository sử dụng kiểu List<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