12/08/2018, 16:05

JPA tut

Table Of Contents Introduction Project setup Basics 3.1. EntityManager and Persistence Unit 3.2. Transactions 3.3. Tables Inheritance Relationships 5.1. OneToOne 5.2. OneToMany 5.3. ManyToMany 5.4. Embedded / ElementCollection Data Types and Converters Criteria API Sequences ...

Table Of Contents

  1. Introduction
  2. Project setup
  3. Basics 3.1. EntityManager and Persistence Unit 3.2. Transactions 3.3. Tables
  4. Inheritance
  5. Relationships 5.1. OneToOne 5.2. OneToMany 5.3. ManyToMany 5.4. Embedded / ElementCollection
  6. Data Types and Converters
  7. Criteria API
  8. Sequences
  9. Download

1. Introduction

JPA là một chuẩn đặc tả cho việc ánh xạ giữa các đối tượng java và csdl quan hệ thông qua công nghệ ORM (Object Relational Mapping). JPA bao gồm 3 thành phần khác nhau:

  • Entities : Trong phiên bản hiện tại của JPA entities là plain old Java objects (POJOs).
  • Object-relational metadata : Cung cấp anh xạ giữa các đối tượng java với tên bảng, tên các thuộc tính trong csdl. Điều này có thể làm bằng cách sử dụng tập tin cấu hình hoặc dùng các annotation.
  • Java Persistence Query Language (JPQL) : cung cấp một số truy vấn có thể được dùng thay cho SQL (tạm gọi là phương ngữ). Phương ngữ này sẽ được dịch sang SQL vì vậy phù hợp với các lại csdl quan hệ khác nhau.

Ví dụ dưới đây sử dụng môi trường :

  • maven> = 3.0
  • JPA 2.1( Java Enterprise Edition - JEE 7.0)
  • Hibernate 4.3.8.Final (thực hiện chuẩn đặc tả của JPA)
  • csdl H2 (version 1.3.176)

2. Project setup

Tạo project maven mvn archetype:create -DgroupId=com.javacodegeeks.ultimate -DartifactId=jpa File pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.javacodegeeks.ultimate</groupId>
    <artifactId>jpa</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <jee.version>7.0</jee.version>
		<h2.version>1.3.176</h2.version>
		<hibernate.version>4.3.8.Final</hibernate.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>${jee.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>${h2.version}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>${hibernate.version}</version>
        </dependency>
    </dependencies>
</project>

3. Basics

3.1. EntityManager and Persistence Unit

Tạo một class đơn giản chứa phương thức run()

public class Main {
	private static final Logger LOGGER = Logger.getLogger("JPA");

	public static void main(String[] args) {
		Main main = new Main();
		main.run();
	}

	public void run() {
		EntityManagerFactory factory = null;
		EntityManager entityManager = null;
		try {
			factory = Persistence.createEntityManagerFactory("PersistenceUnit");
			entityManager = factory.createEntityManager();
			persistPerson(entityManager);
		} catch (Exception e) {
			LOGGER.log(Level.SEVERE, e.getMessage(), e);
			e.printStackTrace();
		} finally {
			if (entityManager != null) {
				entityManager.close();
			}
			if (factory != null) {
				factory.close();
			}
		}
	}
	...

Gần như tất cả các tương tác với JPA được thực hiện thông qua EntityManager. Để lấy một thể hiện của EntityManager chúng ta phải tạo qua EntityManagerFactory. Thông thường chúng ta chỉ cần một EntityManagerFactory cho mỗi persistence-unit cho mỗi ứng dụng. Mỗi pesistence-unit là một tập hợp các JPA classes cùng với cấu hình csdl trong tập tin persistence.xml

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
 http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="PersistenceUnit" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <property name="connection.driver_class" value="org.h2.Driver"/>
            <property name="hibernate.connection.url" value="jdbc:h2:~/jpa"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.hbm2ddl.auto" value="create"/>
            <property name="hibernate.show_sql" value="true"/>
			<property name="hibernate.format_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Tập tin này được tạo ra trong thư mục src/main/resource/META-INF của maven project. Như các bạn thấy chúng định nghĩa một persistence-unit với tên là PesistenceUnit có một transaction-type kiểu RESOURCE_LOCAL (vì đây là dứng dụng local nên tôi chỉ định kiểu này). Khi bạn sử dụng một container JEE thì container có trách nhiệm thiết lập EntityManagerFactory và chỉ cung cấp cho bạn EntityManager. Container cũng xử lý sự bắt đầu và kết thúc của mỗi giao dịch. Trong trường hợp này, bạn sẽ cung cấp giá trị JTA. Bằng cách thiết lập provider làorg.hibernate.ejb.HibernatePersistence, tức đây là JPA implementation chúng ta muốn sử dụng. Bởi vì đã khai báo dependency vì vậy chúng ta có thể tham chiếu provider impelementation ở đây bằng tên lớp. Bước tiếp theo sẽ là thông báo cho JPA provider biết về lạo csdl chúng ta muốn sử dụng. Trong trường hợp trên tôi đang cung cấp thuộc tính connection.driver_class là org.h2.Driver. Để cho phép Hibernate tạo các kết nối đến cơ sở dữ liệu, chúng ta cũng phải cung cấp URL kết nối. H2 cung cấp tùy chọn để tạo ra cơ sở dữ liệu trong một tệp duy nhất mà đường dẫn được đưa ra trong URL JDBC. Do đó URL JDBC jdbc: h2: ~ / jpa nói với H2 để tạo trong thư mục chính chứa DB của người dùng là tệp jpa.h2.db. (Các thông số khác cần thiết cho việc kết nối sử dụng Hibernate FW các bạn tham khảo và tìm hiểu thêm trên GG) H2 là một cơ sở dữ liệu mã nguồn mở có thể được dễ dàng nhúng trong các ứng dụng Java vì nó là một tệp tin jar đơn với kích thước khoảng 1,5 MB. Điều này làm cho nó phù hợp với mục đích của tôi là tạo ra một ứng dụng mẫu đơn giản để chứng minh việc sử dụng JPA. Trong các dự án đòi hỏi các giải pháp có quy mô tốt hơn với số lượng lớn dữ liệu bạn có thể chọn một sản phẩm cơ sở dữ liệu khác, nhưng với số lượng nhỏ dữ liệu có mối quan hệ mạnh mẽ thì H2 là sự lựa chọn tốt. Điều tiếp theo chúng ta phải nói với Hibernate là phương ngữ JDBC mà nó nên sử dụng. Hibernate cung cấp một phương thức dành riêng cho H2, chúng ta chọn một phương thức này với thuộc tính hibernate.dialect. Với phương ngữ này Hibernate có khả năng tạo các câu lệnh SQL thích hợp cho cơ sở dữ liệu H2. Cuối cùng là một phần cũng khá quan trọng, đó là cung cấp 3 tùy chọn hữu ích khi phát triển một ứng dụng mớ, nhứng no sẽ không được sử dụng trong môi trường production. Thứ nhất là thuộc tính hibernate.hbm2ddl.auto, nó sẽ nói với Hibernate tạo ra tất cả các bảng ngay khi ứng dụng khởi động. Tức là nếu như bảng đã tồn tại thì nó sẽ bị xóa, trong ví dụ này thì điều này tốt vì chúng ta muốn từ ban đầu và dữ liệu trống và dữ liệu sẽ được hình thành, được thay đổi trong quá trình chạy ứng dụng. Thứ hai là hibernate.show_sql, điều này sẽ nói với Hibernate in ra tất cả các câu sql trong ứng dụng trên console, qua đó có thể theo dõi câu sql có hoạt động đúng như mong đợi, hay là câu sql đã đúng hay chưa. Và thứ ba là thiết lập thuộc tính hibernate.format_sql bằng true để câu sl in ra trên console dễ nhìn hơn. Quay trở lại với mã Java bên trên :

EntityManagerFactory factory = null;
EntityManager entityManager = null;
try {
	factory = Persistence.createEntityManagerFactory("PersistenceUnit");
	entityManager = factory.createEntityManager();
	persistPerson(entityManager);
} catch (Exception e) {
	LOGGER.log(Level.SEVERE, e.getMessage(), e);
	e.printStackTrace();
} finally {
	if (entityManager != null) {
		entityManager.close();
	}
	if (factory != null) {
		factory.close();
	}
}

Sau khi có một thể hiện EntityManager từ EntityManagerFactory chúng ta có thể sử dụng phương thức persistPerson để lưu dữ liệu vào DB. Chú ý rằng sau kho hoàn thành công việc thì chúng ta cần đóng cả EntityManager và EntityManagerFactory.

3.2. Transactions

EntityManager đại diện cho một pesistence-unit, vì vậy chúng ta sẽ cần trong một ứng dụng RESOURCE_LOCAL chỉ có một thể hiện của EntityManager. Một pesistence-unit là một bộ nhớ cache cho các thực thể đại diện được lưu trữ trong csdl cũng như kết nối csdl. Để lưu trữ dữ liệu trong cơ sở dữ liệu, chúng ta phải chuyển nó tới EntityManager và cùng với bộ nhớ cache bên dưới. Trong trường hợp bạn muốn tạo một bản ghi mới trong cơ sở dữ liệu, điều này được thực hiện bằng cách gọi phương thức persist () trên EntityManager như bên dưới:

private void persistPerson(EntityManager entityManager) {
	EntityTransaction transaction = entityManager.getTransaction();
	try {
		transaction.begin();
		Person person = new Person();
		person.setFirstName("Homer");
		person.setLastName("Simpson");
		entityManager.persist(person);
		transaction.commit();
	} catch (Exception e) {
		if (transaction.isActive()) {
			transaction.rollback();
		}
	}
}

Nhưng trước khi gọi pesit() chúng ta cần mở một transaction mới bằng cứ pháp transaction.begin() trên một đối tượng giao dịch mới mà được lấy ra từ EntityManger. Nếu không đặt trong một transaction thì Hibernate sẽ ném ra một lỗi IllegalStateException vì không đặt pesit() trong một transaction.

java.lang.IllegalStateException: Transaction not active
	at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:70)
	at jpa.Main.persistPerson(Main.java:87)

Sau khi gọi persist() chúng ta sẽ gọi commit để đẩy dữ liệu xuống DB, trong trường hợp exception xảy ra mà không có xử lý try/catch, thì các thao tác sẽ được rollback. Tuy nhiên, vì chúng ta chỉ có thể rollback các giao dịch đang hoạt động, nên chúng ta phải kiểm tra trước khi giao dịch hiện tại đã chạy chưa, vì nó có thể xảy ra ngoại lệ được ném ra trong giao dịch transaction.begin ().

3.3. Tables

Class Person được mapping tới table T_PERSON trong DB bởi annotation @Entity

@Entity
@Table(name = "T_PERSON")
public class Person {
	private Long id;
	private String firstName;
	private String lastName;

	@Id
	@GeneratedValue
	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	@Column(name = "FIRST_NAME")
	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	@Column(name = "LAST_NAME")
	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
}

Annotation @Table là một option, nhưng bạn có thể sử dụng nó để chỉ định một tên bảng cụ thể. Bảng T_PERSON có ba cột: ID, FIRST_NAME, LAST_NAME. Thông tin này được cung cấp tới JPA provider với annotation @Column và thuộc tính cảu chúng. Ngay cả khi các annotation này là tùy chọn. JPA sẽ sử dụng tất các các thuộc tính của đối tượng Java có getter/setter để tạo ra các column cho chúng. Miễn là bạn không loại trừ chúng bằng annotation @Transient. Mặt khác bạn có thể chỉ định thêm thông tin cho mỗi cột bằng cách sử dụng các thuộc tính khác mà annotation @Comun cung cấp.

@Column(name = "FIRST_NAME", length = 100, nullable = false, unique = false)

Ví dụ trên chỉ ra độ dài ký tự của column này là 100, trạng thái là không được null, và không phải là unique. Nếu cố gắng insert vào một bản ghi mà có FIRST_NAME là null thì transaction hiện tại sẽ rollback. Hai annotation @Id và GeneratedValue nói với JPA rằng giá trị này là khóa chính của bảng và nó được tăng tự động. Trong ví dụ trên ta thêm các chú tích annotation và phương thức getter của thuộc tính. Một cách khác lalf ta có thể thêm trực tiếp vào thuộc tính

@Entity
@Table(name = "T_PERSON")
public class Person {
	@Id
	@GeneratedValue
	private Long id;
	@Column(name = "FIRST_NAME")
	private String firstName;
	@Column(name = "LAST_NAME")
	private String lastName;
	...

Cả hai cách này đều tương đương. Sự khác biệt duy nhất là khi bạn muốn ghi đè các thuộc tính trong các class con (kế thừa). Khi chúng ta đặt annotation ở dield thì chúng ta không thể ghi đè field đó ở lớp con kế thừa giống như đặt annotaion ở phương thức getter. Bạn có thể kết hợp các cách viết chú thích annotation ở dụ án JPA, nhưng trong một thực thể và các lớp con của nó phải nhất quán. Nếu bạn cần thay đổi cách dùng annotation ở phân lớp con, bạn có thể sử dụng annotation Access để xác định rằng lớp con này sử dụng một cách khác cho field và phương thức.

@Entity
@Table(name = "T_GEEK")
@Access(AccessType.PROPERTY)
public class Geek extends Person {
...

Nhìn vào đoạn code trên ta thấy class con sử dụng annotation ở cấp field trong khi có thể supper class sử dụng annotation ở cấp phương thức. Khi chạy mã trên, Hibernate sẽ phát hành các truy vấn sau đây đến cơ sở dữ liệu H2 ở local

Hibernate: drop table T_PERSON if exists
Hibernate: create table T_PERSON (id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), primary key (id))
Hibernate: insert into T_PERSON (id, FIRST_NAME, LAST_NAME) values (null, ?, ?)

Như chúng ta thấy đầu tiên Hibernate seax xóa bảng T_PERSON nếu như nó đã tồn tại, tạo lại nó sau đấy. Chúng ta có thể kiểm tra xem mọi thứ có chính xác không bằng cách sử dụng Shell có chứa H2. Để sử dụng Shell này chúng ta chỉ cần h2-1.3.176.jar:

>java -cp h2-1.3.176.jar org.h2.tools.Shell -url jdbc:h2:~/jpa
...
sql> select * from T_PERSON;
ID | FIRST_NAME | LAST_NAME
1  | Homer      | Simpson
(4 rows, 4 ms)

Kết quả truy vấn ở trên cho chúng ta thấy rằng bảng T_PERSON thực sự chứa một bản ghi có id 1 và có giá trị cho tên và họ.

4. Inheritance

Giả sử bây giờ chúng tôi muốn lưu thông tin về ngôn ngữ lập trình yêu thích của từng người.

@Entity
@Table(name = "T_GEEK")
public class Geek extends Person {
	private String favouriteProgrammingLanguage;
	private List<Project> projects = new ArrayList<Project>();

	@Column(name = "FAV_PROG_LANG")
	public String getFavouriteProgrammingLanguage() {
			return favouriteProgrammingLanguage;
	}

	public void setFavouriteProgrammingLanguage(String favouriteProgrammingLanguage) {
		this.favouriteProgrammingLanguage = favouriteProgrammingLanguage;
	}
	...
}

Thêm các annotation @Entity và @Table để Hibernate tạo bảng mới.

Hibernate: create table T_PERSON (DTYPE varchar(31) not null, id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), FAV_PROG_LANG varchar(255), primary key (id))

Như có thể thấy Hibernate tạo ra một bảng cho cả 2 thực thể và thiết lập các thông tin column của cả hai bảng, ngoài ra thêm một column DTYPE. Trường hợp dưới đây tôi sẽ bỏ qua khối try/catch, nếu có ngoại lệ xảy ra thì transaction sẽ rollback.

private void persistGeek(EntityManager entityManager) {
	EntityTransaction transaction = entityManager.getTransaction();
	transaction.begin();
	Geek geek = new Geek();
	geek.setFirstName("Gavin");
	geek.setLastName("Coffee");
	geek.setFavouriteProgrammingLanguage("Java");
	entityManager.persist(geek);
	geek = new Geek();
	geek.setFirstName("Thomas");
	geek.setLastName("Micro");
	geek.setFavouriteProgrammingLanguage("C#");
	entityManager.persist(geek);
	geek = new Geek();
	geek.setFirstName("Christian");
	geek.setLastName("Cup");
	geek.setFavouriteProgrammingLanguage("Java");
	entityManager.persist(geek);
	transaction.commit();
}

Sau khi chạy xong đoạn lệnh trên, hãy cùng nhìn lại bảng T_PERSON

sql> select * from t_person;
DTYPE  | ID | FIRST_NAME | LAST_NAME | FAV_PROG_LANG
Person | 1  | Homer      | Simpson   | null
Geek   | 2  | Gavin      | Coffee    | Java
Geek   | 3  | Thomas     | Micro     | C#
Geek   | 4  | Christian  | Cup       | Java

Theo dự kiến cột DTYPE xác định loại người (PERSON hay GEEKS), còn cột FAV_PROG_LANG có giá trị null cho loại PERSON (bản ghi đầu tiên). Trong phần tiếp theo giả sử bạn muốn có một cột là PERSON_TYPE kiểu số nguyên thay vì cột DTYPE kiểu string thì bạn làm như sau:

@DiscriminatorColumn(name="PERSON_TYPE", discriminatorType = DiscriminatorType.INTEGER)

Và kết quả là

sql> select * from t_person;
PERSON_TYPE | ID | FIRST_NAME | LAST_NAME | FAV_PROG_LANG
-1907849355 | 1  | Homer      | Simpson   | null
2215460     | 2  | Gavin      | Coffee    | Java
2215460     | 3  | Thomas     | Micro     | C#
2215460     | 4  | Christian  | Cup       | Java

Không phải trong mọi tình huống bạn đều muốn có một bảng lưu ctrữ cho tất cả các lọai đối tượng dữ liệu khác nhau. JPA cung cấp cho bạn 3 utyf chọn :

  • SINGLE_TABLE: Chiến thuật này giúp ánh xạ tất cả các bảng tới một table duy nhất. Kết quả là mỗi hàng có tất cả các cột, mặt khác chiến thuật này mang lại lợi thế là một query không bao giờ cần join để lấy ra dữ liệu. Vì vậy truy vấn sẽ nhanh hơn.
  • JOINED: Chiến thuật này tạo ra nhiều bảng, mỗi bảng chỉ ánh xạ đến một đối tượng. Cách tiếp cận này làm giảm không gian lưu trữ nhưng khi cần kết xuất dữ liệu từ nhiều bảng thì tốc độ truy vấn sẽ bị chậm.
  • TABLE_PER_CLASS: Giống như chiến lược JOINED, chiến lược này tạo ra một bảng riêng cho từng loại đối tượng. Nhưng ngược lại với chiến lược JOINED, các bảng này chứa tất cả các thông tin cần thiết để tải thực thể này. Do đó không có truy vấn join cần thiết để tải các thực thể. Để thay đổi cách thực thi sử dụng chiến thuật joined ta ta add thêm annotation trên class
@Inheritance(strategy = InheritanceType.JOINED)

Bây giờ hiberante sẽ ra tạo ra hai bảng T_PERSON và T_GEEK

Hibernate: create table T_GEEK (FAV_PROG_LANG varchar(255), id bigint not null, primary key (id))
Hibernate: create table T_PERSON (id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), primary key (id))

Lấy dữ liệu ra ta thấy như sau

sql> select * from t_person;
ID | FIRST_NAME | LAST_NAME
1  | Homer      | Simpson
2  | Gavin      | Coffee
3  | Thomas     | Micro
4  | Christian  | Cup
(4 rows, 12 ms)
sql> select * from t_geek;
FAV_PROG_LANG | ID
Java          | 2
C#            | 3
Java          | 4
(3 rows, 7 ms)

Như dự kiến dữ liệu được phân phối trên hai bảng. Bảng cơ sở T_PERSON chứa tất cả các thuộc tính chung trong khi bảng T_GEEK chỉ chứa các hàng cho mỗi geek. Mỗi hàng liên quan đến bản ghi trong bảng T_PERSON theo giá trị của cột ID. Khi phát hành một truy vấn cho lấy thông tin từng người, SQL sau đây được gửi tới cơ sở dữ liệu:

select
	person0_.id as id1_2_,
	person0_.FIRST_NAME as FIRST_NA2_2_,
	person0_.LAST_NAME as LAST_NAM3_2_,
	person0_1_.FAV_PROG_LANG as FAV_PROG1_1_,
	case 
		when person0_1_.id is not null then 1 
		when person0_.id is not null then 0 
	end as clazz_ 
from
	T_PERSON person0_ 
left outer join
	T_GEEK person0_1_ 
		on person0_.id=person0_1_.id

Chúng ta thấy rằng một truy vấn join là cần thiết để bao gồm các dữ liệu từ bảng T_GEEK và rằng Hibernate mã hóa các thông tin nếu một hàng là một geek hoặc bằng cách trả về một số nguyên (xem sql bên trên). Mã Java để phát hành một truy vấn như sau:

TypedQuery<Person> query = entityManager.createQuery("from Person", Person.class);
List<Person> resultList = query.getResultList();
for (Person person : resultList) {
	StringBuilder sb = new StringBuilder();
	sb.append(person.getFirstName()).append(" ").append(person.getLastName());
	if (person instanceof Geek) {
		Geek geek = (Geek)person;
		sb.append(" ").append(geek.getFavouriteProgrammingLanguage());
	}
	LOGGER.info(sb.toString());
}

Trước hết chúng ta tạo một đối tượng Query bằng cách gọi phương thức createQuery () của EntityManager. Mệnh đề truy vấn có thể bỏ qua từ khóa select. Tham số thứ hai giúp tham số hóa phương pháp sao cho Query là kiểu Person. Phát hành truy vấn chỉ đơn giản là thực hiện bằng cách gọiquery.getResultList (). Danh sách trả về là iterable, vì vậy chúng ta có thể lặp lại đối tượng Person. Nếu chúng ta muốn biết chúng ta có person hay một geek, chúng ta chỉ có thể sử dụng toán tử instanceof của Java. Chạy đoạn mã trên dẫn tới kết quả sau:

Homer Simpson
Gavin Coffee Java
Thomas Micro C#
Christian Cup Java

to be continued...

0