作者:鐘昕靈,叩丁狼教育高級講師。原創文章,轉載請注明出處。
JPA簡介
JPA是Java Persistence API的簡稱,中文名Java持久層API,是JDK 5.0注解或XML描述對象-關系表的映射關系,并將運行期的實體對象持久化到數據庫中。
Sun引入新的JPA ORM規范出于兩個原因:
其一,簡化現有Java EE和Java SE應用開發工作;
其二,Sun希望整合ORM技術,實現天下歸一。
JPA的宗旨是為POJO提供持久化標準規范,由此可見,經過這幾年的實踐探索,能夠脫離容器獨立運行,方便開發和測試的理念已經深入人心了。Hibernate3.2+、TopLink 10.1.3以及OpenJPA都提供了JPA的實現。
JPA的總體思想和現有Hibernate、TopLink、JDO等ORM框架大體一致。總的來說,JPA包括以下3方面的技術:
ORM映射元數據
JPA支持XML和JDK5.0注解兩種元數據的形式,元數據描述對象和表之間的映射關系,框架據此將實體對象持久化到數據庫表中;
API
用來操作實體對象,執行CRUD操作,框架在后臺替代我們完成所有的事情,開發者從繁瑣的JDBC和SQL代碼中解脫出來。
查詢語言
這是持久化操作中很重要的一個方面,通過面向對象而非面向數據庫的查詢語言查詢數據,避免程序的SQL語句緊密耦合。
JPA開發環境搭建
- jar包的依賴
如果是maven項目,將下面的配置添加到pom.xml文件中
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<target>1.8</target>
<source>1.8</source>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.3.5.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.5.Final</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.21</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.6</version>
</dependency>
</dependencies>
如果是普通的java項目,將下面的jar包添加到項目的lib目錄中
- persistence.xml文件
如果是maven項目,在src/main/resources下創建META-INF文件夾,將persistence.xml文件放在該目錄下
如果是普通的java項目,在src下創建META-INF文件夾,將persistence.xml文件夾放在該目錄下
在persistence.xml文件中做如下配置
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0">
<!--
JPA根據下面的配置信息創建EntityManagerFactory,一個項目中可以配置多個持久單元
name:為當前持久單元命名,可以通過該名稱指定加載對應的配置信息
-->
<persistence-unit name="myPersistence">
<!--指定掃描貼Entity實體類所在的jar包-->
<properties>
<!--數據庫的方言,告訴JPA當前應用使用的數據庫-->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/>
<!--jpa的相關的配置信息-->
<property name="javax.persistence.jdbc.url" value="jdbc:mysql:///jpa"/>
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="javax.persistence.jdbc.password" value="admin"/>
<!--是否在控制臺打印執行的sql語句-->
<property name="hibernate.show_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
到此,開發JPA應用的環境就搭建完成,接下來,在此基礎上來完成基本的CRUD操作吧
基于JPA的CRUD
- 實體類及映射配置
//getter/setter和toString方法
@Getter@Setter@ToString
//JPA會掃描到貼了Entity注解的類,將其作為需要持久化的類
@Entity
//根據需求,對類和表做相關映射(如:表名)
@Table(name="user")
public class User {
//標識該字段為主鍵列對應的字段
@Id
//指定主鍵的生成策略
@GeneratedValue(strategy = GenerationType.AUTO)
//為當前字段和對應的列做映射(如:列名,列的長度等)
@Column(name = "id")
private Long id;
@Column(name = "name",length = 20)
private String name;
@Column(name = "sn",nullable = false)
private String sn;
//對日期類型做映射
@Temporal(TemporalType.DATE)
private Date hiredate;
}
- EntityManagerFactory和EntityManager對象的創建
- EntityManagerFactory:JPA通過加載META-INF/persistence.xml文件中配置的persistence-unit創建EntityManagerFactory對象,該對象相當于一個連接池對象,用來創建EntityManager,是線程安全的,多線程可以共用同一個EntityManagerFactory,創建該對象需要消耗較多的資源,所以通常一個項目只需要創建一個EntityManagerFactory對象
- EntityManager:相當于一個連接對象,該對象線程不安全,所以,每次對象數據庫的訪問應該創建一個新的EntityManager對象
public class JPAUtil {
private static EntityManagerFactory emf;
private JPAUtil() {}
static {
//加載persistence.xml文件中的persistence-util中的配置信息創建EntityManagerFactory對象
emf = Persistence.createEntityManagerFactory("myPersistence");
}
//使用EntityManager創建EntityManager對象
public static EntityManager getEntityManager() {
return emf.createEntityManager();
}
}
- 保存操作
@Test
public void testSave() throws Exception {
//封裝需要持久化的數據
User u = new User();
u.setName("Neld");
u.setSn("sn");
u.setHiredate(new Date());
EntityManager em = JPAUtil.getEntityManager();
//開啟事務
em.getTransaction().begin();
//執行保存
em.persist(u);
//提交事務
em.getTransaction().commit();
//釋放資源
em.close();
}
- 刪除操作
@Test
public void testDelete() throws Exception {
EntityManager em = JPAUtil.getEntityManager();
em.getTransaction().begin();
User u = em.getReference(User.class, 1L);
//執行刪除,將持久化狀態的對象從數據庫中刪除
em.remove(u);
em.getTransaction().commit();
em.close();
}
- 修改操作
@Test
public void testUpdate() throws Exception {
EntityManager em = JPAUtil.getEntityManager();
em.getTransaction().begin();
User u = em.find(User.class, 1L);
u.setName("xxxx");
em.merge(u);
em.getTransaction().commit();
em.close();
}
- 查詢操作
@Test
public void testGet() throws Exception {
EntityManager em = JPAUtil.getEntityManager();
//查詢指定類型和OID的用戶信息
User u = em.find(User.class, 1L);
em.close();
System.out.println(u);
}
- CRUD小結
persistence.xml文件的配置
配置連接數據庫的基本信息
JPA的基本行為配置實體類的基本映射
@Entity:標注該類為持久化類
JPA掃描到類上的注解,會將當前類作為持久化類
@Table:配置當前類和表的相關映射
下面的注解可以貼在字段或者是get方法上,
如果選定了一個位置,那么所有的屬性相關的注解都應該貼在這個位置,意思是說,不能一部分在字段上,一部分在get方法上
@Id:主鍵屬性的映射---和表中的主鍵映射
@GeneratedValue:主鍵生成策略(指定生成主鍵的方式:自增長/手動設置)
@Column:配置當前屬性和列的映射
@Temporal:對日期類型的屬性映射(Date/DateTime/TimeStemp)完成CRUD的步驟
加載persistence.xml文件,使用指定的<persistence-unit>配置創建EntityManagerFactory對象,相當于根據配置信息創建一個連接池對象
創建EntityManager對象,相當于獲取到一個連接對象
開啟事務
執行crud相關的方法(persist/merge/remove/find),查詢所有調用Query中的getResultList方法
Persist:保存數據
Merge:保存或者更新,當對象有OID的時候,更新,反之,保存
Remove:刪除數據
Find:根據主鍵查詢數據
Query:其他的查詢需要使用該對象,傳入對應的JPQL(相當于SQL),調用getResultList方法執行查詢,返回對應的List集合
提交事務
釋放資源
hbm2ddl工具的使用
在持久層應用的開發過程中,我們發現,實體類和表結構是一一對應的,所以,我們會想,是否可以讓JPA根據實體類和對應的映射信息的配置,為我們自動的生成對應的表結構呢?
答案是肯定的,又因為我們現在講的是hibernate對JPA的實現,所以我們應用hibernate中提供的hbm2ddl工具來實現,配置很簡單,在persistence.xml文件中作如下配置即可
<property name="hibernate.hbm2ddl.auto" value="create"/>
接下來,我們來解釋一下每種策略的含義及使用場景
- hibernate.hbm2ddl.auto=create
在啟動的時候先刪除被管理的實體對應的表,然后再創建jpa管理的實體類對應的表 - hibernate.hbm2ddl.auto=create-drop
和create一致,只是在關閉系統之前會刪除jpa管理的所有的表 - hibernate.hbm2ddl.auto=update
在啟動的時候,檢查實體類和表結構是否有變化,如果有,執行更新表結構相關的sql
如果添加一個屬性,JPA可以幫我們在表中添加對應的列
如果刪除一個屬性,JPA不會幫我們去表中刪除對應的列
如果修改一個屬性(類型),JPA不會幫我們去表中刪除對應的列 - hibernate.hbm2ddl.auto=validate
在啟動的時候,檢查實體類和表結構是否有變化,如果有,啟動失敗,拋出異常
Caused by: org.hibernate.HibernateException: Missing column: sn in jpa.user
選擇:
- 在開發階段,我們通常使用create或者create-drop,可以快速的創建對應的表結構
- 在測試階段,不要使用create或者create-drop,因為這樣會將我們辛苦錄入的測試數據刪除,所以,我們使用update,在實體類修改的時候,更新表結構即可
- 在生產環境中,我們通常使用validate,這樣可以在啟動階段發現表結構相關的問題,至于表結構的修改,交給我們的DBA去完成吧.
單對象映射中常用的注解
- 對象映射相關
@Entity:
對實體類的映射,默認使用當前類的簡單名稱作為類名,如在使用JPQL做查詢的時候,使用該名字實現數據的查詢
JPQL語句:SELECT u FROM User u;
User:為默認使用的類名,可以通過Entity中的name屬性修改
@Entity(name=”UserInfo”):將類的名稱修改為UserInfo,那么上面的JPQL中的User修改為UserInfo即可@Table:
指定實體類映射的表的相關信息,如:表名,默認和類名一致
@Table(name=”t_user”):將映射的表名修改為t_userpersistence.xml文件中的相關元素的配置說明
<class>:指定需要掃描的實體類
<exclude-unlisted-classes>:設置為true的時候,表示不掃描這里沒有列出來的類
<jar-file>:指定對項目中引入的jar包中的類進行掃描
- 屬性相關:
-
@GeneratedValue,主鍵生成策略
在一張表中,主鍵列的信息通常需要受到程序員的特殊關照,這里我們需要探討一下主鍵的生成方式(自動生成/手動設值)
首先,我們需要在主鍵屬性上使用@GeneratedValue注解中的strategy屬性來設值主鍵的生成方式strategy=GenerationType.AUTO
把主鍵生成策略交給JPA廠商(Persistence Provider),由它根據具體的數據庫選擇合適的策略,可以是Table/Sequence/Identity中的一種。假如數據庫是Oracle,則選擇Sequence。
如果不做特別指定,默認是使用這種方式生成主鍵strategy=GenerationType.IDENTITY
多數數據庫支持IDENTITY,數據庫會在新行插入時自動給ID賦值,這也叫做ID自增長列,比如MySQL中可以在創建表時聲明“AUTO_INCREMENT”,該策略在Oracle數據庫中不支持strategy=GenerationType.TABLE
有時候為了不依賴于數據庫的具體實現,在不同數據庫之間更好的移植,可以在數據庫中新建序列表來生成主鍵,序列表一般包含兩個字段:第一個字段引用不 同的關系表,第二個字段是該關系表的最大序號。這樣,只需要一張序列就可以用于多張表的主鍵生成。
如果不指定表生成器,JPA廠商會使用默認的表,比如Hibernate在Oracle數據庫上會默認使用表hibernate_sequence。
這種方式雖然通用性最好,所有的關系型數據庫都支持,但是由于不能充分利用具體數據庫的特性,建議不要優先使用。strategy=GenerationType.SEQUENCE
Oracle不支持ID自增長列而是使用序列的機制生成主鍵ID,對此,可以選用序列作為主鍵生成策略:
如果不指定序列生成器的名稱,則使用廠商提供的默認序列生成器,比如Hibernate默認提供的序列名稱為hibernate_sequence。
支持的數據庫: Oracle、PostgreSQL、DB2
屬性映射
@Column:
使用該注解可以對屬性和列進行相關映射
該注解可以貼在字段上,也可貼在getter方法上,但是必須是統一的,不能一部分在字段上,一部分在getter方法上
@Access
在實際開發中,也可以告訴JPA只去掃描哪個位置上的@Column注解,如果沒有就不在去其他地方掃描
@Access(AccessType.PROPERTY):屬性,對應著get方法
@Access(AccessType.FIELD):字段:對應字段@Column
name:列名,通常,屬性名和列名一直的時候,不需要指定,默認使用屬性名作為列名
unique:唯一性約束
nullable:非空約束
insertable:false,表示在生成insert語句的時候不插入這一列的值
updatable:false,表示在生成update語句的時候不更新這一列的值
length:指定該列的長度
columnDefination:自定義列的類型,默認是JPA根據屬性的類型自動生成
precision:在使用decimal類型的時候指定總長度
scale:在使用decimal類型的時候指定小數位數@Temporal:
日期類型的映射
指定日期類型的屬性對應的列的類型(date/datatime/timestamp)@Transient:
非持久化類型的映射
JPA在做對象關系映射的時候,默認是對實體類中的所有屬性進行映射的,如果有不需要映射的屬性,可以使用該注解完成@Lob:
大數據類型的映射
對象如果是String類型的,默認情況下載表中映射的是VARCHAR類型
該注解可以對應text/blob/clob類型進行映射,如:
@Lob
private String content;
一級緩存
在EntityManager中存在一個緩存區域,稱之為一級緩存
在該緩存區中,會將查詢到的對象緩存到該區域中
如果在同一個EntityManager中,查詢相同OID的數據,那么只需要發送一條sql
在事務提交/關閉EntityManager之后,一級緩存會清空,所以在不同的EntityManager中使用不
同的一級緩存
一級緩存也可以使用下面的方法手動清除緩存數據
detach:清除一級緩存中指定的對象
clear:清除一級緩存中的所有的緩存數據
但是一級緩存的緩存能力是非常有限的,因為我們不會經常在一個EntityManager中查詢相同的數據
延遲加載
JPA中,根據主鍵查詢數據可以使用下面兩個方法完成:
<T> T find(Class<T> type, Object oid);
<T> T getReference(Class<T> type, Object oid);
相同點:都是根據主鍵查詢指定類型的數據
不同點: getReference方法是在真實使用該對象的時候才會發送查詢的sql語句,如
public void testGetReference() throws Exception {
EntityManager em = JPAUtil.getEntityManager();
//這里不會立即發送sql查詢
User u = em.getReference(User.class, 1L);
System.out.println("-------------");
//在訪問User對象中的屬性值的時候表示真正使用該對象
System.out.println(u.getName());
em.close();
}
執行結果:
-------------
Hibernate: select user0_.id as id1_0_0_, user0_.hiredate as hiredate2_0_0_, user0_.name as name3_0_0_, user0_.sn as sn4_0_0_ from User user0_ where user0_.id=?
Neld
根據執行的打印結果可以看到,是我們在真正使用該對象的時候才會執行查詢的sql,而在這之前是不會發送SQL執行數據的查詢
延遲加載
getReference方法查詢數據的方式我們稱之為延遲加載
什么是延遲加載? 就是不會立即執行查詢的sql,而是延遲到真正使用的時候再執行,上面的例子已經證明了這一點
再觀察:
find方法查詢到的結果,如果查詢到了對應的數據,返回查詢到的結果即可,反之,返回null,所以可以使用ifnull判斷是否有數據
getReference方法查詢到的結果,無論是否查詢到了數據,結果都不會是null,所以不能使用ifnull判斷是否有對應的數據
如果在表中沒有對應的數據,拋出異常
javax.persistence.EntityNotFoundException: Unable to find cn.wolfcode._01_hello.User with id 2
原理:
JPA使用動態代理機制實現延遲加載,覆寫該對象中的所有的getter方法,在getter方法中執行查詢當前對象的sql
延遲加載需要搞懂的問題:
1.延遲加載什么時候發送SQL執行數據?
2.為什么需要在關閉EntityManager對象之前初始化延遲加載對象?
3.為什么在訪問對象的get方法的時候,會去初始化當前對象(發送SQL執行查詢)呢?
4.使用find方法沒有查詢到數據的時候,返回值是什么?使用getReference方法沒有查詢到數據的時候,返回值是什么?
對象狀態
對象的狀態是JPA中非常重要的概念,描述了實體對象從瞬時到持久、從刪除到游離的狀態變換。對實體的操作其實就是對象實體狀態的改變, 這對于我們分析SQL的執行情況有很大的幫助。
- 瞬時狀態(Transient)
使用new關鍵字創建出來的新對象,沒有OID,不在一級緩存中 - 持久狀態(Persistent)
調用持久化方法之后,將對象保存到數據庫中,對象狀態轉化成持久狀態 - 游離狀態(Detached)
對象存在于數據庫中,但是不在一級緩存中 - 刪除狀態(Removed)
事務一旦提交,對象就會被從數據庫中刪除,是介于持久狀態和被刪除之間的一個臨界狀態
我們可以通過下面的表格了解到各個狀態的特點:
狀態 | 是否在一級緩存 | 是否有OID |
---|---|---|
瞬時狀態(Transient) | 否 | 否 |
持久狀態(Persistent) | 是 | 是 |
游離狀態(Detached) | 否 | 是 |
刪除狀態(Removed) | 是 | 是 |
EntityManager提供一系列的方法管理實體對象的狀態,包括:
- persist, 將新創建的或已刪除的實體轉變為Persistent狀態,數據存入數據庫。
- remove,刪除持久狀態的實體
- merge,將游離實體轉變為Persistent狀態,數據存入數據庫。
如果使用了事務管理,則事務的commit/rollback也會改變實體的狀態。
如圖:
有了對對象狀態的了解之后,我們來分析面的案例中sql的發送
@Test
public void test() throws Exception{
EntityManager em = JPAUtil.getEntityManager();
em.getTransaction().begin();
//通過find方法查詢到處于持久狀態的User對象
User u = em.find(User.class, 1L);
u.setName("Lucy");//①
em.getTransaction().commit();
em.close();
}
執行結果:
Hibernate: select user0_.id as id1_0_0_, user0_.hiredate as hiredate2_0_0_, user0_.name as name3_0_0_, user0_.sn as sn4_0_0_ from User user0_ where user0_.id=?
Hibernate: update User set hiredate=?, name=?, sn=? where id=?
- 分析:
①:在這里,我們修改了查詢出來處于持久狀態的User對象的name屬性的值
我們并沒有調用merge方法去更新User對象,為什么會發送update語句呢?
- 原因:
首先,將數據從數據庫中查詢出來后,在內存中會有兩份數據,一份在EntityManager一級緩存區域,一份在EntityManager的快照區,兩份數據完全一樣
然后,修改User的name屬性時,其實是修改的緩存區的數據
最后,在提交事務的時候,會清理一級緩存,此時會對比兩份數據是否一致,如果不一致,發送對應的update語句將緩存中的臟數據(和數據庫中的數據不一致)同步到數據庫中
所以,在上面的例子中,我們看到執行了一條更新語句,這樣相信大家就能夠理解了,這也是在我們了解了對象的狀態之后對SQL的發送有了更深入的認識