0

Trọn bộ các annotation cơ bản cần phải biết trong Spring Boot (Phần 2)

"Bạn đã sẵn sàng đi tiếp chưa? Khi nền móng hệ thống đã vững vàng ở Phần 1, đây là lúc chúng ta tiến sâu vào 'hệ thần kinh trung ương' của ứng dụng. Phần 2 này sẽ tạm gác lại những khái niệm cơ bản để tập trung vào hiệu năng: từ việc chia tách các luồng xử lý nền, hẹn giờ chạy tự động cho đến bẻ lái hành vi hệ thống theo từng môi trường. Châm một ly cà phê, và cùng nâng cấp tư duy thiết kế của bạn lên một tầm cao mới nhé!"

Phần V: Tương tác Cơ sở dữ liệu (Spring Data JPA)

1. @Entity

Trong Spring Boot (cụ thể là khi sử dụng module Spring Data JPA và Hibernate), @Entity là một annotation cốt lõi dùng để đánh dấu một class Java là đại diện cho một Bảng (Table) trong cơ sở dữ liệu (Database).

Nếu @Controller là người tiếp tân, @Service là bộ não tính toán, @Repository là thủ kho, thì @Entity chính là bản thiết kế của món hàng mà thủ kho sẽ cất vào kho. Mọi đối tượng (object) được tạo ra từ class @Entity sẽ tương ứng với một dòng (row) trong bảng dữ liệu.

Cách sử dụng và Cấu trúc cơ bản

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;

@Entity
public class Product {

    @Id // BẮT BUỘC: Đánh dấu đây là Khóa chính (Primary Key)
    @GeneratedValue(strategy = GenerationType.IDENTITY) // Tự động tăng (Auto Increment)
    private Long id;

    private String name;
    private double price;

    // BẮT BUỘC: Phải có constructor mặc định không tham số (No-args constructor)
    public Product() {}

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    // Cần có các Getters và Setters để Hibernate đọc/ghi dữ liệu
}

Ba quy tắc "Sống còn" của @Entity

Theo chuẩn đặc tả của JPA (Java Persistence API), một class gắn @Entity bắt buộc phải tuân thủ 3 luật sau, nếu vi phạm ứng dụng sẽ báo lỗi ngay khi khởi động:

  1. Phải có Khóa chính (@Id): Mỗi bảng trong database phải có cách để phân biệt các dòng với nhau. Bạn phải gắn @Id lên ít nhất một thuộc tính.

  2. Phải có Constructor không tham số: Hibernate sử dụng công nghệ Reflection để tự động tạo ra các object khi lấy dữ liệu từ DB lên, quá trình này cần một constructor rỗng (public hoặc protected).

  3. Class không được là final: Hibernate cần tạo ra các class Proxy (kế thừa từ Entity của bạn) để thực hiện kỹ thuật Lazy Loading (tải dữ liệu lười biếng). Nếu class là final, nó không thể được kế thừa.

Cạm bẫy thực chiến: @Entity và Lombok (@Data)

Nếu bạn đang dùng thư viện Lombok để code cho ngắn, tuyệt đối KHÔNG nên dùng @Data chung với @Entity.

@Data tự động sinh ra các hàm equals(), hashCode(), và toString() bao gồm tất cả các thuộc tính. Khi Entity của bạn có quan hệ với Entity khác (ví dụ: User có danh sách Order, Order lại thuộc về User), việc gọi toString() sẽ tạo ra một vòng lặp vô tận (Infinite Recursion) gây tràn bộ nhớ (StackOverflowError) và sập ứng dụng.

Cách làm chuẩn mực (Best Practice) với Lombok:

import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
@Getter
@Setter
@NoArgsConstructor // Lombok tự sinh constructor rỗng
public class Employee {
    
    @Id
    private Long id;
    private String fullName;
}

2. @Table

Trong Spring Boot (JPA / Hibernate), nếu @Entity là lời tuyên bố "Class này sẽ được lưu thành một bảng", thì @Table chính là bản hướng dẫn chi tiết để nói với Database rằng: "Cái bảng đó tên chính xác là gì, nằm ở đâu, và có những quy tắc đặc biệt nào không?".

Mặc định, nếu bạn chỉ dùng @Entity mà không có @Table, Hibernate sẽ lấy chính tên của Class (hoặc đôi khi tự động chuyển sang dạng snake_case tùy cấu hình) để làm tên bảng. Tuy nhiên, trong thực tế dự án, chúng ta gần như luôn luôn sử dụng @Table để kiểm soát chặt chẽ cơ sở dữ liệu.

Mục đích phổ biến nhất: Đổi tên Bảng

Trong cơ sở dữ liệu (SQL), có rất nhiều từ khóa cấm (Reserved Keywords) như USER, ORDER, GROUP, TABLE, SELECT. Nếu class Java của bạn vô tình trùng tên với các từ khóa này, ứng dụng sẽ bị crash (báo lỗi SQL Syntax) khi khởi động.

@Table giúp bạn chỉ định một cái tên an toàn và chuẩn mực hơn cho Database (thường tuân theo quy tắc số nhiều và dùng dấu gạch dưới).

import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.Id;

@Entity
// Đổi tên bảng thành "tbl_users" hoặc "users" để tránh từ khóa cấm "USER"
@Table(name = "tbl_users") 
public class User {
    
    @Id
    private Long id;
    private String username;
}

Định nghĩa các quy tắc ràng buộc (Unique Constraints)

Đôi khi, bạn muốn đảm bảo rằng sự kết hợp của 2 cột không bao giờ được trùng lặp. Ví dụ: Trong một bảng EmployeeProject, một nhân viên chỉ được tham gia một dự án duy nhất 1 lần (nghĩa là cặp employee_idproject_id phải là duy nhất).

Bạn không thể dùng @Column(unique = true) vì nó chỉ áp dụng cho 1 cột đơn lẻ. Lúc này, bạn phải cấu hình ở cấp độ bảng bằng thuộc tính uniqueConstraints:

import jakarta.persistence.*;

@Entity
@Table(
    name = "employee_project",
    uniqueConstraints = {
        // Đảm bảo không có 2 dòng nào có cùng mã nhân viên VÀ mã dự án
        @UniqueConstraint(name = "uk_emp_proj", columnNames = {"employee_id", "project_id"})
    }
)
public class EmployeeProject {
    @Id
    private Long id;

    @Column(name = "employee_id")
    private Long employeeId;

    @Column(name = "project_id")
    private Long projectId;
}

Đánh chỉ mục (Indexes) để tăng tốc độ tìm kiếm

Nếu ứng dụng của bạn thường xuyên phải tìm kiếm (Query) theo một cột nào đó (ví dụ: tìm user theo số điện thoại hoặc email), việc đánh Index ở Database là bắt buộc để tránh hệ thống bị chậm khi dữ liệu phình to.

Bạn có thể ra lệnh cho Hibernate tự động tạo Index ở dưới DB thông qua thuộc tính indexes của @Table:

@Entity
@Table(
    name = "users",
    indexes = {
        // Tạo index trên cột email để tốc độ đăng nhập/tìm kiếm nhanh hơn
        @Index(name = "idx_user_email", columnList = "email"),
        // Tạo index trên nhiều cột cùng lúc
        @Index(name = "idx_first_last_name", columnList = "first_name, last_name")
    }
)
public class User {
    // ...
}

Chỉ định Schema hoặc Catalog (Dành cho DB lớn)

Trong các hệ thống lớn sử dụng PostgreSQL, Oracle hoặc SQL Server, một Database có thể được chia thành nhiều khu vực nhỏ gọi là Schema (ví dụ: schema cho hệ thống nhân sự hr, schema cho bán hàng sales).

Bạn có thể chỉ định rõ bảng này sẽ được tạo trong schema nào:Trong các hệ thống lớn sử dụng PostgreSQL, Oracle hoặc SQL Server, một Database có thể được chia thành nhiều khu vực nhỏ gọi là Schema (ví dụ: schema cho hệ thống nhân sự hr, schema cho bán hàng sales).

Bạn có thể chỉ định rõ bảng này sẽ được tạo trong schema nào:

@Entity
@Table(name = "salary_records", schema = "hr")
public class SalaryRecord {
    // Bảng này sẽ được lưu với tên đầy đủ là: hr.salary_records
}

💡 Lưu ý Thực chiến (Best Practice): Mặc định Spring Boot sử dụng một chiến lược đặt tên gọi là CamelCaseToUnderscoresNamingStrategy. Tức là nếu bạn có class tên là UserProfile và bạn KHÔNG dùng @Table, nó sẽ tự động tạo bảng tên là user_profile ở DB. Tuy nhiên, để code rõ ràng, dễ bảo trì, và an toàn khi chuyển đổi qua lại giữa các loại Database (MySQL, Oracle, SQL Server...), các đội ngũ dự án thường thống nhất luôn luôn viết @Table(name = "...") một cách tường minh thay vì dựa dẫm vào tính năng tự động của Spring.

3. @Id

Trong Spring Boot (JPA / Hibernate), @Id là một annotation (thuộc package jakarta.persistence) mang ý nghĩa sống còn: Nó được dùng để đánh dấu một thuộc tính (field) đóng vai trò là Khóa chính (Primary Key) của bảng trong cơ sở dữ liệu.

Theo đặc tả của JPA, mọi class được gắn @Entity đều BẮT BUỘC phải có ít nhất một trường được đánh dấu @Id. Nếu không, ứng dụng sẽ báo lỗi (crash) ngay khi vừa khởi động.

Dưới đây là chi tiết về cách sử dụng @Id và các chiến lược tạo giá trị tự động (Auto-generation) đi kèm với nó.

Sử dụng @Id cơ bản (Khóa chính tự gán)

Nếu bạn chỉ dùng một mình annotation @Id, điều đó có nghĩa là bạn (hoặc người dùng) sẽ tự tay cung cấp giá trị cho khóa chính này trước khi lưu xuống Database.

Thường được dùng cho các trường hợp mã (code) do con người hoặc hệ thống bên ngoài tự định nghĩa (ví dụ: Mã sinh viên, Mã số thuế, CCCD).

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class Employee {

    @Id // Đánh dấu đây là Khóa chính
    private String employeeCode; // Ví dụ: "EMP-001" (Người dùng tự nhập)

    private String fullName;
}

Lưu ý: Nếu bạn gọi lệnh save() mà quên set giá trị cho employeeCode, hệ thống sẽ văng lỗi vì Khóa chính không bao giờ được phép null.

Sinh khóa tự động với @GeneratedValue

Trong 90% trường hợp thực tế, chúng ta không tự nhập Khóa chính mà muốn Database tự động sinh ra một con số (tăng dần 1, 2, 3...) mỗi khi có một dòng dữ liệu mới.

Để làm được điều này, bạn phải kết hợp @Id với annotation @GeneratedValue. Spring Boot (Hibernate) cung cấp 4 chiến lược (Strategy) tạo khóa chính như sau:

a. GenerationType.IDENTITY (Phổ biến nhất)

Chiến lược này giao toàn quyền sinh khóa cho Database bằng cột tự tăng (ví dụ: AUTO_INCREMENT trong MySQL, PostgreSQL). Hibernate sẽ gửi lệnh INSERT trắng trường ID, DB tự sinh ID và trả về cho Hibernate.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // ID tự nhảy 1, 2, 3...

👉 Khuyên dùng cho: Các database hỗ trợ AUTO_INCREMENT như MySQL, SQL Server, PostgreSQL.

b. GenerationType.SEQUENCE (Tối ưu hiệu năng cho DB lớn)

Thay vì gắn trực tiếp thuộc tính tự tăng vào bảng, chiến lược này sử dụng một "bộ đếm" độc lập trong Database gọi là Sequence. Hibernate có thể lấy trước (fetch) 50 cái ID một lúc từ Sequence mang về bộ nhớ Java, sau đó chèn dần vào DB giúp tăng tốc độ insert cực nhanh (kỹ thuật batch insert).

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
@SequenceGenerator(name = "user_seq", sequenceName = "my_user_sequence", allocationSize = 50)
private Long id;

👉 Khuyên dùng cho: Oracle, PostgreSQL (Các hệ thống lớn cần Insert hàng vạn record cùng lúc).

c. GenerationType.AUTO (Mặc định)

Nếu bạn không ghi thuộc tính strategy, Spring sẽ dùng AUTO. Ở chế độ này, Hibernate sẽ tự động nhìn vào loại Database bạn đang dùng và tự chọn chiến lược phù hợp (thường là SEQUENCE hoặc tạo hẳn một bảng riêng để lưu số đếm). 👉 Lời khuyên: Không nên dùng. Hãy chỉ định rõ ràng IDENTITY hoặc SEQUENCE để kiểm soát tốt nhất hệ thống của bạn.

d. Sinh mã UUID (Unique ngẫu nhiên)

Nếu dự án của bạn là hệ thống phân tán (Microservices) hoặc bạn không muốn người ngoài đoán được số lượng user bằng cách nhìn vào ID (vì ID 1, 2, 3 rất dễ đoán), người ta thường dùng UUID (Một chuỗi 36 ký tự cực kỳ phức tạp).

import java.util.UUID;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;

@Id
@GeneratedValue(strategy = GenerationType.UUID) // Từ Hibernate 6 (Spring Boot 3)
private UUID id; 
// Kết quả: 550e8400-e29b-41d4-a716-446655440000

Vấn đề thực chiến: Khóa chính phức hợp (Composite Key)

Đôi khi, khóa chính của bạn không phải là 1 cột, mà là sự kết hợp của 2 cột (Ví dụ: Bảng StudentSubject điểm danh có khóa chính là student_idsubject_id).

Bạn KHÔNG THỂ gắn 2 chữ @Id vào 2 biến riêng lẻ được. Để giải quyết bài toán này, bạn cần sử dụng một trong hai kỹ thuật là @IdClass hoặc @EmbeddedId.

Ví dụ sử dụng @EmbeddedId:

// BƯỚC 1: Tạo một class chứa 2 ID
@Embeddable
public class StudentSubjectId implements Serializable {
    private Long studentId;
    private Long subjectId;
}

// BƯỚC 2: Sử dụng trong Entity
@Entity
public class StudentSubject {
    
    @EmbeddedId // Đánh dấu thuộc tính này là Khóa chính phức hợp
    private StudentSubjectId id;

    private Double score;
}

4. @Column

Trong Spring Boot (JPA / Hibernate), nếu @Entity là bản thiết kế của cái Bảng, thì @Column (thuộc package jakarta.persistence) chính là bản vẽ chi tiết cho từng Cột (Column) trong bảng đó.

Mặc định, nếu bạn khai báo một thuộc tính trong class @Entity (như String firstName), Hibernate sẽ tự động tạo một cột trong Database. Spring Boot có cơ chế ngầm định tự động chuyển đổi định dạng tên từ camelCase (chữ lạc đà) sang snake_case (rắn cắn), tức là firstName sẽ thành cột first_name.

Tuy nhiên, khi bạn muốn can thiệp sâu hơn vào cấu trúc của cột (độ dài, bắt buộc nhập, duy nhất,...), bạn sẽ cần sử dụng @Column.

Các thuộc tính (Attributes) thường dùng nhất

Dưới đây là những tùy chỉnh bạn sẽ gặp ở hầu hết các dự án thực tế:

a. name - Đổi tên cột

// Bắt Hibernate phải map với cột tên là "usr_email_address" thay vì "email"
@Column(name = "usr_email_address")
private String email;

b. nullable - Bắt buộc nhập (NOT NULL)

Mặc định nullable = true (cho phép trống). Khi đổi thành false, cột đó trong Database sẽ được gắn cờ NOT NULL. Nếu bạn cố lưu một object có trường này bằng null, ứng dụng sẽ văng lỗi.

@Column(nullable = false)
private String password;

c. unique - Giá trị duy nhất (UNIQUE)

Đảm bảo không có 2 dòng nào trong bảng có cùng một giá trị ở cột này. Thường dùng cho Email, Số điện thoại, CCCD.

@Column(unique = true)
private String phoneNumber;

d. length - Độ dài chuỗi (VARCHAR size)

Chỉ áp dụng cho kiểu String. Mặc định Hibernate sẽ tạo cột VARCHAR(255). Bạn nên tinh chỉnh lại cho chuẩn với thiết kế.

@Column(length = 10) // Sẽ tạo thành VARCHAR(10) trong DB
private String zipCode;

Các thuộc tính nâng cao (Dành cho Logic nghiệp vụ)

a. updatableinsertable - Khóa thao tác

Đôi khi có những cột bạn chỉ muốn lưu lần đầu tiên lúc tạo mới, và vĩnh viễn không bao giờ được phép sửa đổi (ví dụ: ngày tạo tài khoản, người tạo).

  • updatable = false: Lệnh UPDATE của SQL sẽ bỏ qua cột này.
  • insertable = false: Lệnh INSERT của SQL sẽ bỏ qua cột này.
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt; // Cột này không bao giờ bị sửa sau khi đã tạo

b. columnDefinition - Định nghĩa SQL "hardcode"

Nếu bạn muốn dùng một kiểu dữ liệu đặc thù của riêng loại Database bạn đang dùng (ví dụ kiểu JSON trong MySQL, kiểu TEXT siêu dài), hoặc muốn set giá trị Default dưới DB.

// Ép Hibernate phải tạo cột bằng câu lệnh SQL chuẩn xác này
@Column(columnDefinition = "TEXT")
private String description;

@Column(columnDefinition = "boolean default true")
private Boolean isActive;

(Lưu ý: Hạn chế lạm dụng columnDefinition vì nó làm mất đi tính "độc lập CSDL" của Hibernate. Nếu sau này dự án đổi từ MySQL sang Oracle, câu lệnh SQL bị hardcode này có thể gây lỗi).

Ví dụ thực chiến tổng hợp

Khi kết hợp tất cả lại, một Entity chuẩn mực trong dự án thực tế sẽ trông như thế này:

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // Tên cột là username, không được null, không được trùng, dài tối đa 50 ký tự
    @Column(name = "username", nullable = false, unique = true, length = 50)
    private String username;

    // Password bắt buộc phải có
    @Column(nullable = false)
    private String password;

    // Email không được trùng, độ dài mặc định là 255
    @Column(unique = true)
    private String email;

    // Ngày tạo: Không được null, không cho phép update sau khi đã lưu lần đầu
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    // Dùng kiểu TEXT của database thay vì VARCHAR
    @Column(columnDefinition = "TEXT")
    private String bio;
}

Sau khi đã thiết kế xong Bảng và Cột, mảnh ghép quan trọng nhất của Database Relational (Cơ sở dữ liệu quan hệ) là cách các bảng liên kết với nhau.

5. @OneToOne, @OneToMany, @ManyToOne, @ManyToMany

Bốn annotation @OneToOne, @OneToMany, @ManyToOne@ManyToMany là trái tim của JPA/Hibernate. Chúng giúp chuyển đổi mô hình Cơ sở dữ liệu quan hệ (Relational Database) với các Khóa ngoại (Foreign Key) thành thế giới Lập trình hướng đối tượng (OOP) của Java.

Để làm chủ được chúng, bạn cần nhớ một nguyên tắc bất di bất dịch: Trong một mối quan hệ luôn có "Bên chủ" (Owner) và "Bên phụ" (Inverse). Bên chủ là bên nắm giữ Khóa ngoại trong Database.

1. @OneToOne (Quan hệ Một - Một)

Kịch bản: Mỗi User (Người dùng) có đúng 1 UserProfile (Hồ sơ chi tiết), và ngược lại.

Cách thiết lập: Chúng ta quyết định bảng User sẽ giữ khóa ngoại profile_id. Vậy User là Bên chủ.

// BÊN CHỦ (Nắm giữ khóa ngoại)
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // @JoinColumn ra lệnh: Tạo một cột tên là "profile_id" trong bảng user
    @OneToOne
    @JoinColumn(name = "profile_id") 
    private UserProfile profile;
}

// BÊN PHỤ (Không chứa khóa ngoại)
@Entity
public class UserProfile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // mappedBy = "profile" nghĩa là: Hãy nhìn vào biến 'profile' bên class User 
    // để biết cách liên kết. Đừng tạo thêm cột nào ở bảng user_profile cả.
    @OneToOne(mappedBy = "profile")
    private User user;
}

2. @OneToMany & @ManyToOne (Quan hệ Một - Nhiều / Nhiều - Một)

Đây là mối quan hệ phổ biến nhất (chiếm 80% cấu trúc DB).

Kịch bản: 1 Category (Danh mục) có Nhiều Product (Sản phẩm). 1 Product thuộc về 1 Category.

Nguyên tắc Database: Khóa ngoại luôn luôn nằm ở bảng "Nhiều" (bảng Product). Vậy Product bắt buộc phải là Bên chủ.

// BÊN CHỦ - BẢNG "NHIỀU" (Nắm giữ khóa ngoại)
@Entity
public class Product {
    @Id
    private Long id;

    // Nhiều Product thuộc về 1 Category
    // Tạo cột "category_id" trong bảng product
    @ManyToOne
    @JoinColumn(name = "category_id") 
    private Category category;
}

// BÊN PHỤ - BẢNG "MỘT"
@Entity
public class Category {
    @Id
    private Long id;

    // 1 Category có Nhiều Product
    // mappedBy = "category" -> Ánh xạ theo biến 'category' bên class Product
    @OneToMany(mappedBy = "category")
    private List<Product> products;
}

Lưu ý: Nếu bạn quên chữ mappedBy@OneToMany, Hibernate sẽ tưởng đây là một quan hệ độc lập và tự động sinh ra một bảng thứ 3 (bảng trung gian) làm rác Database.

3. @ManyToMany (Quan hệ Nhiều - Nhiều)

Kịch bản: 1 Student (Sinh viên) học nhiều Course (Khóa học), và 1 Course có nhiều Student.

Trong Database, quan hệ Nhiều-Nhiều bắt buộc phải có một bảng trung gian (Join Table) để nối 2 bảng này lại (ví dụ bảng student_course).

// CHỌN ĐẠI MỘT BÊN LÀM CHỦ (Ví dụ chọn Student)
@Entity
public class Student {
    @Id
    private Long id;

    @ManyToMany
    @JoinTable(
        name = "student_course", // Tên bảng trung gian
        joinColumns = @JoinColumn(name = "student_id"), // Khóa ngoại trỏ về Student
        inverseJoinColumns = @JoinColumn(name = "course_id") // Khóa ngoại trỏ về Course
    )
    private List<Course> courses;
}

// BÊN PHỤ
@Entity
public class Course {
    @Id
    private Long id;

    @ManyToMany(mappedBy = "courses") // Ánh xạ theo biến 'courses' bên class Student
    private List<Student> students;
}

4. Hai thuộc tính "Sống còn": FetchTypeCascadeType

Khi khai báo các quan hệ trên, bạn sẽ luôn phải đối mặt với 2 cấu hình đi kèm rất quan trọng:

a. Kéo dữ liệu thế nào? (FetchType.LAZY vs FetchType.EAGER)

Khi bạn query lấy 1 Category từ Database, Spring Boot có nên tự động query lấy luôn danh sách hàng nghìn Products của nó lên bộ nhớ không?

  • EAGER (Tham lam): Lấy tất cả lên ngay lập tức. Dễ gây tràn RAM và chậm hệ thống. (Mặc định của @ManyToOne@OneToOne).

  • LAZY (Lười biếng): Chỉ lấy dữ liệu của Category. Danh sách Products sẽ bị bỏ trống, và nó CHỈ được query lấy từ DB khi nào code của bạn thực sự gọi hàm category.getProducts(). (Mặc định của @OneToMany@ManyToMany).

Quy tắc vàng: Ở các dự án lớn, luôn chủ động set tất cả các quan hệ về FetchType.LAZY để tối ưu hiệu năng.

@ManyToOne(fetch = FetchType.LAZY) // Ép về Lazy để tối ưu
@JoinColumn(name = "category_id")
private Category category;

Hiệu ứng dây chuyền (CascadeType)

Khi bạn Xóa (hoặc Lưu) Category, bạn có muốn Spring Boot tự động Xóa (hoặc Lưu) tất cả Products thuộc về nó không?

  • CascadeType.ALL: Áp dụng mọi thao tác dây chuyền (Lưu, Xóa, Cập nhật).
  • CascadeType.REMOVE: Parent bị xóa thì Child cũng bị xóa theo.
// Khi 1 Category bị xóa, toàn bộ Product của nó sẽ bị xóa sạch khỏi DB
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL)
private List<Product> products;

6. @JoinColumn

Trong Spring Boot (JPA / Hibernate), nếu @ManyToOne hay @OneToOne là bản thỏa thuận giữa hai Class Java rằng chúng có liên quan đến nhau, thì @JoinColumn chính là chiếc đinh ghim xác định vị trí chính xác của Khóa ngoại (Foreign Key) ở dưới Database.

Nói cách khác, @JoinColumn là cách bạn ra lệnh cho Hibernate: "Hãy tạo một cột trong bảng này, dùng nó làm Khóa ngoại để trỏ sang bảng kia".

Vị trí đặt @JoinColumn (Quy tắc "Bên Chủ")

Nguyên tắc tối thượng: @JoinColumn LUÔN LUÔN được đặt ở "Bên Chủ" (Owning Side) của mối quan hệ. Bên chủ chính là bảng thực tế trong Database sẽ chứa cột Khóa ngoại.

  • Trong quan hệ @ManyToOne: Bên "Nhiều" luôn là bên chủ (Ví dụ: Bảng Product chứa category_id).

  • Trong quan hệ @OneToOne: Bạn tự quyết định bảng nào chứa khóa ngoại, bảng đó sẽ gắn @JoinColumn.

  • Trong quan hệ @OneToMany: Rất hiếm khi dùng @JoinColumn trực tiếp (vì khóa ngoại nằm ở bảng kia). Thay vào đó, ta dùng mappedBy.

Các thuộc tính quan trọng nhất

Giống như @Column, @JoinColumn cung cấp các công cụ để bạn thiết kế vật lý cho cột Khóa ngoại:

a. name (Tên cột Khóa ngoại)

  • Xác định tên của cột trong Database.
  • Lưu ý: Nếu bạn không dùng @JoinColumn, Hibernate sẽ tự động sinh ra tên cột theo công thức: tên_biến + "" + tên_khóa_chính_bảng_kia (ví dụ: categoryid). Tuy nhiên, best practice là luôn khai báo rõ ràng.

b. referencedColumnName (Cột đích trỏ tới)

  • Mặc định, Khóa ngoại sẽ trỏ tới Khóa chính (@Id) của bảng kia. Nếu bạn muốn khóa ngoại trỏ tới một cột khác (không phải khóa chính, nhưng cột đó phải là Unique), bạn dùng thuộc tính này.

c. nullable (Bắt buộc phải có liên kết không?)

  • Nếu nullable = false, khi bạn lưu Product mà không có Category, Database sẽ chặn lại và báo lỗi.

d. unique (Đảm bảo quan hệ 1-1)

  • Thường được sử dụng đi kèm với @OneToOne để chắc chắn rằng không có 2 dòng nào dùng chung một Khóa ngoại.

Ví dụ thực chiến

Kịch bản: Một Sản phẩm (Product) bắt buộc phải thuộc về một Danh mục (Category). Cột khóa ngoại sẽ tên là cat_id.

@Entity
@Table(name = "product")
public class Product {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // Đây là "Bên Chủ"
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(
        name = "cat_id",                // Tên cột khóa ngoại trong bảng product
        referencedColumnName = "id",    // Trỏ tới cột 'id' của bảng category (có thể bỏ trống vì là mặc định)
        nullable = false                // Bắt buộc product phải có category
    )
    private Category category;
}

Phân biệt @JoinColumn vs mappedBy

Đây là "cạm bẫy" lớn nhất mà 99% người học JPA đều ít nhất một lần mắc phải. Hãy nhớ rằng chúng là hai thái cực đối lập nhau và KHÔNG BAO GIỜ được đặt chung trên một biến.

Tiêu chí @JoinColumn mappedBy (Bên trong @OneToMany / @OneToOne)
Vai trò "Tôi là chủ. Hãy tạo Khóa ngoại ở bảng của tôi." "Tôi là phụ. Khóa ngoại nằm ở bảng kia kìa, hãy nhìn theo biến đó."
Hành động ở DB Tạo ra một cột vật lý trong bảng hiện tại. KHÔNG tạo ra cột nào trong bảng hiện tại.
Vị trí Đặt ở Bên chứa khóa ngoại (thường là @ManyToOne). Đặt ở Bên không chứa khóa ngoại (thường là @OneToMany)

Ví dụ sửa lỗi sai kinh điển:

// LỖI SAI: Đặt cả 2 cái ở bảng Category
@Entity
public class Category {
    // SAI HOÀN TOÀN: Đã mappedBy (nhường quyền) thì không được dùng JoinColumn (đòi quyền)
    @OneToMany(mappedBy = "category")
    @JoinColumn(name = "category_id") 
    private List<Product> products;
}

// CÁCH VIẾT ĐÚNG:
@Entity
public class Category {
    // Chỉ dùng mappedBy. Báo cho Hibernate biết khóa ngoại nằm ở biến 'category' của class Product
    @OneToMany(mappedBy = "category")
    private List<Product> products;
}

Phần VI: Quản lý Giao dịch (Transaction Management)

1. @Transactional

Trong Spring Boot, @Transactional là một trong những annotation quyền lực và quan trọng nhất khi làm việc với cơ sở dữ liệu. Nó được dùng để quản lý Giao dịch (Transaction).

Hiểu đơn giản, một giao dịch là một chuỗi các thao tác với Database. @Transactional đảm bảo quy tắc "Tất cả hoặc Không gì cả" (All-or-Nothing): Hoặc là toàn bộ chuỗi thao tác thành công (Commit), hoặc nếu có bất kỳ lỗi nào xảy ra ở giữa chừng, toàn bộ dữ liệu sẽ được hoàn tác về trạng thái ban đầu (Rollback).

Bài toán thực tế: Chuyển tiền ngân hàng

Giả sử bạn có hàm chuyển tiền từ tài khoản A sang tài khoản B, gồm 2 bước:

  1. Trừ 500k của A.

  2. Cộng 500k cho B.

Nếu sau khi trừ tiền của A xong, máy chủ bị sập hoặc có lỗi xảy ra khiến bước 2 không thực hiện được. Kết quả: A mất tiền, B không nhận được. Dữ liệu bị sai lệch nghiêm trọng.

Giải pháp: Đặt @Transactional lên đầu hàm.

import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Service;

@Service
public class BankService {

    @Transactional
    public void transferMoney(Long accountA, Long accountB, double amount) {
        // Bước 1: Trừ tiền A
        accountRepository.deductBalance(accountA, amount);
        
        // Cố tình gây lỗi (Ví dụ: chia cho 0, hoặc lỗi kết nối DB)
        // int error = 1 / 0; 

        // Bước 2: Cộng tiền B
        accountRepository.addBalance(accountB, amount);
        
        // Nếu có lỗi ném ra, Spring sẽ tự động Rollback bước 1. Tiền của A vẫn còn nguyên.
    }
}

Hai "cạm bẫy" chết người khi dùng @Transactional

Trong thực tế, 80% lỗi liên quan đến @Transactional đến từ việc lập trình viên không hiểu rõ cơ chế ngầm (Proxy) của nó. Dưới đây là 2 lỗi kinh điển nhất:

Cạm bẫy 1: Quên cấu hình rollbackFor (Lỗi Rollback hụt)

Mặc định, @Transactional CHỈ thực hiện Rollback khi gặp Unchecked Exception (các lỗi kế thừa từ RuntimeException như NullPointerException, IllegalArgumentException).

Nếu code của bạn ném ra một Checked Exception (các lỗi bắt buộc phải có try-catch hoặc throws như IOException, SQLException), Spring sẽ... bỏ qua và vẫn lưu dữ liệu (Commit).

Cách khắc phục: Luôn tạo thói quen khai báo rollbackFor = Exception.class để bắt mọi loại lỗi.

// Ép Spring phải Rollback với TẤT CẢ các loại Exception
@Transactional(rollbackFor = Exception.class)
public void importFile() throws IOException {
    // ... logic đọc file và lưu DB
}

Cạm bẫy 2: Lỗi gọi hàm nội bộ (Self-Invocation / Proxy Bypass)

Bản chất của @Transactional là Spring sẽ tạo ra một lớp "môi giới" (Proxy) bọc ngoài class của bạn. Khi ai đó gọi hàm từ bên ngoài vào, nó phải đi qua Proxy, Proxy sẽ mở Transaction.

Tuy nhiên, nếu bạn gọi một hàm @Transactional từ một hàm khác nằm trong cùng một class, lệnh gọi đó không đi qua Proxy. Kết quả: @Transactional bị vô hiệu hóa hoàn toà

@Service
public class OrderService {

    // Hàm này KHÔNG có @Transactional
    public void createOrder() {
        System.out.println("Bắt đầu tạo đơn...");
        
        // GỌI HÀM NỘI BỘ TRONG CÙNG CLASS
        // Lỗi: Transaction sẽ KHÔNG được kích hoạt!
        saveToDatabase(); 
    }

    @Transactional
    public void saveToDatabase() {
        // ... logic lưu DB
    }
}

Cách khắc phục: Chuyển hàm saveToDatabase sang một Class/Service khác, hoặc gọi trực tiếp từ Controller vào hàm có sẵn @Transactional.

Điều hướng giao dịch (Propagation)

Khi một hàm có @Transactional gọi một hàm khác cũng có @Transactional, Spring sẽ xử lý thế nào? Đây gọi là tính lan truyền (Propagation).

Có 7 loại cấu hình Propagation, nhưng bạn chỉ cần nhớ 2 loại dùng nhiều nhất:

  • Propagation.REQUIRED (Mặc định): "Nhập gia tùy tục". Nếu bên ngoài chưa có giao dịch, nó tạo mới. Nếu bên ngoài đã có, nó sẽ "hòa chung" vào giao dịch bên ngoài. Chỉ cần 1 chỗ lỗi, TẤT CẢ cùng Rollback.

  • Propagation.REQUIRES_NEW: "Khu tự trị". Luôn luôn tạm dừng giao dịch bên ngoài (nếu có) và tạo ra một giao dịch mới hoàn toàn độc lập. Giao dịch mới lỗi chỉ rollback chính nó, không ảnh hưởng đến cái bên ngoài.

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog() {
    // Lưu lịch sử thao tác. 
    // Dù giao dịch chính bên ngoài có bị lỗi và rollback, 
    // hàm lưu log này vẫn được commit vào DB.
}

Vị trí đặt @Transactional chuẩn mực

  • Nên đặt ở Tầng Service (@Service): Vì đây là nơi chứa Logic nghiệp vụ (Business Logic). Một tính năng thường gọi nhiều lệnh từ Database (Repository), đặt ở Service giúp bao bọc tất cả trong 1 giao dịch.

  • Có thể đặt ở Class hoặc Method: Nếu đặt trên đầu class, mọi hàm public trong class đó đều tự động có Transaction. Nếu đặt ở hàm, nó sẽ ghi đè cấu hình của class. (Chỉ có tác dụng với hàm public, các hàm private hay protected sẽ bị Spring ngó lơ).

Phần VII: Lập lịch & Xử lý Bất đồng bộ (Scheduling & Async)

1. @EnableScheduling

Trong Spring Boot, @EnableScheduling đóng vai trò là "Công tắc tổng" để bật tính năng Hẹn giờ / Lập lịch chạy ngầm (Scheduled Tasks / Cron Jobs).

Nếu ứng dụng của bạn cần thực hiện những công việc tự động lặp đi lặp lại như: gửi email báo cáo vào 8h sáng mỗi ngày, quét dọn file rác mỗi 30 phút, hoặc cập nhật tỷ giá ngân hàng mỗi 5 giây, bạn sẽ cần đến annotation này.

Kích hoạt tính năng (Bật công tắc)

Để sử dụng lập lịch, trước tiên bạn phải gắn @EnableScheduling vào một class cấu hình (có @Configuration) hoặc gắn trực tiếp lên class Main (class chứa hàm main của Spring Boot).

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling // BẮT BUỘC CÓ: Bật tính năng chạy ngầm cho toàn hệ thống
public class MySpringBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(MySpringBootApplication.class, args);
    }
}

2. @Scheduled

Trong Spring Boot, annotation chính xác được sử dụng để lập lịch chạy tác vụ là @Scheduled (thuộc module Spring Task Scheduling). Nó cho phép bạn thực thi một method tự động lặp đi lặp lại theo một chu kỳ thời gian nhất định (như 5 phút một lần) hoặc vào một thời điểm cụ thể (như 12h đêm mỗi ngày), tương tự như Cron job trong Linux.

Ứng dụng phổ biến: Dọn dẹp dữ liệu rác trong database mỗi đêm, đồng bộ dữ liệu với hệ thống khác mỗi giờ, hoặc gửi email tổng kết hàng ngày.

Định nghĩa các tác vụ với @Scheduled

Sau khi đã bật công tắc tổng, bạn có thể biến bất kỳ hàm (method) nào bên trong các Spring Bean (@Service, @Component) thành một tác vụ tự động bằng cách gắn thêm annotation @Scheduled. Lưu ý bắt buộc: Method này phải trả về void và không được có tham số truyền vào.

Có 3 chiến lược lập lịch phổ biến nhất:

a. fixedRate (Chạy lặp lại theo nhịp độ cố định)

Hệ thống sẽ đếm thời gian kể từ lúc bắt đầu của lần chạy trước. (Tính bằng mili-giây).

@Service
public class ReportService {
    
    // Cứ đều đặn mỗi 5000ms (5 giây) sẽ kích hoạt hàm này một lần.
    @Scheduled(fixedRate = 5000)
    public void generateReport() {
        System.out.println("Đang xuất báo cáo định kỳ...");
    }
}

b. fixedDelay (Chạy lặp lại với thời gian nghỉ cố định)

Hệ thống sẽ đếm thời gian kể từ lúc kết thúc của lần chạy trước. Nó đảm bảo tác vụ cũ phải làm xong hoàn toàn, nghỉ ngơi đủ thời gian thì mới chạy tiếp.

@Service
public class CleanupService {

    // Chờ hàm chạy xong, nghỉ chính xác 10 giây (10000ms) rồi mới chạy lần tiếp theo.
    @Scheduled(fixedDelay = 10000)
    public void clearTempFiles() {
        System.out.println("Đang dọn rác... (Có thể mất thời gian lâu)");
    }
}

c. cron (Hẹn giờ chính xác theo lịch lịch)

Đây là cách mạnh mẽ nhất, sử dụng biểu thức Cron (Cron Expression) để chỉ định chính xác giờ, ngày, tháng chạy tác vụ.

Cấu trúc Cron trong Spring gồm 6 tham số: Giây Phút Giờ Ngày Tháng Thứ.

@Service
public class EmailService {

    // Chạy vào đúng 08:00:00 sáng mỗi ngày
    @Scheduled(cron = "0 0 8 * * *")
    public void sendMorningEmail() {
        System.out.println("Gửi email chào buổi sáng!");
    }

    // Chạy vào lúc 12:00:00 trưa vào các ngày Thứ 2 đến Thứ 6 hàng tuần
    @Scheduled(cron = "0 0 12 * * MON-FRI")
    public void sendWorkReminder() {
        System.out.println("Nhắc nhở công việc trưa");
    }
}

Cạm bẫy "Tắc đường" (Single Thread Pitfall)

Đây là lỗi thực chiến mà rất nhiều người gặp phải.

Theo mặc định, Spring Boot chỉ cấp phát Duy nhất một Luồng (Single Thread) để chạy tất cả các tác vụ @Scheduled trong hệ thống của bạn. Điều này có nghĩa là:

  • Nếu bạn có Task A và Task B cùng đến giờ chạy.
  • Nhưng Task A xử lý quá nặng và mất 10 phút mới xong.
  • Thì Task B bắt buộc phải "xếp hàng" chờ đủ 10 phút đó mới được chạy (dù lịch của nó đã đến từ lâu).

Cách giải quyết: Bạn phải mở rộng Thread Pool (Hồ bơi luồng) cho Scheduler bằng cách thêm cấu hình vào file application.properties:

# Yêu cầu Spring cấp tối đa 10 luồng để chạy các tác vụ lập lịch đồng thời
spring.task.scheduling.pool.size=10

Bí kíp: Xử lý khi fixedRate bị "tắc nghẽn"

Khác với cạm bẫy "Tắc đường" ở trên đó là các tác vụ khác nhau chặn nhau, thế nhưng nếu một luồng tự chặn chính nó thì sao. Cấu hình spring.task.scheduling.pool.size là chưa đủ để giải quyết bài toán này

Như đã biết, fixedRate yêu cầu tác vụ phải chạy đều đặn. Ví dụ: fixedRate = 5000 (cứ 5 giây chạy 1 lần).

  • Lúc 0s: Chạy lần 1.
  • Lúc 5s: Chạy lần 2.
  • Lúc 10s: Chạy lần 3.

Câu hỏi đặt ra: Điều gì xảy ra nếu lần 1 phải xử lý quá nhiều dữ liệu và mất tới 8 giây mới xong? Lần 2 có được chạy đúng vào giây thứ 5 không?

Trả lời: Theo mặc định là KHÔNG. Lần 2 sẽ phải đợi lần 1 xong xuôi (ở giây thứ 8) rồi mới lật đật chạy bù. Nhịp độ bị phá vỡ.

Để giải quyết và ép hệ thống phải chạy song song đúng nhịp độ (lần 1 chưa xong kệ lần 1, đến giây thứ 5 hệ thống tự mở luồng khác để chạy lần 2), bạn phải kết hợp @Scheduled với annotation @Async (Bất đồng bộ).

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;

// Lưu ý: Phải gắn thêm @EnableAsync ở class Main hoặc class Configuration

@Service
public class HeavyTaskService {

    @Async // Tách ra một luồng riêng biệt
    @Scheduled(fixedRate = 5000)
    public void processHeavyData() throws InterruptedException {
        System.out.println("Bắt đầu xử lý... Luồng: " + Thread.currentThread().getName());
        
        // Giả lập tác vụ mất 8 giây
        Thread.sleep(8000); 
        
        System.out.println("Xong!");
    }
}

Kết quả: Nhờ @Async, mỗi khi đến hạn 5 giây, Spring sẽ ném tác vụ đó cho một Thread mới xử lý, đảm bảo nhịp độ fixedRate luôn chính xác tuyệt đối mà không bị tắc nghẽn bởi các lần chạy trước.

Tóm lại

  • pool.size=10 dùng để các Task khác nhau không chặn nhau.
  • @Async dùng để các lần chạy của Cùng một Task không chặn nhau.

Giải quyết vấn đề Múi giờ (zone) với Cron

Nếu bạn dùng cron, thời gian mặc định sẽ được tính theo múi giờ của hệ điều hành máy chủ (Server). Điều này rất nguy hiểm nếu Server của bạn đặt ở Mỹ (AWS, Google Cloud) nhưng bạn lại muốn gửi email báo cáo vào 8h sáng giờ Việt Nam.

Thuộc tính zone sẽ giúp bạn chốt cứng múi giờ.

// Bất kể server đặt ở đâu, đúng 08:00 sáng giờ Việt Nam mới chạy
@Scheduled(cron = "0 0 8 * * *", zone = "Asia/Ho_Chi_Minh")
public void sendMorningReport() {
    System.out.println("Gửi báo cáo lúc 8h sáng VN");
}

3. @EnableAsync

Trong Spring Boot, @EnableAsync đóng vai trò là "Công tắc tổng" để để kích hoạt hệ thống xử lý bất đồng bộ.

Kích hoạt và Sử dụng cơ bản

Bạn chỉ cần đặt @EnableAsync lên class Main hoặc một class @Configuration bất kỳ hoặc gắn trực tiếp lên class Main (class chứa hàm main của Spring Boot).

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync // Bật tính năng xử lý bất đồng bộ
public class MySpringBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(MySpringBootApplication.class, args);
    }
}

4. @Async

Trong Spring Boot, annotation @Async được sử dụng để thực thi một method một cách bất đồng bộ (asynchronous). Thay vì block (chặn) thread hiện tại cho đến khi method chạy xong, Spring sẽ đưa task đó cho một thread khác xử lý ngầm, giúp thread chính có thể tiếp tục làm việc khác ngay lập tức.

Ứng dụng phổ biến nhất của @Async là các tác vụ chạy ngầm như: Gửi email, đẩy push notification, gọi API bên thứ ba chậm chạp, hoặc xử lý file/batch job.

Sử dụng trên Method

Đánh dấu method bạn muốn chạy bất đồng bộ bằng @Async. Method này phải trả về void (nếu không cần kết quả) hoặc CompletableFuture (nếu cần lấy kết quả).

@Service
public class EmailService {

    @Async
    public void sendWelcomeEmail(String email) {
        System.out.println("Bắt đầu gửi email trên luồng: " + Thread.currentThread().getName());
        // Giả lập gửi email mất 3 giây
        // Code ở hàm gọi (ví dụ Controller) sẽ không bị khựng lại chờ 3 giây này.
    }
}

Bản chất hoạt động ngầm (Cơ chế Proxy)

Spring sử dụng kỹ thuật AOP (Aspect-Oriented Programming) để xử lý @Async. Khi ứng dụng khởi động, Spring không đưa class EmailService thật cho Controller dùng, mà nó tạo ra một Proxy (Lớp vỏ bọc).

Khi Controller gọi hàm sendWelcomeEmail():

  1. Nó gọi vào Proxy.
  2. Proxy thấy hàm này có @Async, nó liền bốc công việc này ném vào một Thread Pool (Hồ bơi luồng).
  3. Proxy ngay lập tức trả lại quyền điều khiển (return) cho Controller để Controller phản hồi cho người dùng.
  4. Ở dưới background, một Thread trong Pool sẽ lẳng lặng thực thi việc gửi email.

CẠM BẪY CHẾT NGƯỜI: Lỗi tràn RAM (OutOfMemoryError)

Nếu bạn chỉ bật @EnableAsync và dùng, hệ thống của bạn đang mang một quả bom nổ chậm.

Mặc định, Spring Boot sử dụng một trình quản lý luồng tên là SimpleAsyncTaskExecutor. Thằng này có một đặc điểm cực kỳ tồi tệ: Nó KHÔNG tái sử dụng luồng (No Thread Reuse). Cứ mỗi lần bạn gọi hàm @Async, nó lại new Thread() một luồng mới cứng. Nếu có 10.000 user gọi API, nó tạo ra 10.000 luồng. Hệ thống sẽ cạn kiệt RAM và sập ngay lập tức.

Cách khắc phục (Bắt buộc làm thực tế): Bạn phải cấu hình một ThreadPoolTaskExecutor chuẩn mực để giới hạn số lượng luồng.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;

@Configuration
public class AsyncConfig {

    @Bean(name = "customTaskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        
        // 1. Số luồng cốt lõi luôn duy trì (dù không có việc)
        executor.setCorePoolSize(5);
        
        // 2. Số lượng luồng tối đa được phép tạo ra khi quá tải
        executor.setMaxPoolSize(20);
        
        // 3. Hàng đợi: Nếu 20 luồng đều bận, các task tiếp theo sẽ đứng xếp hàng (tối đa 500 task)
        executor.setQueueCapacity(500);
        
        // 4. Tiền tố tên của luồng (để dễ debug trong log)
        executor.setThreadNamePrefix("MyAsyncThread-");
        
        executor.initialize();
        return executor;
    }
}

Lúc này, bạn chỉ định rõ tên Thread Pool khi gọi @Async:

@Async("customTaskExecutor")
public void sendWelcomeEmail(String email) { ... }

Lấy dữ liệu trả về từ hàm @Async

Nếu hàm chạy ngầm của bạn cần tính toán và trả về kết quả (không phải kiểu void), bạn KHÔNG THỂ trả về kiểu dữ liệu thông thường (như String hay User). Bạn bắt buộc phải bọc nó trong CompletableFuture.

@Async("customTaskExecutor")
public CompletableFuture<String> processPayment() {
    // Xử lý mất 5 giây...
    return CompletableFuture.completedFuture("Thanh toán thành công");
}

Ở nơi gọi hàm, bạn có thể chờ kết quả nếu muốn:

CompletableFuture<String> futureResult = paymentService.processPayment();

// Làm các việc khác trong lúc chờ thanh toán...

// Lấy kết quả (Hàm get() sẽ block luồng chính cho đến khi luồng Async làm xong)
String result = futureResult.get();

Cạm bẫy tương tự @Transactional: Self-Invocation

@Async hoạt động dựa trên Proxy, nên nó dính chung một nhược điểm chí mạng với @Transactional: Tính năng sẽ bị vô hiệu hóa nếu bạn gọi hàm nội bộ.

@Service
public class UserService {

    public void registerUser() {
        System.out.println("Đăng ký thành công!");
        
        // GỌI HÀM NỘI BỘ TRONG CÙNG CLASS
        // Lỗi: Hàm này vẫn chạy ĐỒNG BỘ trên cùng một luồng, giao diện người dùng bị treo!
        sendEmailAsync(); 
    }

    @Async
    public void sendEmailAsync() {
        // Gửi email...
    }
}

Cách giải quyết: Giống hệt @Transactional. Bạn phải tách hàm sendEmailAsync() ra một class @Service khác (ví dụ EmailService) và @Autowired nó vào để sử dụng.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí