JPA (Java Persistence API) là một tiêu chuẩn kỹ thuật chính thức của Java cho việc lưu trữ và thao tác với dữ liệu trong cơ sở dữ liệu quan hệ một cách đơn giản và hiệu quả. Trong JPA, Specification là một API mạnh mẽ giúp xây dựng các truy vấn cơ sở dữ liệu một cách linh hoạt và hiệu quả. Trong bài viết này chúng ta cùng tìm hiểu về Specification trong JPA.

Giới thiệu về Specification trong JPA

Specification trong JPA là một phần của Spring Data JPA, cho phép xây dựng các câu truy vấn cơ sở dữ liệu một cách linh hoạt bằng cách kết hợp các tiêu chí tìm kiếm khác nhau. Điều này làm cho việc truy vấn dữ liệu trở nên hiệu quả và mềm dẻo hơn, đặc biệt khi chúng ta phải xử lý các truy vấn động với nhiều điều kiện khác nhau.

Cơ bản về Specification

Specification là một interface trong Spring Data JPA, định nghĩa một tập hợp các tiêu chí truy vấn, có thể tái sử dụng và kết hợp với nhau. Một Specification được xây dựng dựa trên API Criteria của JPA, cho phép chúng ta xác định các tiêu chí truy vấn cụ thể dưới dạng các đối tượng Java.

Lợi ích của Specification

  • Tái sử dụng: Các Specification có thể được tái sử dụng và kết hợp với nhau để tạo ra các truy vấn phức tạp.
  • Linh hoạt: Cho phép xây dựng các truy vấn linh động mà không cần viết lại mã nguồn.
  • Rõ ràng và dễ đọc: Các truy vấn được xây dựng dễ hiểu hơn, giúp cải thiện khả năng bảo trì và debug.

Ví dụ về Specification

Ví dụ 1: Book

Giả sử chúng ta có một ứng dụng quản lý sách và muốn tìm các sách dựa trên tiêu chí như tên sách, tác giả, và năm xuất bản.

Nếu không sử dụng Specification, thì để thực hiện tìm kiếm chúng ta phải làm như sau:

Định nghĩa Entity Book:

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Entity
@Table(name = "books")
@FieldDefaults(level = AccessLevel.PRIVATE)
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;

    @Column(nullable = false)
    String title;

    @Column(nullable = false)
    String author;

    @Column(name = "publish_year")
    Integer publishYear;
}
Book.java

Để tìm kiếm chúng ta cần định nghĩa các phương thức truy vấn cụ thể trong repository. Dưới đây là ví dụ về cách phương thức truy vấn

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Long> {
    List<Book> findByTitleContainingAndAuthorContainingAndPublishYear(String title, String author, Integer publishYear);
}
BookRepository.java

Sau đó, chúng ta có thể sử dụng phương thức này trong service của mình để thực hiện tìm kiếm:

@Autowired
private BookRepository bookRepository;

public List<Book> findBooks(String title, String author, Integer publishYear) {
    return bookRepository.findByTitleContainingAndAuthorContainingAndPublishYear(title, author, publishYear);
}

BookService.java

Khi sử dụng Specification, chúng ta không cần định nghĩa các phương thức truy vấn cụ thể như trên. Thay vào đó, chúng ta sẽ định nghĩa các Specification có thể kết hợp linh hoạt.

public class BookSpecifications {

    public static Specification<Book> titleContains(String title) {
        return (root, query, cb) -> cb.like(cb.lower(root.get("title")), "%" + title.toLowerCase() + "%");
    }

    public static Specification<Book> hasAuthor(String author) {
        return (root, query, cb) -> cb.equal(cb.lower(root.get("author")), author.toLowerCase());
    }

    public static Specification<Book> isPublishedInYear(Integer year) {
        return (root, query, cb) -> cb.equal(root.get("publishYear"), year);
    }
}
BookSpecifications.java

3. Sử dụng Specifications:

@Autowired
private BookRepository bookRepository;

public List<Book> findBooks(String author, String title, Integer publishYear) {
    Specification<Book> spec = Specification.where(null);

    if (author != null) {
        spec = spec.and(BookSpecifications.hasAuthor(author));
    }

    if (title != null) {
        spec = spec.and(BookSpecifications.titleContains(title));
    }

    if (publishYear != null) {
        spec = spec.and(BookSpecifications.isPublishedInYear(publishYear));
    }

    return bookRepository.findAll(spec);
}

Trong ví dụ trên, chúng ta đã tạo ba Specification riêng lẻ: một cho tác giả, một cho tiêu đề, và một cho năm xuất bản. Chúng được kết hợp một cách linh hoạt trong phương thức findBooks để tạo ra truy vấn cuối cùng dựa trên các tiêu chí được cung cấp.

Ví dụ 2: Movie

Chúng ta có entity Movie như sau:

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Entity
@Table(name = "movies")
@FieldDefaults(level = AccessLevel.PRIVATE)
public class Movie {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Integer id;

    String name;

    @Column(columnDefinition = "TEXT")
    String description;

    String poster;
    Integer releaseYear;
    Integer duration;
    Boolean status;

    @ManyToOne
    @JoinColumn(name = "country_id")
    Country country;

    @ManyToMany
    @JoinTable(
            name = "movie_genre",
            joinColumns = @JoinColumn(name = "movie_id"),
            inverseJoinColumns = @JoinColumn(name = "genre_id")
    )
    @Fetch(FetchMode.SUBSELECT)
    Set<Genre> genres = new LinkedHashSet<>();
}
Movie.java

Với ví dụ này chúng ta sẽ tìm kiếm movie theo các tiêu chí như: trạng thái, thể loại phim, quốc gia, năm phát hành, tiêu đề

Đầu tiên chúng ta cần định nghĩa MovieSpecifications để thực hiện logic tìm kiếm theo các tiêu chí trên

public class MovieSpecifications {

    public static Specification<Movie> findMovies(String genreName, String countryName, Integer releaseYear, String titleKeyword, Boolean status) {
        return (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();

            if (status != null) {
                predicates.add(cb.equal(root.get("status"), status));
            }

            if (genreName != null && !genreName.isEmpty()) {
                predicates.add(cb.equal(root.join("genres").get("name"), genreName));
            }

            if (countryName != null && !countryName.isEmpty()) {
                predicates.add(cb.equal(root.get("country").get("name"), countryName));
            }

            if (releaseYear != null) {
                predicates.add(cb.equal(root.get("releaseYear"), releaseYear));
            }

            if (titleKeyword != null && !titleKeyword.isEmpty()) {
                predicates.add(cb.like(root.get("name"), "%" + titleKeyword + "%"));
            }

            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }
}
MovieSpecifications.java

Trong ví dụ MovieSpecification, chúng ta xây dựng một Specification cho đối tượng Movie. Mục tiêu là tạo ra một truy vấn linh hoạt có thể áp dụng nhiều tiêu chí khác nhau khi lấy dữ liệu từ cơ sở dữ liệu:

  • Predicate List: Đầu tiên, chúng ta tạo một danh sách predicates để lưu trữ các điều kiện truy vấn (Predicate). Mỗi Predicate tương ứng với một điều kiện mà chúng ta muốn áp dụng trong truy vấn.
  • Kiểm tra Trạng Thái: Nếu status được cung cấp, một Predicate mới được thêm vào danh sách, kiểm tra nếu trường status của Movie bằng với giá trị đã cho.
  • Lọc theo Thể Loại: Khi một tên thể loại được cung cấp, chúng ta thực hiện một join với bảng genres và thêm một Predicate để kiểm tra tên thể loại.
  • Lọc theo Quốc Gia: Tương tự, nếu có tên quốc gia, một Predicate được thêm vào để lọc các bộ phim theo quốc gia.
  • Lọc theo Năm Phát Hành: Khi releaseYear được chỉ định, một điều kiện được thêm vào để kiểm tra trường releaseYear của bộ phim.
  • Tìm kiếm theo Từ Khóa trong Tiêu Đề: Nếu có từ khóa tiêu đề, một Predicate được tạo ra để tìm các bộ phim có tên chứa từ khóa này, sử dụng hàm like.
  • Kết hợp các Predicates: Cuối cùng, tất cả các Predicate được kết hợp lại bằng phương thức cb.and(), tạo ra một điều kiện truy vấn tổng thể để lọc dữ liệu.

Ví dụ: Sử dụng MovieSpecification

Giả sử chúng ta muốn tìm tất cả các bộ phim có trạng thái là true (ví dụ: đang hiển thị), thuộc thể loại “Hành động”, sản xuất ở “Mỹ”, phát hành trong năm 2021 và có từ khóa “Anh hùng” trong tiêu đề. Chúng ta có thể sử dụng MovieSpecification như sau:

@Autowired
private MovieRepository movieRepository;

public List<Movie> findMovies() {
    Specification<Movie> spec = MovieSpecification.findMovies("Hành động", "Mỹ", 2021, "Anh hùng", true);
    return movieRepository.findAll(spec);
}

Kết luận

Specification trong JPA là một công cụ mạnh mẽ, cho phép xây dựng các truy vấn linh hoạt và hiệu quả. Bằng cách sử dụng Specification, chúng ta có thể tăng cường khả năng tái sử dụng mã và làm cho mã nguồn của mình dễ đọc và bảo trì hơn.