JPA là gì? Giới thiệu về Java Persistence

Người dịch: Trần Ngọc Quân - Học viên lớp Java08
Reviewer: Trịnh Minh Cường
Email liên hệ: quan.tranngoc317@gmail.com
Bài viết gốc: https://www.infoworld.com/article/3379043/what-is-jpa-introduction-to-the-java-persistence-api.html

Theo chỉ dẫn kỹ thuật, Persistence là 1 đặc tính của Jakarta Persistence API (tên gọi cũ Java Persistence API), hiểu khái quát là bất kỳ cơ chế nào giúp các đối tượng Java (objects) vẫn còn tồn tại sau khi chương trình tạo ra chúng kết thúc. Không phải tất cả các đối tượng Java (objects) đều cần được lưu trữ (persisted), thay vào đó chỉ cần lưu trữ các đối tượng key phục vụ cho business của chương trình. JPA giúp ta định nghĩa những đối tượng nào cần được lưu trữ lại (persisted) và lưu trữ như nào trong ứng dụng Java của bạn.

Về bản chất, JPA không phải là một công cụ hay framework, thay vào đó, nó định nghĩa một loạt các đặc tả để cho các công cụ triển khai JPA thực thi theo. Mô hình JPA object-relational mapping (ORM – cơ chế thực hiện ánh xạ database sang đối tượng của ngôn ngữ lập trình) đã được mở rộng và phát triển thêm rất nhiều so với lúc đầu chỉ dựa trên Hibernate
. Nhờ đó, ngoài khả năng chỉ sử dụng cho các cơ sở dữ liệu quan hệ (relational database) như trước, một số tính năng của JPA đã được phát triển để có thể áp dụng cho các cơ sở dữ liệu phi quan hệ (NoSQL). Một framework phổ biến hỗ trợ JPA và NoSQL là EclipseLink và bắt đầu được hỗ trợ từ JPA 3.

Ý tưởng chính của JPA mà khác biệt với JDBC đó là giúp ta không cần phải lo lắng về các mối quan hệ. Trong JPA, ta định nghĩa các quy tắc persistence thông qua code và đối tượng (objects), trong khi JDBC đòi hỏi người dùng phải chuyển đổi thủ công giữa code và dữ liệu trong database và ngược lại.

Mô hình Java Persistence API được ra mắt lần đầu như một tính năng con của Enterprise JavaBeans 3.0 trong Java EE 5. Kể từ đó, nó được phát triển thành một nhánh riêng bắt đầu với JPA 2.0 trong Java EE 6(JSR 317). JPA được coi như một dự án độc lập kể từ Jakarta EE năm 2019. Phiên bản hiện tại là JPA 3.1

Các công nghệ phổ biến dùng để triển khai JPA như Hibernate và EclipseLink hiện nay đều hỗ trợ JPA 3. Việc chuyển đổi từ JPA 2 sang JPA 3 kéo theo một số thay đổi về package (namespace), tuy nhiên những thay đổi này không ảnh hưởng đến kết quả nhận được.

JPA và Hibernate

Do lịch sử phát triển gắn liền với nhau, Hibernate và JPA thường bị coi là một. Tuy nhiên, theo chỉ dẫn kĩ thuật Java Servlet, JPA là một tập hơp rất nhiều công cụ và framework mà trong đó Hibernate chỉ là một trong các công cụ của JPA.
Được phát triển bởi Gavin King và ra mắt lần đầu vào năm 2002, Hibernate là một thự viện ORM của Java. Gavin King phát triển Hibernate như một giải pháp để tạo bean cho persistence. Tại thời điểm đó, Hibernate vô cùng phổ biến và cần thiết, do vậy phần lớn các khái niệm của nó đã được áp dụng và phát triển trong đặc tính kĩ thuật đầu tiên của JPA.
Ngày nay, Hibernate ORM là một trong số những công cụ triển khai JPA hoàn thiện nhất, và vẫn là một lựa chọn phổ biến cho ORM trong java. Phiên bản mới nhất hiện tại là Hibernate ORM 6, triển khai JPA 2.2. Hibernate cũng bao gồm một số công cụ bổ khác như Hibernate Search, Hibernate ValidatorHibernate OGM hỗ trợ NoSQL.

JPA and EJB
Như đã nhắc đến trước đó, JPA được giới thiệu là một thành phần con của Enterprise JavaBeans (EJB) 3.0, tuy nhiên được phát triển với các tiêu chí kĩ thuật riêng. EJB là một đặc tính với cách tiếp cận khác so với JPA và được triển khai trong EJB container. Mỗi EJB container sẽ bao gồm 1 lớp persistence được định nghĩa theo JPA.

JAVA ORM là gì?

Mặc dù cách hoạt động khác nhau, tất cả các công cụ triển khai của JPA đều cung cấp một số kiểu layer ORM. Để có thể hiểu về JPA và các công cụ JPA-compatible, ta cần phải nắm vững ORM.
Object-relational mapping (ORM) – quá trình chuyển đổi,ánh xạ dữ liệu từ ngôn ngữ hướng đối tượng sang dữ liệu database và ngược lại – là một công việc mà các lập trình viên nên tránh phải triển khai thủ công. Một framework như Hibernate ORM hoặc EclipseLink sẽ thực thi quá trình đó bên trong 1 thư viện, framework , hoặc 1 layer ORM. Như một phần trong cấu trúc của chương trình, lớp ORM có vai trò quản lý sự chuyển đổi qua lại giữa các đối tượng (objects) với các bảng (tables) và cột (colums) trong cơ sở dữ liệu quan hệ (relational database). Trong Java, lớp ORM biến đổi các lớp (class) và đối tượng (object) Java để chúng có thể được lưu trữ và quản lý trong cơ sở dữ liệu.
Mặc định, tên của đối tượng được lưu (persisted) sẽ trở thành tên của bảng (table) và các thuộc tính trở thành cột (column). Khi bảng được tạo thành công, mỗi dòng trong bảng sẽ tương ứng với một đối tượng (object) trong ứng dụng. Nguyên tắc ánh xạ chuyển đổi có thể được cấu hình tùy biến, tuy nhiên bằng cách sử dụng chế độ mặc định, bạn có thể tránh được việc phải bảo trì cấu hình metadata.

JPA với NoSQL
Trước đây, cơ sở dữ liệu phi quan hệ vẫn là một khái niệm xa lạ. Mô hình NoSQL đã thay đổi hoàn toàn, ngày nay rất nhiều cơ sở dữ liệu NoSQL có sẵn cho các lập trình viên Java có thể ứng dụng. Một số công cụ triển khai JPA đã được phát triển để hỗ trợ NoSQL, trong đó có Hibernate OGM và EclipseLink

1
Ảnh 1. JPA và Java ORM layer

Thiết lập lớp Java ORM
Khi khởi tạo một dự án mới sử dụng JPA, ta cần thiết lập nơi lưu trữ dữ liệu và JPA provider. Ta cần cài đặt một công cụ (datastore connector) để kết nối đến cơ sở dữ liệu (SQL hoặc NoSQL). Bên cạnh đó, ta cũng cần thiết lập JPA provider ví dụ như Hibernate hoặc EclipseLink framework. Mặc dù, JPA có thể được thiết lập 1 cách thủ công, nhiều lập trình viên vẫn lựa chọn sử dụng các công cụ hỗ trợ bên ngoài của Spring. Chúng ta sẽ cùng tìm hiểu cả cách cài đặt thủ công và cài đặt theo Spring JPA.

Java Data Objects
Java Data Objects (DJO) là một framework tiêu chuẩn hóa persistence khác với JPA ở việc hỗ trợ logic của persistence trong đối tượng (object) và khả năng hỗ trợ lâu dài khi làm việc với cơ sở dữ liệu phi quan hệ. Một số JDO providers cũng có khả năng hỗ trợ JPA.

Data persistence trong Java

Từ quan điểm của lập trình, lớp ORM là một lớp chuyển đổi: nó chuyển đổi ngôn ngữ của đối tượng lập trình sang ngôn ngữ của SQL và bảng dữ liệu. Lớp ORM cho phép các lập trình viên lập trình ngôn ngữ hướng đối tượng xây dựng phần mềm lưu trữ dữ liệu vẫn tuân theo quy tắc hướng đối tượng.
Khi sử dụng JPA, ta tạo ra một một công cụ chuyển đổi dữ liệu từ cơ sở dữ liệu sang dữ liệu kiểu đối tượng trong chương trình. Thay vì phải thiết lập cách lưu trữ và trích xuất dữ liệu, bạn có thể định nghĩa hướng chuyển đổi giữa đối tượng (object) và cơ sở dữ liệu (database), sau đó sử dụng JPA để lưu trữ chúng. Nếu ta sử dụng cơ sở dữ liệu quan hệ, hầu hết những kết nối giữa ứng dụng và cơ sở dữ liệu sẽ được xử lý bởi JDBC.
JPA có tính năng cung cấp các metadata annotation (đánh dấu), giúp ta có thể định nghĩa cách chuyển đổi giữa đối tượng và cơ sở dữ liệu (database). Mỗi công cụ triển khai JPA đều cung cấp những triển khai riêng cho JPA annotation. Tính năng của JPA còn cung cấp PersistanceManager hoặc EntityManager – đóng vai trò mấu chốt trong việc kết nối với hệ thống JPA ( nơi code xử lý các đối tượng được chuyển đổi).
Để nắm rõ hơn, cùng tham khảo ví dụ với lớp Musician

public class **Musician** {
  private Long id;
  private String name;
  private Instrument mainInstrument;
  private ArrayList performances = new ArrayList();
  public **Musician**( Long id, String name){ /* constructor setters... */ }
  public void setName(String name){
      this.name = name;
  }
  public String getName(){
      return this.name;
  }
  public void setMainInstrument(Instrument instr){
      this.instrument = instr;
  }
  public Instrument getMainInstrument(){
      return this.instrument;
  }
  // ...Other getters and setters...
}

Class Musician được sử dụng để nhận dữ liệu. Nó có thể bao gồm dữ liệu nguyên thủy như trường name hoặc các quan hệ với các class khác như mainInstrument và perfomrances.
Mục đích của class Musician là để chứa dữ liệu. Kiểu class này đôi khi có vai trò như 1 DTO – data transfer object. DTOs là một đặc tính cơ bản của phát triển phần mềm. Chúng có thể chứa nhiều kiểu dữ liệu nhưng không chứ bất cứ logic lập trình nào. Lưu trữ đối tượng dữ liệu là một công việc cơ bản và thường xuyên trong phát triển phần mềm.

Data persistence với JDBC

Một cách khác để đồng bộ 1 đối tượng của class Musiccian vào cơ sở dữ liệu quan hệ là sử dụng thư viện JDBC. JDBC là 1 lớp abstraction cho phép ứng dụng xuất các câu lệnh SQL mà không cần sử dụng đến công cụ triển khai database.
Ví dụ bên dưới thể hiện cách để lưu class Musiccian sử dụng JDBC

**Musician** georgeHarrison = new **Musician**(0, "George Harrison");
      String myDriver = "org.gjt.mm.mysql.Driver";
      String myUrl = "jdbc:mysql://localhost/test";
      Class.forName(myDriver);
      Connection conn = DriverManager.getConnection(myUrl, "root", "");
      String query = " insert into users (id, name) values (?, ?)";
      PreparedStatement preparedStmt = conn.prepareStatement(query);
      preparedStmt.setInt     (1, 0);
      preparedStmt.setString (2, "George Harrison");
      preparedStmt.setString (2, "Rubble");
      preparedStmt.execute();
      conn.close();
// Error handling removed for brevity

Đối tượng georgeHarrison có thể đến từ bất kì đâu (từ phía front-end, công cụ bên ngoài, etc…) và nó chứa trường ID và name. Các trường của đối tượng được sử dụng làm các giá trị của câu lệnh SQL. (class PreparedStatement là 1 phần của JDBC, cung cấp cách áp dụng an toàn các giá trị vào câu lệnh SQL)
Trong khi JDBC cung cấp khả năng kiểm soát thông qua các thiết lập thủ công, nó khá là phức tạp khi so sánh với JPA. Để thay đổi cơ sở dữ liệu, ta cần tạo câu lệnh SQL để chuyển đổi từ đối tượng Java sang các bảng dữ liệu trong cơ sở dữ liệu quan hệ (relational database). Sau đó bạn sẽ phải thay đổi câu lệnh SQL mỗi khi thuộc tính của đối tượng (object) thay đổi. Do vậy, khi sử dựng JDBC, việc bảo trì SQL tạo thêm nhiều khó khăn cho lập trình viên.

Data persistence với JPA

Tiếp theo là cách ta đồng bộ class Musician sử dụng JPA

**Musician** georgeHarrison = new **Musician**(0, "George Harrison");
musicianManager.save(georgeHarrison);

Cách này đã thay thế một loạt câu lệnh SQL chỉ với 1 dòng duy nhất, entityManager.save(), để thông báo cho JPA lưu dữ liệu của object. Sau đó, JPA sẽ xử lý chuyển đổi SQL và lập trình viên có thể tiếp tục với với mô hình hướng đối tượng.

Metadata annotations trong JPA

Cách trình bày ngắn gọn trong ví dụ trước đó là kết quả của việc thiết lập bằng JPA annotation. Lập trình viên sử dụng annotation để thông báo cho JPA đối tượng (object) nào cần được lưu trữ và được lưu trữ như nào.

@Entity
public class **Musician** {
  // ..class body
}

Đối tượng lưu trữ được gọi là entity. Thêm annotation @Entity cho 1 class như Musiccian để cho JPA biết rằng những đối tượng của class này sẽ được đồng bộ xuống database.

Cấu hình JPA

Cũng giống như hầu hết các framework ngày nay, JPA triển khai bởi các quy ước( hay gọi là quy ước thiết lập – convention over configuration), JPA cung cấp các cấu hình mặc định dựa theo các thực nghiệm tối ưu. Ví dụ, 1 class Musician có thể được chuyển đổi mặc định đến 1 bảng trong cơ sở dữ liệu có tên Musician.
Các quy ước thiết lập giúp tiết kiệm thời gian trong rất nhiều trường hợp. Bên cạnh đó, ta cũng có thể tùy biến các thiết lập JPA. Ví dụ, ta có thể sử dụng annotation @Table để gán cụ thể tên bảng trong cơ sở dữ liệu sẽ được dung để lưu class Musician

@Entity
@Table(name="musician")
public class *Musician* {
  // ..class body
}

Primary key - khóa chính

Trong JPA, primary key là trường dung để phân biệt các đối tượng trong database. Primary key rất hữu dụng cho việc truy vấn và liên kết các đối tượng đến các entity khác. Mỗi khi lưu 1 đối tượng vào bảng database, ta sẽ cần đánh dấu trường nào đóng vai trò primary key

@Entity
public class *Musician* {
   @Id
   private Long id;

Trong trường hợp này, ta sử dụng annotation @Id để đánh dấu trường idprimary key cho entity Musician. Mặc định, thiết lập này sẽ cho phép primary key được tạo ra bởi database – ví dụ, khi trường id được cài đặt auto-increment
trong bảng database
JPA hỗ trợ các giải pháp khác để tạo primary key cho 1 đối tượng (object). Ngoài ra, còn có các annotation để thay đổi tên của các trường của entity. Nói chung, JPA có thể linh hoạt thay đổi cách chuyển đổi theo nhu cầu của lập trình viên.

Thao tác CRUD

Sau khi ánh xạ 1 class vào 1 bảng trong database và khởi tạo primary key, ta có thể sử dụng tất cả các công cụ để tạo, truy xuất, xóa và cập nhật dữ liệu của class đó trong database. Phương thức entityManager.save() sẽ tạo hoặc cập nhật 1 class cụ thể, phụ thuộc vào việc kiểm tra primary key là null hoặc cập nhật vào một entity đã tồn tại. Phương thức entityManager.remove() sẽ xóa 1 class cụ thể.

Các kiểu quan hệ Entity

Lưu trữ 1 đối tượng với các trường dữ liệu nguyên thủy chỉ mới là 1 nửa của bài toán. JPA còn cho phép chúng ta quản lý các entity và các quan hệ giữa chúng. Dưới đây là 4 kiểu quan hệ giữa các entity áp dụng cho cả bảng database và đối tượng object :

  • One-to-many: một - nhiều
  • Many-to-one:nhiều – một
  • Many-to-many:nhiều-nhiều
  • One-to-one:một-một

Mỗi kiểu quan hệ sẽ diễn tả cách 1 entity có quan hệ như nào với các entity khác. Ví dụ, entiy Musician có mối quan hệ one-to-many với Performance – được khai báo bằng collection như List hoặc Set.
Nếu class Musician chứa 1 trường là Band, mối quan hệ giữa chúng có thể là many-to-one, thể hiện có 1 tập hợp các Musician trong 1 class Band (giả sử mỗi nhạc sĩ – musician chỉ chơi trong 1 nhóm nhạc -band duy nhất).
Nếu class Musician chứa 1 trường là BandMates, nó có thể mô tả mối quan hệ many-to-many giữa các entity Musician với nhau.
Cuối cùng, Musician có thể có quan hệ one-to-one với 1 Quote entity, dung để hiển thị 1 câu nói nổi tiếng: Quote famousQuote = new Quote().

Định nghĩa kiểu quan hệ

JPA tích hợp các annotation cho từng kiểu mapping quan hệ. Ví dụ bên dưới sử dụng để đánh dấu mối quan hệ one-to-many giữa MusicianPerformances.

public class Musician {
  @OneToMany
  @JoinColumn(name="musicianId")
  private List performances = new ArrayList();
  //...
}

Cần lưu ý rằng annotation @JoinColumn báo cho JPA biết cột nào trong bảng Performance
sẽ được liên kết đến entity Musician, mỗi performance sẽ được liên kết với 1 Musician và được tracking theo cột đó. Khi lưu 1 entity Musician hoặc Performance
vào database, JPA sẽ sử dụng thông tin này để tổ chức lại sơ đồ đối tượng.

Các trạng thái của entity và detached entity

Một entity là một đối tượng được map với ORM và luôn có 1 trong 4 trạng thái sau: transient, managed, detached, removed.
Trong trường hợp ta gặp một detached entity – đồng nghĩa với việc entity này không còn liên kết với database, các cập nhật hoặc thay đổi của entity này sẽ không được JPA ghi nhận và đồng bộ xuống database. Ta có thể liên kết lại (reattach) kiểu entity này bằng phương thức entityManager.merge().

Một entity vừa được khởi tạo sẽ ở trạng thái Transient (transient entity) và không tương ứng với bất kỳ một dòng dữ liệu nào trong database. Để chuyển qua trạng thái Persistent chúng ta cần gọi entityManager.persist().

Một managed object
là một entity ở trạng thái persistent.

Khi một entity đã được xóa khỏi database nhưng vẫn còn tồn tại như một đối tượng trong chương trình sẽ có trạng thái làremoved
.

Tác dụng của EntityManager.flush()

Những người mới làm quen với JPA thường không nắm rõ về mục đích của phương thức EntityManager.flush() . JPA manager sử dụng bộ nhớ đệm để lưu các hành vi cần thiết để duy trì sự đồng bộ giữa trạng thái của các entity với database, và lưu trữ hàng loạt để tăng hiệu quả.

Đôi khi, ta cần kích hoạt thủ công JPA đồng bộ entity xuống database. Trong trường hợp đó, ta có thể sử dụng phương thức flush(), và tất cả các entity chưa ở trạng thái persisted đều sẽ được đồng bộ xuống database ngay lập tức.

Kiểu Fetching

Bên cạnh việc xác định vị trí lưu trữ các entity trong database, ta cũng cần xác định cách để load chúng bằng JPA. Kiểu fetching cho JPA biết cách thức để load các entity có quan hệ với nhau. Khi truy xuất và lưu đối tượng, một JPA framework sẽ phải cung cung chức năng xử lý mô hình đối tượng.
Ta có thể sử dụng annotation để tùy biến kiểu fetching hoặc sử dụng cấu hình mặc định của JPA:

  • One-to-many: Lazy
  • Many-to-one: Eager
  • Many-to-many: Lazy
  • One-to-one: Eager

Cài đặt và thiết lập JPA

Ví dụ dưới đây sẽ minh họa cách cài đặt và thiết lập JPA cho ứng dụng Java sử dụng trình triển khai là EclipseLink. Đầu tiên ta cần thêm dependency EclipseLink vào file Maven pom.xml

<dependency>
    <groupId>org.eclipse.persistence</groupId>
    <artifactId>eclipselink</artifactId>
    <version>4.0.0-M3</version>
</dependency>

Tiếp theo, ta cần thêm dependency để kết nối với database.

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.29</version>
</dependency>

Sau đó, ta cần cấu hình liên kết database trong file persistence.xml

http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
      <persistence-unit name="MyUnit" transaction-type="RESOURCE_LOCAL">
           <properties>
                        <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/foo_bar"/>
                        <property name="jakarta.persistence.jdbc.user" value=""/>
                        <property name="jakarta.persistence.jdbc.password" value=""/>
                        <property name="jakarta.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
           </properties>
      </persistence-unit>
</persistence>

Cấu hình Spring cho JPA

Sử dụng Spring sẽ giúp việc ứng dụng JPA trong chương trình của bạn trở nên dễ dàng hơn. Ví dụ, annotation @SpringBootApplication sẽ giúp Spring tự động scan các class và then EntityManager theo yêu cầu, dựa trên cấu hình mà bạn cài đặt.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.6.7</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>2.6.7</version>
</dependency>

Khi nào nên dùng JPA

Việc có nên sử dụng JPA hay không luôn là một câu hỏi đau đầu trong giai đoạn thiết kế ứng dụng Java.
Đặc biệt là khi quyết định lựa chọn công nghệ nào được áp dụng từ đầu, bạn sẽ không muốn nhận được data persistence không chính xác – yếu tố có tính quan trọng và kéo dài. Để giải quyết vấn đề này, hãy nhớ rằng các ứng dụng có thể phát triển và mở rộng để sử dụng JPA. Ta có thể xây dựng chương trình sử dụng JDBC hoặc NoSQL library sau đó thêm vào JPA, chúng đều có thể hoạt động đồng thời một cách hiệu quả.

Một thử thách tiếp theo là khi được tích hợp, một số cấu hình của JPA có thể gây khó khăn trong việc phát triển ứng dụng. JPA có thể rất hữu ích cho tổng thể hệ thống về sự ổn định và dễ bảo trì, và đó là những mục tiêu chính của những dự án lớn hoặc phức tạp, tuy nhiên, ta đều biết đôi khi càng đơn giản càng tốt, đặc biệt là ở giai đoạn đầu của dự án.

Nếu team của bạn chưa có khả năng tích hợp JPA ngay từ đầu, hãy cân nhắc đến việc sử dụng nó trong tương lai

Tổng kết

Mọi ứng dụng làm việc với database đều nên định nghĩa một layer có vai trò duy nhất là để tách biệt phần code cho persistence. Như đã tìm hiểu trong bài viết, Jakarta Persistence API giới thiệu 1 loạt chức năng và công cụ cho Java object persistence. Những ứng dụng đơn giản có thể sẽ không cần đến tất cả các chức năng của JPA, và trong 1 số trường hợp, việc cấu hình JPA ngay từ đầu có thể không cần thiết. Khi ứng dụng được phát triển và mở rộng, mô hình và tính đóng gói của JPA thực sự phát huy vai trò hữu ích. Sử dụng JPA giúp code trở nên đơn giản và dễ dàng truy cập dữ liệu trong ứng dụng Java.