Java Persistence API cơ bản
1. JPA là gì?
Java Persistence API (JPA) là đặc tả (specification) chuẩn của Java để ánh xạ đối tượng Java với các bảng trong cơ sở dữ liệu quan hệ (ORM) và để lưu trữ dữ liệu một cách bền vững (persistence). JPA ban đầu thuộc Java EE (Oracle) và hiện được phát triển bởi Eclipse Foundation với tên Jakarta Persistence.
JPA có Java Persistence Query Language (JPQL), một ngôn ngữ truy vấn hướng đối tượng, độc lập với từng loại database. Nhờ vậy, ứng dụng có thể đổi database (MySQL, PostgreSQL, Oracle...) mà không cần sửa code.
Lưu ý: JPA chỉ là đặc tả. Để chạy được cần một implementation (JPA provider).
Các JPA provider phổ biến
- Hibernate (phổ biến nhất)
- EclipseLink (reference implementation)
- OpenJPA
- DataNucleus (hỗ trợ cả JPA và JDO)
Ý tưởng chính: nếu chỉ dùng API chuẩn của JPA, bạn có thể thay đổi provider mà không sửa code. Tuy nhiên Hibernate còn cung cấp native API (HQL, Criteria của Hibernate, Session...) với nhiều tính năng vượt ngoài JPA, đổi lại bạn bị phụ thuộc vào Hibernate.
2. JPA và JDO
JDO (Java Data Objects) và JPA đều là đặc tả, không phải implementation, và đều là chuẩn persistence của Java.
| JPA | JDO | |
|---|---|---|
| Datastore hỗ trợ | Chỉ RDBMS | Mọi loại datastore (RDBMS, NoSQL, object DB...) |
| Mức độ phổ biến | Rất cao | Thấp hơn |
| Implementation | Hibernate, EclipseLink, OpenJPA... | DataNucleus... |
Hibernate không implement JDO. Nếu cần JDO, hãy tham khảo DataNucleus.
3. Lịch sử phát triển
Quá trình tiến hoá của các công nghệ persistence trong Java:
- JDBC: thao tác SQL trực tiếp
- Data Mapper (MyBatis...)
- EJB (Entity Bean)
- JDO
- JPA
- JPA 1.0 (cùng EJB 3.0, 2006)
- JPA 2.0 (2009)
- JPA 2.1 (2013)
- JPA 2.2 (2017)
- Jakarta Persistence 3.0 (2020, đổi package sang
jakarta.persistence) - Jakarta Persistence 3.1 (2022) và 3.2 (2024)
4. Cấu trúc của JPA
JPA gồm 4 phần:
- The Java Persistence API:
EntityManager,EntityManagerFactory, annotation... - The Criteria API: xây dựng truy vấn bằng code, an toàn kiểu (type-safe)
- Query language (JPQL)
- Object-relational mapping metadata: annotation hoặc XML (
@Entity,@Table,@Column...)
5. Ví dụ nhanh
Entity
@Entity
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
// constructor mặc định (bắt buộc), getter/setter
}persistence.xml (đặt trong META-INF/)
<persistence-unit name="demoPU" transaction-type="RESOURCE_LOCAL">
<class>com.example.Customer</class>
<properties>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/demo"/>
<property name="jakarta.persistence.jdbc.user" value="root"/>
<property name="jakarta.persistence.jdbc.password" value="secret"/>
<!-- Hibernate dialect, thường tự nhận diện ở bản mới -->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
</properties>
</persistence-unit>Danh sách dialect: Hibernate Configuration (TutorialsPoint)
Dùng EntityManager (CRUD)
EntityManagerFactory emf = Persistence.createEntityManagerFactory("demoPU");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Customer c = new Customer();
c.setName("An");
em.persist(c); // Create
em.getTransaction().commit();
Customer found = em.find(Customer.class, c.getId()); // Read
em.getTransaction().begin();
found.setName("Binh"); // Update (tự động flush khi commit)
em.getTransaction().commit();
em.getTransaction().begin();
em.remove(found); // Delete
em.getTransaction().commit();
em.close();
emf.close();find() vs getReference()
find()truy vấn DB ngay và trả về entity (hoặcnullnếu không có).getReference()trả về một proxy lazy, chỉ truy vấn khi truy cập dữ liệu. Nếu không tồn tại sẽ némEntityNotFoundException. Hữu ích khi chỉ cần tham chiếu khoá ngoại mà không cần tải toàn bộ entity.
6. Vòng đời của Entity
| Trạng thái | Mô tả |
|---|---|
| New / Transient | Vừa tạo bằng new, chưa gắn với persistence context |
| Managed | Được quản lý bởi EntityManager, thay đổi sẽ tự được đồng bộ xuống DB |
| Detached | Đã tách khỏi persistence context (sau close(), detach()) |
| Removed | Đã đánh dấu xoá, sẽ bị xoá khi commit |
7. Ánh xạ quan hệ (Relationship Mapping)
4 loại quan hệ
@OneToOne@ManyToOne@OneToMany@ManyToMany
Mỗi quan hệ có thể là một chiều (unidirectional) hoặc hai chiều (bidirectional), và có thể tải ngay (EAGER) hoặc theo yêu cầu (LAZY).
Mặc định: @OneToOne và @ManyToOne là EAGER, @OneToMany và @ManyToMany là LAZY.
Owning side và Inverse side
Mỗi quan hệ có hai phía:
- Owning side: chịu trách nhiệm cập nhật quan hệ xuống database, thường là phía giữ khoá ngoại.
- Inverse side: phía còn lại, ánh xạ ngược về owning side bằng thuộc tính
mappedBy.
Nếu bạn quen Hibernate,
mappedBycủa JPA tương đương thuộc tínhinversecủa Hibernate: phía inverse sẽ bị bỏ qua khi cập nhật quan hệ.
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
// Inverse side
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
}
@Entity
public class OrderItem {
@Id @GeneratedValue
private Long id;
// Owning side (giữ khoá ngoại order_id)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
}Cascade
CascadeType gồm PERSIST, MERGE, REMOVE, REFRESH, DETACH, ALL. Dùng để lan truyền thao tác từ entity cha sang entity con.
Embedded object và ElementCollection
@Embeddable/@Embedded/@EmbeddedId: nhúng một đối tượng giá trị (ví dụAddress) hoặc khoá chính tổ hợp. Xem JPA EmbeddedId example@ElementCollection: lưu tập hợp các giá trị cơ bản hoặc embeddable. Xem ElementCollection (Wikibooks)
8. Truy vấn trong JPA
Có 3 cách truy vấn:
- Native query: SQL thuần
- JPQL (Java Persistence Query Language, SQL hướng đối tượng):
Query,TypedQuery,@NamedQuery/@NamedQueries - Criteria API: xây dựng truy vấn bằng code, động và type-safe
// JPQL với tham số (khuyến nghị, tránh SQL injection)
TypedQuery<Customer> q = em.createQuery(
"SELECT c FROM Customer c WHERE c.name = :name", Customer.class);
q.setParameter("name", "An");
List<Customer> list = q.getResultList();
// Named Query
@Entity
@NamedQuery(name = "Customer.findByName",
query = "SELECT c FROM Customer c WHERE c.name = :name")
public class Customer { ... }
em.createNamedQuery("Customer.findByName", Customer.class)
.setParameter("name", "An")
.getResultList();
// Criteria API
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Customer> cq = cb.createQuery(Customer.class);
Root<Customer> root = cq.from(Customer.class);
cq.select(root).where(cb.equal(root.get("name"), "An"));
List<Customer> result = em.createQuery(cq).getResultList();- Named Query là truy vấn tĩnh, được parse và kiểm tra cú pháp khi khởi động ứng dụng, dễ tái sử dụng.
- Truy vấn động (dynamic) (JPQL hoặc native) được tạo lúc runtime.
- Criteria API linh hoạt hơn và an toàn kiểu hơn khi cần xây dựng điều kiện động.
- Luôn dùng parameter binding (
:name,?1) thay vì nối chuỗi để chống SQL injection.
9. Kế thừa (Inheritance)
JPA có 3 chiến lược ánh xạ kế thừa:
| Chiến lược | Mô tả | Ưu điểm | Nhược điểm |
|---|---|---|---|
SINGLE_TABLE (mặc định) | Cả hệ thống phân cấp lớp vào một bảng (kèm cột phân biệt DTYPE) | Không cần JOIN, truy vấn nhanh | Nhiều cột NULL, không đặt được NOT NULL cho cột lớp con |
JOINED | Mỗi lớp một bảng, bảng con chỉ chứa cột riêng và join với bảng cha qua khoá chính | Dữ liệu chuẩn hoá, không trùng lặp | Phải JOIN nên truy vấn chậm hơn |
TABLE_PER_CLASS | Mỗi lớp cụ thể một bảng chứa đầy đủ cột (kể cả cột kế thừa) | Truy vấn lớp cụ thể không cần JOIN | Dữ liệu chung bị lặp ở nhiều bảng, truy vấn đa hình dùng UNION; là tính năng tuỳ chọn trong spec |
Ví dụ với JOINED, đặt annotation ở entity cha:
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Vehicle { ... }
@Entity
public class Car extends Vehicle { ... }Ngoài ra có @MappedSuperclass: lớp cha chỉ để dùng chung các trường (ví dụ id, createdAt), không phải entity và không có bảng riêng.
Ghi chú (legacy) về Google App Engine: với Datastore cũ dùng DataNucleus,
JOINEDvàSINGLE_TABLEkhông được hỗ trợ;TABLE_PER_CLASSvàMAPPED_SUPERCLASShoạt động như mô tả trong tài liệu DataNucleus.
10. Một số lưu ý thực tế (Best practices)
- N+1 problem: duyệt quan hệ LAZY trong vòng lặp có thể sinh rất nhiều câu SQL. Khắc phục bằng
JOIN FETCH,@EntityGraphhoặc batch fetching. - Ưu tiên
FetchType.LAZYcho mọi quan hệ, chỉ dùng EAGER khi thật sự cần. - Luôn có constructor mặc định (không tham số) cho entity, và override
equals()/hashCode()cẩn thận. - Với quan hệ hai chiều, hãy đồng bộ cả hai phía bằng phương thức tiện ích (ví dụ
order.addItem(item)). - Đừng dùng
hbm2ddl.auto=updatetrên production; hãy dùng công cụ migration như Flyway hoặc Liquibase. - Trong Spring, dùng Spring Data JPA (
JpaRepository) để giảm code lặp, nhưng vẫn cần hiểu JPA bên dưới.
0 comments:
Post a Comment