Database relations và JPA mapping
Cardinality luôn được đọc từ một phía cụ thể. Cùng quan hệ Department–Employee là one-to-many khi nhìn từ Department và many-to-one khi nhìn từ Employee.
1. Quy tắc xác định cardinality
- Chọn một record ở bảng A.
- Hỏi record đó liên kết tối đa với bao nhiêu record ở B: một hay nhiều?
- Đổi chiều và hỏi lại từ B sang A.
- Với quan hệ 1–N thông thường, đặt foreign key ở phía “nhiều”.
Employee có quan hệ many-to-one và bảng employee giữ department_id. Đọc ngược lại, “Một Department có nhiều Employee” là one-to-many.2. Bảng tổng hợp các kiểu quan hệ
| Quan hệ | Đọc thành câu | Thiết kế relational | JPA mapping thường gặp |
|---|---|---|---|
| One-to-one (1–1) | Một User có tối đa một Profile. | Foreign key ở một bảng kèm UNIQUE, hoặc shared primary key. | @OneToOne; phía có @JoinColumn là owning side. |
| One-to-many (1–N) | Một Department có nhiều Employee. | department_id nằm trong bảng employee. | @OneToMany(mappedBy="department") thường là inverse side. |
| Many-to-one (N–1) | Nhiều Employee thuộc một Department. | Cũng chính foreign key employee.department_id. | @ManyToOne và @JoinColumn ở Employee, thường là owning side. |
| Many-to-many (N–N) | Một Student học nhiều Course; một Course có nhiều Student. | Bảng nối enrollment(student_id, course_id). | Có thể dùng @ManyToMany, nhưng nên map bảng nối thành entity khi quan hệ có dữ liệu nghiệp vụ. |
Điểm cốt lõi: one-to-many và many-to-one không phải hai quan hệ database khác nhau; chúng là hai hướng nhìn của cùng một foreign key.
3. SQL mẫu: Department và Employee
CREATE TABLE department (
id BIGINT PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE employee (
id BIGINT PRIMARY KEY,
name TEXT NOT NULL,
department_id BIGINT NOT NULL,
FOREIGN KEY (department_id) REFERENCES department(id)
);
Mỗi row employee chỉ chứa một department_id, nhưng nhiều employee có thể lặp cùng giá trị đó. Vì vậy Employee → Department là N–1, còn Department → Employee là 1–N. Index foreign key không được PostgreSQL tự động tạo; hãy thêm index khi access pattern, join hoặc parent delete/update cần nó.
4. Mapping JPA hai chiều và owning side
@Entity
class Employee {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "department_id", nullable = false)
private Department department;
}
@Entity
class Department {
@Id
private Long id;
@OneToMany(mappedBy = "department")
private List<Employee> employees = new ArrayList<>();
}
- Owning side trong JPA là phía điều khiển foreign key: ở đây là
Employee.departmentvì nó có@JoinColumn. mappedBy = "department"trỏ tới tên field Java trongEmployee, không phải tên column trong database.- Owning side của object mapping không thay đổi nơi database thực sự lưu foreign key hoặc constraint.
- Chỉ thêm employee vào collection mà không set
employee.departmentlàm object graph lệch và có thể khiến foreign key không được cập nhật như mong muốn.
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this);
}
5. One-to-one cần UNIQUE
CREATE TABLE user_profile (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL UNIQUE,
FOREIGN KEY (user_id) REFERENCES app_user(id)
);
Foreign key một mình chỉ đảm bảo mỗi profile trỏ tới một user. Nếu không có UNIQUE(user_id), nhiều profile vẫn có thể trỏ cùng user và quan hệ thực tế trở thành many-to-one. Shared primary key là lựa chọn khác khi child phụ thuộc hoàn toàn vào identity và lifecycle của parent.
6. Many-to-many và entity bảng nối
CREATE TABLE enrollment (
student_id BIGINT NOT NULL REFERENCES student(id),
course_id BIGINT NOT NULL REFERENCES course(id),
enrolled_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL,
PRIMARY KEY (student_id, course_id)
);
Khi bảng nối có status, thời gian, role, quantity hoặc audit fields, nó đã là một domain concept. Map thành Enrollment với hai quan hệ many-to-one giúp model lifecycle, constraints và query rõ hơn @ManyToMany trực tiếp. Nếu một sinh viên có thể đăng ký lại cùng khóa học nhiều lần, composite key trên hai foreign key không còn đủ; cần identity hoặc key bao gồm attempt/version phù hợp invariant.
7. Optionality khác cardinality
Cardinality cần đọc cả min và max: 0..1, 1, 0..* hoặc 1..*. Employee có thể chưa thuộc department là 0..1 ở chiều Employee → Department, nên foreign key nullable. Nếu bắt buộc, dùng cả NOT NULL và optional = false. Annotation JPA giúp ORM hiểu model nhưng không thay database constraint cho các writer khác.
8. Các bẫy JPA thường gặp
| Bẫy | Cách suy nghĩ đúng |
|---|---|
| Fetch mặc định | Theo Jakarta Persistence, to-one mặc định EAGER, còn to-many mặc định LAZY. LAZY chỉ là hint cho to-one theo spec, nên phải đo SQL thực tế của provider và thiết kế query theo use case. |
| N+1 query | Load một danh sách rồi chạm association từng row có thể tạo N query phụ. Dùng fetch join, entity graph, projection hoặc batch phù hợp; không đổi mọi association thành EAGER. |
| Cascade | Cascade JPA truyền operation trong persistence context; nó không đồng nghĩa với ON DELETE CASCADE của database. |
orphanRemoval | Chỉ dùng khi child thực sự thuộc lifecycle độc quyền của parent; remove khỏi collection có thể dẫn tới DELETE. |
| JSON recursion | Serialize association hai chiều trực tiếp dễ lặp vô hạn hoặc vô tình load cả graph. Dùng DTO tại API boundary. |
equals/hashCode | Không đưa mutable collection hoặc lazy association vào equality. Với generated ID, phải cân nhắc trạng thái transient và tính ổn định khi entity nằm trong hash collection. |
9. Sơ đồ quyết định nhanh
A có nhiều B, mỗi B thuộc một A
=> foreign key a_id nằm ở B
=> B: @ManyToOne + @JoinColumn
=> A: @OneToMany(mappedBy = "a") nếu cần đi ngược
A có nhiều B và B cũng có nhiều A
=> cần bảng nối A_B
=> có dữ liệu nghiệp vụ trên quan hệ? Map A_B thành entity
A chỉ có tối đa một B
=> foreign key + UNIQUE, hoặc shared primary key
10. Checklist tự kiểm tra
- Đọc được cardinality theo cả hai chiều bằng một câu rõ nghĩa.
- Xác định đúng bảng chứa foreign key và giải thích vì sao.
- Biết 1–N và N–1 là hai hướng của cùng một quan hệ.
- Phân biệt JPA owning side, inverse side và database ownership.
- Biết khi nào bảng nối nên trở thành entity.
- Phân biệt cardinality, optionality, cascade và fetch strategy.