什么是JPA
JPA(Java Persistence API)是Java標准中的一套ORM規范,借助JPA技術可以通過注解或者XML描述【對象-關系表】之間的映射關系,並將實體對象持久化到數據庫中(即Object Model與Data Model間的映射)。
JPA之於ORM(持久層框架,如MyBatis、Hibernate等,用於管理應用層Object與數據庫Data之間的映射)正如JDBC之於數據庫驅動。
JDBC是Java語言定義的一套標准,規范了客戶端程序訪問關系數據庫(如MySQL、Oracle、Postgres、SQLServer等)的應用程序接口,接口的具體實現(即數據庫驅動)由各關系數據庫自己實現。
隨着業務系統的復雜,直接用JDBC訪問數據庫對開發者來說變得很繁瑣,代碼難以維護,為解決此問題,ORM(Object Relation Mapping)框架出現了,如MyBatis、Hibernate等,百花齊放。
愛大一統的Java又出手了,Java針對ORM提出了JPA,JPA 本質上是一種 ORM 規范,不是 ORM 框架,只是定制了一些規范,提供了一些編程的 API 接口,具體實現由 ORM 廠商實現,如Hiernate、Eclipselink等都是JAP的具體實現,主要有:

另:關於Java Persistence規范的演進(OMG、EJB1.0 CMP、EJB2.0 CMP等)可參閱:https://en.wikibooks.org/wiki/Java_Persistence/What_is_JPA%3F
JPA was meant to unify the EJB 2 CMP, JDO, Hibernate, and TopLink APIs and products
It is a standard and part of EJB3 and Java EE.
JPA主要包括Statix Named Query、Criteria Query API兩部分(Query包含select、update、delete、insert等)。分為靜態查詢和動態查詢:
靜態查詢在編譯期即確定查詢邏輯,為Static Named Query,如getByName等。
動態查詢運行時確定查詢邏輯,主要是Criteria API。Spring的Specification Query API對Criteria API進行了簡化封裝,此外Spring還提供了Example動態查詢(query by example (QBE))。
使用JPA Query時與SQL Query最大的區別在於前者是面向Object Model(即定義的Java Bean)而后者是面向Data Model(即數據庫表)的。
JPQL allows the queries to be defined in terms of the object model, instead of the data model. Since developers are programming in Java using the object model, this is normally more intuitive. This also allows for data abstraction and database schema and database platform independence.
Spring data JPA與JPA的關系
如上面所述,JPA是Java標准中的一套規范。其為我們提供了:
- ORM映射元數據:JPA支持通過XML和注解兩種元數據形式描述對象和表間的映射關系,並持久化到數據庫表中。如@Entity、@Table等
- JPA的Criteria API:提供API來操作實體對象,執行CRUD操作,框架會自動將之轉換為對應的SQL,使開發者從繁瑣的JDBC、SQL中解放出來。
- JPQL查詢語言:提供面向Java對象而非面向數據庫自動的查詢語言,避免程序與SQL語句耦合
關系圖:

Spring Data JPA是Spring提供的一套簡化JPA開發的框架(Criteria API還是太復雜了),按照約定好的【方法命名規則】寫dao層接口,就可以在不寫接口實現的情況下,實現對數據庫的訪問和操作。同時提供了很多除了CRUD之外的功能,如分頁、排序、復雜查詢等等。
關系圖:

通過Repository來支持上述功能,默認提供的幾種Repository已經滿足了絕大多數需求:
JpaRepository( 為Repository的子接口:JpaRepository -> PagingAndSortingRepository -> CrudRepository -> Repository)
QueryByExampleExecutor
JpaSpecificationExecutor
后兩者用於更復雜的查詢,如動態查詢、關聯查詢等;第一種用得最多,提供基於方法名(query method)的查詢,用戶可基於第一種繼承創建自己的子接口(只要是Repository的子接口即可),並聲明各種基於方法名的查詢方法。
題外話:PagingAndSortingRepository及其繼承的幾個接口實際上不僅可用於Spring Data JPA,還可用於Spring Data MongoDB等,可見可復用性很好。
Spring Data JPA 其實並不依賴於 Spring 框架。
JPA注解
注解位置
通過JPA定義的Object至少需要@Entity、@Id注解,示例:
import javax.persistence.*; ... @Entity public class Employee { @Id private long id; private String firstName; private String lastName; private Address address; private List<Phone> phones; private Employee manager; private List<Employee> managedEmployees; ... ... }
這些注解的位置可以有兩種(Access Type):Field(在變量上)上、Property(在變量的get方法)上。一個Ojbect內的JPA注解要么在Field上要么在Property上(當然可以在類上),不能兩者同時有。詳情可參閱:https://en.wikibooks.org/wiki/Java_Persistence/Mapping#Access_Type
- Field:will be accessed directly to store and load the value from the database。It avoids any unwanted side-effect code that may occur in the application get/set methods.
- Property:
getandsetmethods will be used to store and load the value from the database. It allows the application to perform conversion of the database value when storing it in the object.
JPA 2.0開始允許通過@Acdess注解來指定默認access type並通過該注解來指定例外acess type,從而達到混合使用的效果。
注解
**@Entity**
@Entity 標注用於實體類聲明語句之前,指出該Java 類為實體類,將映射到指定的關系數據庫表。(類似的,使用@Document可以映射到mongodb)
應用了此注解后,將會自動將類名映射作為數據庫表名、將類內的字段名映射為數據庫表的列名。映射策略默認是按駝峰命名法拆分將類名或字段名拆分成多部分,然后以下划線連接,如StudentEntity -> student_entity、studentName -> student_name。若不按默認映射,則可通過@Table、@Column指定,見下面。
**@Table**
當實體類與其映射的數據庫表名不同名時需要使用 @Table 標注說明,該標注與 @Entity 標注並列使用
- schema屬性:指定數據庫名
- name屬性:指定表名,不知道時表名為類名
**@id**
@Id 標注用於聲明一個實體類的屬性映射為數據庫的一個主鍵列
@Id標注也可置於屬性的getter方法之前。以下注解也一樣可以標注於getter方法前。
若同時指定了下面的@GeneratedValue則存儲時會自動生成主鍵值,否則在存入前用戶需要手動為實體賦一個主鍵值。主鍵值類型可能是:
- Primitive types: boolean, byte, short, char, int, long, float, double.
- Equivalent wrapper classes from package java.lang:
Byte, Short, Character, Integer, Long, Float, Double. - java.math.BigInteger, java.math.BigDecimal.
- java.lang.String.
- java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp.
- Any enum type.
- Reference to an entity object.
- composite of several keys above
指定聯合主鍵,有@IdClass、@EmbeddedId兩種方法,可參閱:https://en.wikibooks.org/wiki/Java_Persistence/Identity_and_Sequencing#Composite_Primary_Keys
**@IdClass**
修飾在實體類上,指定聯合主鍵。如:@IdClass(StudentExperimentEntityPK.class),主鍵類StudentExperimentEntityPK需要滿足:
- 實現Serializable接口
- 有默認的public無參數的構造方法
- 重寫equals和hashCode方法。equals方法用於判斷兩個對象是否相同,EntityManger通過find方法來查找Entity時,是根據equals的返回值來判斷的。hashCode方法返回當前對象的哈希碼
示例:
package com.sensetime.sensestudy.common.entity; import java.io.Serializable; import java.util.Objects; import javax.persistence.Column; import javax.persistence.Id; import com.sensetime.sensestudy.common.entity.ddl.ColumnLengthConstrain; public class CustomerCourseEntityPK implements Serializable { /** * */ private static final long serialVersionUID = 1L; private String customerId; private String courseId; @Id @Column(name = "customer_id", length = ColumnLengthConstrain.LEN_ID_MAX) public String getCustomerId() { return customerId; } public void setCustomerId(String customerId) { this.customerId = customerId; } @Id @Column(name = "course_id", length = ColumnLengthConstrain.LEN_ID_MAX) public String getCourseId() { return courseId; } public void setCourseId(String courseId) { this.courseId = courseId; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CustomerCourseEntityPK that = (CustomerCourseEntityPK) o; return Objects.equals(customerId, that.customerId) && Objects.equals(courseId, that.courseId); } @Override public int hashCode() { return Objects.hash(customerId, courseId); } }
package com.sensetime.sensestudy.common.entity; import java.sql.Timestamp; import javax.persistence.Basic; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.IdClass; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; import com.sensetime.sensestudy.common.entity.ddl.ColumnLengthConstrain; @Entity @Table(name = "customer_course", catalog = "") @IdClass(CustomerCourseEntityPK.class) public class CustomerCourseEntity { private String customerId; private String courseId; private Timestamp purchaseExpireTime; private Boolean isPurchaseExpire;// 由觸發器自動更新 // TODO zmm 最大使用人數 private Integer maxNumber; private CourseEntity courseByCourseId; public CustomerCourseEntity() { } public CustomerCourseEntity(String customerId, String courseId, Timestamp purchaseExpireTime) { this.customerId = customerId; this.courseId = courseId; this.purchaseExpireTime = purchaseExpireTime; this.isPurchaseExpire = false; } public CustomerCourseEntity(String customerId, String courseId, Timestamp purchaseExpireTime, Integer maxNumber) { this.customerId = customerId; this.courseId = courseId; this.purchaseExpireTime = purchaseExpireTime; this.maxNumber = maxNumber; this.isPurchaseExpire = false; } @Id @Column(name = "customer_id", length = ColumnLengthConstrain.LEN_ID_MAX) public String getCustomerId() { return customerId; } public void setCustomerId(String customerId) { this.customerId = customerId; } @Id @Column(name = "course_id", length = ColumnLengthConstrain.LEN_ID_MAX) public String getCourseId() { return courseId; } public void setCourseId(String courseId) { this.courseId = courseId; } @Basic @Column(name = "purchase_expire_time", nullable = false) public Timestamp getPurchaseExpireTime() { return purchaseExpireTime; } public void setPurchaseExpireTime(Timestamp purchaseExpireTime) { this.purchaseExpireTime = purchaseExpireTime; } @Basic @Column(name = "is_purchase_expire", nullable = false) public Boolean getIsPurchaseExpire() { return isPurchaseExpire; } public void setIsPurchaseExpire(Boolean isPurchaseExpire) { this.isPurchaseExpire = isPurchaseExpire; } @Basic @Column(name = "max_number") public Integer getMaxNumber() { return maxNumber; } public void setMaxNumber(Integer maxNumber) { this.maxNumber = maxNumber; } @ManyToOne @JoinColumn(name = "course_id", referencedColumnName = "id", nullable = false, insertable = false, updatable = false) public CourseEntity getCourseByCourseId() { return courseByCourseId; } public void setCourseByCourseId(CourseEntity courseByCourseId) { this.courseByCourseId = courseByCourseId; } }
**@EmbeddedId**
功能與@IdClass一樣用於指定聯合主鍵。不同的在於其是修飾實體內的一個主鍵類變量,且主鍵類應該被@Embeddable修飾。
此外在主鍵類內指定的字段在實體類內可以不再指定,若再指定則需為@Column加上insertable = false, updatable = false屬性
**@GeneratedValue**
@GeneratedValue 用於標注主鍵的生成策略,通過 strategy 屬性指定。默認情況下,JPA 自動選擇一個最適合底層數據庫的主鍵生成策略:SqlServer 對應 identity,MySQL 對應 auto increment
- IDENTITY:采用數據庫 ID自增長的方式來自增主鍵字段,Oracle 不支持這種方式
- AUTO: JPA自動選擇合適的策略,是默認選項
- TABLE:通過表產生主鍵,框架借由表模擬序列產生主鍵,使用該策略可以使應用更易於數據庫移植。
- SEQUENCE:通過序列產生主鍵,通過 @SequenceGenerator 注解指定序列名,MySql 不支持這種方式
更多詳情可參閱:https://en.wikibooks.org/wiki/Java_Persistence/Identity_and_Sequencing#Sequence_Strategies
**@Basic**
表示一個簡單的屬性到數據表的字段的映射,對於沒有任何標注的 getXxx() 方法,默認為 @Basic
fetch 表示屬性的讀取策略,有 EAGER 和 LAZY 兩種,分別為立即加載和延遲加載
optional 表示該屬性是否允許為 null,默認為 true
**@Column**
此注解不是必須的,無此字段也會將字段映射到表列。當實體的屬性與其映射的數據庫表的列不同名時需要使用 @Column 標注說明,其有屬性 name、unique、nullable、length 等。
類的字段名在數據庫中對應的字段名可以通過此注解的name屬性指定,不指定則默認為將屬性名按駝峰命名法拆分並以下划線連接,如createTime對應create_time。注意:即使name的值中包含大寫字母,對應到db后也會轉成小寫,如@Column(name="create_Time")在數據庫中字段名仍為create_time。
可通過SpringBoot配置參數 spring.jpa.hibernate.naming.physical-strategy 配置上述對應策略,如指定name值是什么數據庫中就對應什么名字的列名。默認值為: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
**@Transient**
表示該屬性並非一個到數據庫表的字段的映射,ORM 框架將忽略該屬性
如果一個屬性並非數據庫表的字段映射,就務必將其標識為 @Transient,否則ORM 框架默認為其注解 @Basic,例如工具方法不需要映射
**@Temporal**
在 JavaAPI 中沒有定義 Date 類型的精度,而在數據庫中表示 Date 類型的數據類型有 Date,Time,TimeStamp 三種精度(日期,時間,兩者兼具),進行屬性映射的時候可以使用 @Temporal 注解調整精度。目前此注解只能用於修飾java.util.Date、java.util.Calendar類型的變量,TemporalType取DATE、TIME、TIMESTAMP時在MySQL中分別對應的DATE、TIME、DATETIME類型。示例:
@Temporal(TemporalType.TIMESTAMP)
@CreationTimestamp //org.hibernate.annotations.CreationTimestamp,用於在JPA執行insert操作時自動更新該字段值
@Column(name = "create_time", updatable=false )//為防止手動set,可設false以免該字段被更新 private Date createTime;
@Temporal(TemporalType.TIMESTAMP) @UpdateTimestamp //org.hibernate.annotations.UpdateTimestamp,用於在JPA執行update操作時自動更新該字段值 @Column(name = "update_time") private Date updateTime;
@CreationTimestamp、@UpdateTimestamp是Hibernate的注解,SpringData JPA也提供了類似功能(推薦用此):@CreatedDate、@LastModifiedDate、@CreatedBy、@LastModifiedBy,可參閱https://blog.csdn.net/tianyaleixiaowu/article/details/77931903
**@MappedSuperClass**
用來修飾一個類,類中聲明了各Entity共有的字段,也即數據庫中多表中共有的字段,如create_time、update_time、id等。
標注為@MappedSuperclass的類將不是一個完整的實體類,他將不會映射到數據庫表,但是他的屬性都將映射到其子類的數據庫字段中。
標注為@MappedSuperclass的類不能再標注@Entity或@Table注解,也無需實現序列化接口。
允許多級繼承。
**@Inheritance**
用於表結構復用。指定被該注解修飾的類被子類繼承后子類和父類的表結構的關系。通過strategy屬性指定關系,有三種策略:
- SINGLE_TABLE:適用於共同字段多獨有字段少的關聯關系定義。子類和父類對應同一個表且所有字段在一個表中,還會自動生成(也可通過@DiscriminatorColumn指定)一個字段 varchar 'dtype' 用來表示一條數據是屬於哪個實體的。為默認值(未使用@Inheritance或使用了但沒指定strategy屬性時默認采用此策略)。
- JOINED:子類和父類對應不同表,父類屬性對應的列(除了主鍵)不會且無法再出現在子表中。子表自動產生與父表主鍵對應的外鍵與父表關聯。同樣地也可通過@DiscriminatorColumn為父類指定一個字段用於標識一條記錄屬於哪個子類。
- TABLE_PER_CLASS:子類和父類對應不同表且各類自己的所有字段(包括繼承的)分別都出現在各自的表中;表間沒有任何外鍵關聯。此策略最終效果與@MappedSuperClass等同。
更多詳情可參閱:https://www.ibm.com/developerworks/cn/java/j-lo-hibernatejpa/index.html
@Inheritance與@MappedSuperclass的區別:后者子類與父類沒有外鍵關系、后者不會對應一個表等、前者適用於表關聯后者適用於定義公共字段。另:兩者是可以混合使用的。詳見:https://stackoverflow.com/questions/9667703/jpa-implementing-model-hierarchy-mappedsuperclass-vs-inheritance。
總而言之,@Inheritance、@MappedSuperClass可用於定義Inheritance關系。詳情可參閱:https://en.wikibooks.org/wiki/Java_Persistence/Inheritance。這些方式的一個缺點是子類中無法覆蓋從父類繼承的字段的定義(如父類中name是not null的但子類中允許為null)。
除了 @Inheritance、@MappedSuperClass外,還有一種Inheritance方法(此法可解決上述不足):先定義一個Java POJO(干凈的POJO,沒有任何對該類使用任何的ORM注解),然后不同子類繼承該父類並分別在不同子類中進行ORM定義即可。此法下不同子類擁有父類的公共字段且該字段在不同子類中對應的數據庫列定義可不同。
實踐示例:
翻譯表與主表關聯方案設計
多語言表(翻譯表)與原表(主表)關聯方案設計,需求:字段(列)復用以免重復代碼定義、同一個列的定義如是否為空在不同表中可不一樣(如有些字段主表中非空但翻譯表中可空),有如下方案:
無關聯,重復定義。pass
有關聯
通過@MappeSuperclass,不同子類可以完全繼承父類列定義且分別對應不同表,表結構完全相同,但不能覆蓋父類的定義。pass
通過@Inheritance,三種策略:
SINGLE_TABLE:父、子類對應同一張表。子類無法覆蓋父類的字段定義;源課程和翻譯課程id一樣,違背主鍵唯一約束。pass
JOINED:父、子類對應不同表且子類自動加與父類主鍵一樣的字段與父類主鍵關聯,但父表中除主鍵之外的所有字段無法在子表中再出現。pass
TABLE_PER_CLASS:父、子類對應不同表且表定義完全相同,無外鍵,但子類無法覆蓋父類的字段定義(即同一字段在不同表中字段定義無法不同)。pass
定義個普通父類,子類繼承父類並分別進行@Column定義:不同子類對應不同表,不同表含有的字段及定義可不一樣。selected
JPA對象屬性與數據庫列的映射
(attribute map between object model and data model)
基本類型(String、Integer等)、時間、枚舉、復雜對象如何自動映射到數據庫列,詳情可參閱:https://en.wikibooks.org/wiki/Java_Persistence/Basic_Attributes
以下是基本類型的映射:

對於非基本類型的屬性,其映射:
法1:
By default in JPA any
Serializableattribute that is not a relationship or a basic type (String, Number, temporal, primitive), will be serialized to aBLOBfield.
法2:JPA 2.1起可通過 @Convert 指定屬性與數據庫列間的映射邏輯,其可將任意對象映射到數據庫的一個列(詳見后文)。在這之前沒有@Convert,可以通過get、set方法實現類似效果,示例:
@Entity public class Employee { ... private boolean isActive; ... @Transient public boolean getIsActive() { return isActive; } public void setIsActive(boolean isActive) { this.isActive = isActive; } @Basic private String getIsActiveValue() { if (isActive) { return "T"; } else { return "F"; } } private void setIsActiveValue(String isActive) { this.isActive = "T".equals(isActive); } }
Spring Data JPA使用小記
指定對象與數據庫字段映射時注解的位置
如@Id、@Column等注解指定Entity的字段與數據庫字段對應關系時,注解的位置可以在Field(屬性)或Property(屬性的get方法上),兩者統一用其中一種,不能兩者均有。推薦用前者。
詳情可參閱:https://en.wikibooks.org/wiki/Java_Persistence/Mapping
JPA命名查詢的原理
基本用法:通過方法名來指定查詢邏輯,而不需要自己實現查詢的SQL邏輯,示例:List<Student> getByName(String name)
方法名解析原理:對方法名中除了保留字(findBy、top、within等)外的部分以and為分隔符提取出條件單詞,然后解析條件獲取各個單詞並看是否和Entity中的屬性對應(不區分大小寫進行比較)。get/find 與 by之間的會被忽略,所以getNameById與getById是等價的,會根據id查出整個Entity而不會只查name字段。(指定部分字段的查詢見后面條目)
查詢條件解析原理:假設School和Student是一對多關系,Student中有個所屬的School school字段、School有個String addressCode屬性,以如下查詢為例:
Studetn getByNameAndSchoolAddressCode(String studentName, String addressCode)(先說結果:JPA會自動生成條件studentName和關聯條件student.school.addressCode進行查詢)
- 由And分割得到studentName、SchoolAddressCode;
- 分別看Student中是否有上述兩屬性,顯然前者有后者沒有,則后者需要進一步解析(見下步)
- JPA按駝峰命名格式從后往前嘗試分解SchoolAddressCode:先得到 [SchoolAdress、Code],由於Student沒有SchoolAddress屬性故繼續嘗試分解,得到[School、AdressCode];由於Student有School屬性且School有addressCode屬性故滿足,最終得到條件student.school.addressCode。注:但若Student中有個SchoolAdress schoolAddress屬性但schoolAddress中沒有code屬性,則會因找不到student.schoolAdress.code而報錯,所以可通過下划線顯示指定分割關系,即寫成: getByNameAndSchool_AddressCode
查詢字段解析原理:默認會查出Entity的所有字段且返回類型為該Entity類型,有兩種情況可查詢部分字段(除此外都會查出所有字段):
1、通過@Query寫的自定義查詢邏輯中只查部分字段。這種不屬於直接通過方法名指定查詢,這里先不討論(見后面查詢指定部分字段的條目)。
2:返回類型為自定義接口或該接口列表,接口中僅包含部分字段的get方法,此時會根據接口方法名查詢部分字段。示例:
//CourseRepository.java
List<MyCustomColumns> findCustomColumnsByGroupId(String groupId);//find和By間的部分在解析時會被忽略。為了見名知意,最好加上字段信息,如findVersionByGroupId public interface MyCustomColumns {//JPA生成查詢語句時只會查下面get方法中指定的字段名。需要確保Entity中有該字段名否則會報錯 public String getId(); public String getVersion(); public String getGroupId(); }
在查詢時,通常需要同時根據多個屬性進行查詢,且查詢的條件也格式各樣(大於某個值、在某個范圍等等),Spring Data JPA 為此提供了一些表達條件查詢的關鍵字,大致如下:
And --- 等價於 SQL 中的 and 關鍵字,比如 findByUsernameAndPassword(String user, Striang pwd); Or --- 等價於 SQL 中的 or 關鍵字,比如 findByUsernameOrAddress(String user, String addr); Between --- 等價於 SQL 中的 between 關鍵字,比如 findBySalaryBetween(int max, int min); LessThan --- 等價於 SQL 中的 "<",比如 findBySalaryLessThan(int max); GreaterThan --- 等價於 SQL 中的">",比如 findBySalaryGreaterThan(int min); IsNull --- 等價於 SQL 中的 "is null",比如 findByUsernameIsNull(); IsNotNull --- 等價於 SQL 中的 "is not null",比如 findByUsernameIsNotNull(); NotNull --- 與 IsNotNull 等價; Like --- 等價於 SQL 中的 "like",比如 findByUsernameLike(String user); NotLike --- 等價於 SQL 中的 "not like",比如 findByUsernameNotLike(String user); OrderBy --- 等價於 SQL 中的 "order by",比如 findByUsernameOrderBySalaryAsc(String user); Not --- 等價於 SQL 中的 "! =",比如 findByUsernameNot(String user); In --- 等價於 SQL 中的 "in",比如 findByUsernameIn(Collection<String> userList) ,方法的參數可以是 Collection 類型,也可以是數組或者不定長參數; NotIn --- 等價於 SQL 中的 "not in",比如 findByUsernameNotIn(Collection<String> userList) ,方法的參數可以是 Collection 類型,也可以是數組或者不定長參數;
Containing --- 包含指定字符串
StargingWith --- 以指定字符串開頭
EndingWith --- 以指定字符串結尾
JPA 集合類型查詢參數
List<StudentEntity> getByIdInAndSchoolId(Collection<String> studentIdList, String schoolId); ,關鍵在於 In 關鍵字。參數用Collection類型,當然也可以用List、Set等,但用Collection更通用,因為此時實際調用可以傳List、Set等實參。
nativeQuery
Repository盡可能避免使用nativeQuery,使得與數據庫字段的耦合限制在Entity內而不擴散到Repository內,更易於維護
盡可能避免在JPQL、nativeQuery中進行聯表查詢,而是在Service層通過JPA Specification進行動態關聯查詢
Repository nativeQuery返回Entity
使用nativeQuery時SQL語句查詢的字段名若沒as則是數據庫中的字段名,如school_id,而API返回值通常是schoolId,可以在SQL里通過 school_id as schoolId取別名返回。然而若查詢很多個字段值則得一個個通過as取別名,很麻煩,可以直接將返回值指定為數據庫表對應的Entity,不過此法要求查詢的是所有字段名,如:
@Query(value = " select t.* from teacher t where t.school_id=?1 "// 以下為搜索字段 + "and (?4 is NULL or name like %?4% or job_number like %?4% or bz like %?4% or phone like %?4% or email like %?4%) " + " order by job_number limit ?2, x?3 ", nativeQuery = true) List<TeacherEntity> myGetBySchoolIdOrderByJobNumber(String schoolId, int startIndex, Integer size, String searchNameOrJobnumOrBzOrPhoneOrEmai);// nativeQuery返回類型可以聲明為Entity,會自動進行匹配,要求查回與Entitydb中字段對應的所有db中的字段
延遲加載與立即加載(FetchType)
通常可以在@OneToMany中用LAZY、在@ManyToOne/Many中用EAGER,但不絕對,看具體需要。
FetchType.LAZY:延遲加載,在查詢實體A時,不查詢出關聯實體B,在調用getxxx方法時,才加載關聯實體,但是注意,查詢實體A時和getxxx必須在同一個Transaction中,不然會報錯:no session。即會表現為兩次單獨的SQL查詢(非聯表查詢)
FetchType.EAGER:立即加載,在查詢實體A時,也查詢出關聯的實體B。即會表現為一次查詢且是聯表查詢
默認情況下,@OneToOne、@ManyToOne是LAZY,@OneToMany、@ManyToMany是EAGER。
有兩個地方用到延遲加載:relationship(@OneToMany等)、attribute(@Basic)。后者一般少用,除非非常確定字段很少訪問到。
JPA Join查詢
只有有在Entity內定義的關聯實體才能進行關聯查詢,示例:
@Query("select cd, d from CourseDeveloperEntity cd join cd.developer d where d.nickName='stdeveloper'")
該句實際上等價於:
@Query("select cd, cd.developer from CourseDeveloperEntity cd where cd.developer.nickName='stdeveloper'")
若將一個對象的關聯對象指定為延遲加載LAZY,則每次通過該對象訪問關聯對象時(如courseDeveloper.developer)都會執行一次SQL來查出被關聯對象,顯然如果被關聯對象訪問頻繁則此時性能差。解決:法1是改為EAGER加載;法2是使用join fetch查詢,其會立即查出被關聯對象。示例:
@Query("select cd from CourseDeveloperEntity cd join fetch cd.developer where cd.id='80f78778-7b39-499c-a1b6-a906438452a9'")
Join Fetch背后是使用inner join,可以顯示指定用其他關聯方式如 left join fetch
Join Fetch的缺點之一在於有可能導致“Duplicate Data and Huge Joins”,如多個實驗關聯同一課程,則查詢兩個實驗時都關聯查出所屬課程,后者重復查詢。
更多(join、join fetch等)可參閱:https://en.wikibooks.org/wiki/Java_Persistence/Querying#Joining,_querying_on_a_OneToMany_relationship
JPA分頁或排序
靜態方式:直接在方法體現(如 getByNameOrderById),也可以在JPQL的@Query的邏輯中加上order by(此時字段名是Entity中的字段名)
動態方式:可以在Repository的方法的最后加一個Sort 或者 Pageable 類型的參數,便可動態生成排序或分頁語句(編譯后會自動在語句后加order by或limit語句)。
Repository中的一個方法myGetByCourseIdAndStudentId:
@Query("select se from StudentExperimentEntity se where se.studentId= ?2 and se.experimentId in ( select e.id from ExperimentEntity e where e.courseId= ?1 ) ")
List<StudentExperimentEntity> myGetByCourseIdAndStudentId(String courseId, String studentId, Pageable pageable);//沒有寫上述@Query語句也可以加Pageable。雖然實際傳值時傳PageRequest對象,但若這里生命為PageRequest則不會分頁,總是返回所有數據,why?
調用:
studentExperimentRepository.myGetByCourseIdAndStudentId(courseId, studentId, PageRequest.of(0, count, new Sort(Sort.Direction.DESC, "lastopertime")));
編譯后會在myGetByCourseIdAndStudentId所寫SQL后自動加上 order by studentexp0_.lastopertime desc limit ?
注:上述用法也支持nativeQuery,示例:
@Query(value = "select d.*, u.username from developer d inner join user u on d.id=u.id " + " where (?1 is null or d.nick_name like %?1% ) ", nativeQuery = true) List<DeveloperEntity> myGetByNicknameOrPhoneOrEmailOrBz(String searchNicknameOrPhoneOrEmailOrBz, Pageable pageable);
如果要同時返回分頁對象,則可用Page<XX>返回類型,如: Page<DeveloperEntity> myGetByNicknameOrPhoneOrEmailOrBz(String searchNicknameOrPhoneOrEmailOrBz, Pageable pageable);
需要注意的是,只有元素是Entity類型時才支持直接將返回值聲明為Page對象,否則會出現Convert Exception。
Repository中更新或創建並返回該Entity
如 UserEntity u=userRepository.save(userEntity) ,其中UserEntity包含成員變量private SchoolEntity schoolEntity。Repository的save方法會返回被save的entity,但若是第一次保存該entity(即新建一條記錄)時u.schoolEntity的值會為null,解決:用saveAndFlush
查詢Entity中的部分字段
若不需要聯表查詢則用下面的2中的方法,若需要聯表查詢則用1.3中的方法。
1、通過@Query注解
對於只返回一個字段的查詢:
@Query(value = "select languageType from CourseTranslationEntity where courseId=?1") Set<Locale> myGetLanguageTypeByCourseId(String courseId);
對於返回多個字段的查詢:
1.1、對於nativeQuery,直接select部分字段即可,結果默認會自動包裝為Map。為了便於理解可以直接將結果聲明為Map。示例:
@Query(value = "select g.id, g.school_id as schoolId, g.name, g.createtime, g.bz, count(s.id) as stuCount from grade g left join student s "
+ " on g.name=s.grade where g.school_id=(select a.school_id from admin a where a.id=?1)" // 以下為搜索條件
+ " and (?4 is null or g.name like %?4% or g.bz like %?4% ) "
+ " group by g.id limit ?2,?3", nativeQuery = true)
List<Map<String, Object>> myGetGradeList(String adminId, Integer page, Integer size,
String searchGradeNameOrGradeBz);
其可以達到目的,但缺點是sql里用的直接是數據庫字段名,導致耦合大,數據庫字段名一變,所有相關sql都得相應改變。
1.2、對於非nativeQuery:(sql里的字段名是entity的字段名,數據庫字段名改動只要改變entity中對應屬性的column name即可,解決上述耦合大的問題)
當Repository返回類型為XXEntity或List<XXEntity>時通常默認包含所有字段,若要去掉某些字段,可以去掉XXEntity中該字段的get方法。此法本質上還是查出來了只是spring在返回給調用者時去掉了。治標不治本。
也可以自定義一個bean,然后在Repository的sql中new該bean。此很死板,要求new時寫bean的全限定名,比較麻煩。
更好的辦法是與nativeQuery時類似直接在sql里select部分字段,不過非nativeQuery默認會將結果包裝為List而不是Map,故不同的是:這里需要在sql里new map,此'map'非jdk里'Map';需要為字段名取別名,否則返回的Map里key為數值0、1、2... 。示例:
//為'map'不是'Map' @Query("select new map(g.name as name, count(s.id) as stuCount) from GradeEntity g, StudentEntity s where g.name=s.grade and g.schoolId=?1 group by g.id") List<Map<String, Object>> myGetBySchoolId(String schoolId);
需要注意的是,由於聲明為Map時並不知道數據的返回類型是什么,故默認會用最大的類型(如對於數據庫中的整型列,查出時Map中該字段的類型為BigInteger),除非不用Map而是指明了字段類型,見下面的1.3。
1.3、不管是否是nativeQuery,方法簽名中返回值指定為Map不太好(指定為Map時,實際類型是 org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap ,該類型只能讀不能改或寫)。
更好的是用一個類:我們可以自定義一個接口,然后將返回類型聲明為接口列表即可。接口get方法名中的字段與上面as后的字段名對應(當然,也可以不用接口而是用自定義包含部分字段的類,此時new時需要用類的全限定名)。此法不管是否nativeQuery均有效。示例:
1 //1 nativeQuery 2 @Query(value = "select g.id, g.school_id as schoolId, g.name, g.createtime, g.bz, count(s.id) as stuCount from grade g left join student s " 3 + " on g.name=s.grade where g.school_id=(select a.school_id from admin a where a.id=?1)" // 以下為搜索條件 4 + " and (?4 is null or g.name like %?4% or g.bz like %?4% ) " 5 + " group by g.id limit ?2,?3", nativeQuery = true) 6 List<IAdminInfo> myGetGradeList(String adminId, Integer page, Integer size, 7 String searchGradeNameOrGradeBz); 8 public interface IAdminInfo{ 9 public String getId(); 10 public String getSchoolId(); 11 ... 12 } 13 14 15 16 17 //2 nativeQuery 18 // JPQL查詢部分字段時,默認返回List類型,可通過new map指定返回map,此時map key默認為順序0 1 2 等,可通過as指定key名 19 @Query("select new map(id as idx, languageType as languageType) from CourseEntity where id in ?1 ") 20 List<IdAndLanguageType> myGetLanguagesTypeByCourseIdIn(Collection<String> courseIdCollection); 21 22 // 以下為為了查詢Entity部分字段而定義的返回類型 23 public interface IdAndLanguageType { 24 public String getIdx(); //get方法中的字段名須與上面通過as取的key別名對應 25 26 public String getLanguageType(); 27 }
@Query用於查詢時返回值總結 :
若是查詢多個字段則返回時默認將這些字段包裝為Object[]、若返回有多條記錄則包裝成List<Object[]>,若只查詢一個字段則不用數組而是直接用該字段。示例:

select中僅有一個字段時(可以是表中的一個列名、Entity中的一個字段名、或一個Entity對象名,如language_type、languageType、courseEntity),方法返回類型須聲明為該字段的類型或類型列表(如String、CourseEntity、List<String>、List<CourseEntity>)
select中有至少兩個字段時,默認會將每條記錄的幾個字段結果包裝為數組。可以手動指定包裝為map,此時map的key為字段序號,故最通過as指定key為字段名。可參閱:https://en.wikibooks.org/wiki/Java_Persistence/Querying#Query_Results
2、不通過@Query注解,直接通過方法名簽名指定部分字段查詢
實際上,在前面介紹JPA查詢時介紹了直接通過方法簽名實現只查詢部分字段的方法。上面的查詢不用@Query注解的等價寫法如下:
List<IdAndLanguageType> getLanguagesTypeByCourseIdIn(Collection<String> courseIdCollection);
其內部原理是根據接口的get方法解析出要查詢的字段,可以理解為JPA內部將之轉成了用@Query注解的查詢,內部生成的大概過程如下:
獲得要查詢的字段名:idx、languageType
生成@Query查詢: @Query(" select new map(idx as idx, languageType as languageType) from CourseEntity where id in ?1 ") 。注意這里第一個字段名為idx而不是id,因為是根據接口方法產生的,可見:如果使用者不用@Query則需要確保接口中get方法名中的字段與Entity中的一致,而如果使用@Query則不需要,因為可以自己通過as取別名
上述1.3和2通過聲明帶get方法的接口來接收JPA Repository查詢返回的部分字段,這其實就是投影(Projection)操作。可將接口換為POJO?實踐發現不可以,只能為接口。
JPA的update、delete
(需要加@Transactional、@Modefying)
@Transactional //也可以放在service方法上
@Modifying @Query("delete from EngineerServices es where es.engineerId = ?1")//update與此類似 int deleteByEgId(String engineerId);
對於delete操作,可以與query的寫法類似,直接通過方法名聲明(注:update不支持這樣寫):
@Transactional @Modifying int deleteByEgId(String engineerId);
甚至更直接寫為: int deleteByEgId(String engineerId); ,但此時記得需要在上層調用者部分添加@Transactional
注:JPA中非nativeQuery的刪除操作(如deleteByName)內部實際上是先分析出方法名中的條件、接着按該條件查詢出Entity,然后根據這些Entity的id執行SQL刪除操作。
也正因為這樣,軟刪除功能中指定 @SQLDelete("update student set is_delete='Y' where id=? ") 即可對所有非nativeQuery起作用。
JPA的update操作:
法1:Repository中@Modifying、@Query組合
法2:通過Repository的save方法。
方式1:JPA會判根據Entity的主鍵判斷該執行insert還是update,若沒指定主鍵或數據庫中不存在該主鍵的記錄則執行update。此法在通過在Entity指定@Where實現了軟刪除的情況下行不通,因為JPA通過內部執行查詢操作判斷是否是update時查詢操作也被加上了@Where,從而查不到數據而被,進而最終執行insert,此時顯然報主鍵沖突。
方式2:更好的做法是先通過Repository查出來,修改后再執行save,這樣能確保為update操作
可參閱:https://stackoverflow.com/questions/11881479/how-do-i-update-an-entity-using-spring-data-jpa
JPA的count
Integer countByName(String name);
外鍵關聯
相關注解:@ManyToOne/@OneToMany/@OneToOne 、 @JoinColumn/@PrimaryKeyJoinColumn、@MapsId,用法及區別見:https://www.cnblogs.com/chiangchou/p/mappedBy.html
- (1)@JoinColumn用來指定外鍵,其name屬性指定該注解所在Entity對應的表的一個列名
- (2)@ManyToOne等用來指定對應關系是多對一等數量對應關系
通過(2)指定數量對應關系時,須在多的一方標注(@ManyToOne),一的一方注不注均可。(以下以School、Student為例,為一對多關系)
- 若只用(2)沒用(1)則在生成表時會自動生成一張關聯表來關聯School、Student,表中包含School、Studeng的id
- 若在用了(2)的基礎上用了(1)則不會自動生成第三張表,而是會在多的一方生成一個外鍵列。列名默認為 ${被引用的表名}_id (可以通過@JoinColumn的name屬性指定列名),引用了目標表的id。
- 上法的缺點是在insert多的一方后會再執行一次update操作來設置外鍵的值(即使在insert時已經指定了),避免額外update的方法:在一的一方不使用@JoinColumn,而是改為指定@OneToMany的mappedBy屬性。(1)和(2)的mappedBy屬性不能同時存在,會報錯。
示例:
進行如下設置后,JPA會自動生成為student表生成兩個外鍵約束:student表school_id關聯school表id自動、student表id字段關聯user表id字段。
//StudentEntity //get set ... @Column(name = "id") private String sId; @Column(name = "school_id") private String schoolId; @ManyToOne @JoinColumn(name = "school_id", referencedColumnName = "id", insertable = false, updatable = false)//school.school_id字段外鍵關聯到school.id字段;多個字段對應數據庫同一字段時會報錯,通過添加insertable = false, updatable = false即可 private SchoolEntity schoolBySchoolId; @OneToOne @JoinColumn(name = "id", referencedColumnName = "id", insertable = false, updatable = false) //student.id字段外鍵關聯到user.id字段。也可用@PrimaryKeyJoinColumn @MapsId(value = "id") private UserEntity userByUserId;
對於外鍵屬性(如上面student表的school_id),當該屬性不是當前表的主鍵時,通過 @OneToOne/@ManyToOne + @JoinColumn 定義即可成功地在數據庫中自動生成產生外鍵約束。但當該屬性也是當前表的主鍵時(如為student.id定義外鍵來依賴user.id字段),單靠@OneToOne + @JoinColumn並不能自動產生外鍵約束,此時可通過加@MapIds來解決。
總結:
通過@ManyToOne/@OneToMany/@OneToOne + @JoinColumn/@PrimaryKeyJoinColumn定義外鍵,是否需要@MapsId視情況而定。
外鍵場景有兩種:
外鍵屬性不是當前表的主鍵(如上面student表的school_id字段不是主鍵)
外鍵屬性也是當前表的屬性(如上面student表的id字段是主鍵)
基於這兩種場景,各注解使用時的組合及效果如下:

說明:
使用注解組合后是否會自動為表生成外鍵約束?打鈎的表示會、打叉的表示不會、辦勾辦叉的表示會但是生成的不是預期的(如場景1中期望school_id關聯了school id自動,但一種結果是id關聯了user id、另一種是自動產生了school_by_school_id字段並關聯到了school id,顯然都不符合期望)。
結論:
1、外鍵屬性不是主鍵的場景(第一種),用 @OneToOne/@ManyToOne + @JoinColumn 即可,為了簡潔推薦不用@MapIds,示例見上面的school_id關聯school id設置。
2、外鍵屬性是主鍵的場景(第二種),用 @OneToOne + @JoinColumn + @MapsId,示例見上面的student id關聯user id設置。
雖從表中可見場景二有三種組合都可以達到目標,但為了符合業務語義(主鍵嘛,當然是唯一的,因此是一對一)且為了和場景一的盡可能統一,我們采用這個的組合。
實踐發現,使用@MapsId時,要求外鍵字段、被關聯的字段 的數據庫列名得相同且都得為"id"。why?如何避免?TODO
級聯操作(CASCADE)
Use of the cascade annotation element may be used to propagate the effect of an operation to associated entities. The cascade functionality is most typically used in parent-child relationships.
用於有依賴關系的實體間(@OneToMany、@ManyToOne、@OneToOne等)的級聯操作:當對一個實體進行某種操作時,若該實體加了與該操作相關的級聯標記,則該操作會傳播到與該實體關聯的實體(即對被級聯標記的實體施加某種與級聯標記對應的操作時,與該實體相關聯的其他實體也會被施加該操作)。包括:
CascadeType.PERSIST:持久化,即保存
CascadeType.REMOVE:刪除當前實體時,關聯實體也將被刪除
CascadeType.MERGE:更新或查詢
CascadeType.REFRESH:級聯刷新,即在保存前先更新別人的修改:如Order、Item被用戶A、B同時讀出做修改且B的先保存了,在A保存時會先更新Order、Item的信息再保存。
CascadeType.DETACH:級聯脫離,如果你要刪除一個實體,但是它有外鍵無法刪除,你就需要這個級聯權限了。它會撤銷所有相關的外鍵關聯。
CascadeType.ALL:上述所有
注:
級聯應該標記在One的一方。如對於 @OneToMany的Person 和 @ManyToOne的Phone,若將CascadeType.REMOVE標記在Phone則刪除Phone也會刪除Person,顯然是錯的。
慎用CascadeType.ALL,應該根據業務需求選擇所需的級聯關系,否則可能釀成大禍。
示例可參閱:http://westerly-lzh.github.io/cn/2014/12/JPA-CascadeType-Explaining/
like查詢
對於單字段的可以直接在方法名加Containing
@Query("select s from SchoolEntity s where s.customerId=?1 "// 以下為搜索條件
+ " and (?2 is null or s.name like %?2% or s.bz like %?2% ) ")
List<SchoolEntity> getByCustomerId(String customerId, String searchSchoolnameOrBz, Pageable pageable);
Entity中將任意對象映射為一個數據庫字段
借助JPA converter to map your Entity to the database.
在要被映射的字段上加上注解: @Convert(converter = JpaConverterJson.class)
實現JpaConverterJson:
public class JpaConverterJson implements AttributeConverter<Object, String> {//or specialize the Object as your Column type private final static ObjectMapper objectMapper = new ObjectMapper(); @Override public String convertToDatabaseColumn(Object meta) { try { return objectMapper.writeValueAsString(meta); } catch (JsonProcessingException ex) { return null; // or throw an error } } @Override public Object convertToEntityAttribute(String dbData) { try { return objectMapper.readValue(dbData, Object.class); } catch (IOException ex) { // logger.error("Unexpected IOEx decoding json from database: " + dbData); return null; } } }
需要注意的是,若Entity字段是一個 JavaBean 或 JavaBean 列表(如 TimeSlice 或 List<TimeSlice> ),則反序列化時相應地會反序列化成 LinkedHashMap 或 List<LinkedHashMap>,故強轉成TimeSlice或List<TimeSlice>雖然編譯期不會報錯但運行時就出現類型轉換錯誤。故需要進一步轉換成JavaBean,示例:
1 public static class TimeTableConverter implements AttributeConverter<List<TimeSlice>, String> {// or specialize the Object as your Column type 2 3 private final static ObjectMapper objectMapper = new ObjectMapper(); 4 5 @Override 6 public String convertToDatabaseColumn(List<TimeSlice> data) { 7 try { 8 return objectMapper.writeValueAsString(data); 9 } catch (JsonProcessingException ex) { 10 throw new ApiCustomException(ApiErrorCode.OTHER, "fail to convert list to string"); 11 } 12 } 13 14 @Override 15 public List<TimeSlice> convertToEntityAttribute(String dbData) { 16 // return objectMapper.readValue(dbData, List.class);//直接return會報ClassCastException 17 18 Field[] fields = TimeSlice.class.getDeclaredFields(); 19 20 try { 21 List<Map<String, Object>> tmpMapList = objectMapper.readValue(dbData, List.class);// 對於鍵值對類型元素,默認反序列化成LinkedListMap類型,故需進一步轉換成TimeSlice 22 List<TimeSlice> timeSliceList = null; 23 if (null != tmpMapList) { 24 timeSliceList = new ArrayList<>(); 25 for (Map<String, Object> map : tmpMapList) { 26 TimeSlice tmpTimeSlice = new TimeSlice(); 27 timeSliceList.add(tmpTimeSlice); 28 for (Field field : fields) {// 復制出所有屬性 29 try { 30 field.setAccessible(true); 31 field.set(tmpTimeSlice, map.get(field.getName())); 32 } catch (IllegalArgumentException | IllegalAccessException e) { 33 e.printStackTrace(); 34 } 35 } 36 } 37 } 38 return timeSliceList; 39 } catch (IOException ex) { 40 throw new ApiCustomException(ApiErrorCode.OTHER, "fail to convert string to list"); 41 } 42 } 43 }
參考資料:https://stackoverflow.com/questions/25738569/jpa-map-json-column-to-java-object
將任意非基本數據類型(如java bean、list等)對應到數據庫字段
本質上就是將數據序列化成基本數據類型如String。如要把List<String> gradeIdList對應到數據庫中的字符串類型的courseSchedule字段。
法1:可以在業務層寫代碼將gradeIdList序列化成String: String res=objectMapper.writeValueAsString(gradeIdList);// 借助objectMapper.writeValueAsString(data); ,之后保存即可。從數據庫中讀取時: List<String> gradeIdList=objectMapper.readValue(dbData, List<String>.class); 。此法可以解決問題,但每個字段都得自己手動寫此過程。
法2:實現一個AttributeConverter,並應用於Entity字段。此法相當於指定了AttributeConverter后讓框架去自動做轉換
@Column(name = "course_schedule") @Convert(converter = MyJpaConverterJson.class) private List<String> courseSchedule; public class MyJpaConverterJson implements AttributeConverter<List<String>, String> {// or specialize the Object as your // Column type private final static ObjectMapper objectMapper = new ObjectMapper(); @Override public String convertToDatabaseColumn(List<String> data) { try { return objectMapper.writeValueAsString(data); } catch (JsonProcessingException ex) { throw new ApiCustomException(ApiErrorCode.OTHER, "fail to convert list to string"); } } @Override public List<String> convertToEntityAttribute(String dbData) { try { return objectMapper.readValue(dbData, List.class); } catch (IOException ex) { throw new ApiCustomException(ApiErrorCode.OTHER, "fail to convert string to list"); } } }
枚舉示例
@Column(name = "sex") @Enumerated(EnumType.ORDINAL)//持久化為0,1 private Sex sex; @Column(name = "type") @Enumerated(EnumType.STRING)//持久化為字符串 private Role role;
示例:
@Override public Page<CourseDeveloperEntity> listJoinedDevelopersOfGivenCourse(Integer page, Integer size, String courseId, String optionalExcludedDeveloperId, String optionalEmailOrNickName) { PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Order.desc("developer.nickName"), Sort.Order.asc("role"))); return courseDeveloperRepository.findAll((Specification<CourseDeveloperEntity>) (root, query, criteriaBuilder) -> { List<Predicate> predicateList = new ArrayList<Predicate>(); predicateList.add(criteriaBuilder.equal(root.get("course").get("id"), courseId)); if (null != optionalExcludedDeveloperId) {// 排除指定開發者 predicateList.add(criteriaBuilder.notEqual(root.get("developer").get("id"), optionalExcludedDeveloperId)); } if (null != optionalEmailOrNickName && optionalEmailOrNickName.trim().length() > 0) { // Predicate p1 = criteriaBuilder.equal(root.get("developer").get("nickName"), optionalEmailOrNickName); // Predicate p2 = criteriaBuilder.equal(root.get("developer").get("email"), optionalEmailOrNickName); Predicate p1 = criteriaBuilder.like(root.get("developer").get("nickName"), "%" + optionalEmailOrNickName + "%"); Predicate p2 = criteriaBuilder.like(root.get("developer").get("email"), "%" + optionalEmailOrNickName + "%"); predicateList.add(criteriaBuilder.or(p1, p2)); } return criteriaBuilder.and(predicateList.toArray(new Predicate[predicateList.size()])); }, pageRequest); }
In查詢
不管是否是nativeQuery都可以用in查詢,如:
@Query( "select * from student where id in ?1", nativeQuery=true) //@Query( "select s from StudentEntity s where s.id in ?1")
List<StudentEntity> myGetByIdIn(Collection<String> studentIds );//復雜查詢,自定義查詢邏輯
List<StudentEntity> getByIdIn( Collection<String> studentIds );//簡單查詢,聲明語句即可
不管是否自己寫查詢語句、不管是否是nativeQuery,都要求調用該方法時所傳的id列表必須至少有一個元素,否則執行時會報錯。
原因:運行時動態生成sql語句,如果id列表為null或空列表,則最終生成的sql語句中"where id in null"不符合sql語法。
有時候有特殊需求:
問題描述:如需要有兩個方法,他們的查詢邏輯及返回一樣,只不過一個方法帶Collection參數而另一個不帶,此時在Repository中兩個方法的查詢語句會重復寫,這給維護帶來了麻煩。示例:
@Query("xxx1")
List<StudentEntity> myGetByIdAndNameIn(String id, Collection<String> names )//調用者須確保names至少有一個元素
@Query("xxx2")
List<StudentEntity> myGetById(String id)
在這種情況下,如何將兩個方法合並為一個?
解決:將Collection參數視為非必須參數,為null時當成不將該參數作為查詢條件
示例:
@Query( "select * from student where id=?1 and (coalesce(?2) is null or name in (?2))", nativeQuery=true) //@Query( "select s from StudentEntity s where id=?1 and (coalesce(?2) is null or s.name in (?2))") List<StudentEntity> myGetByIdAndNameIn(String id, Collection<String> optionalNames );// 調用者可通過myGetByIdAndNameIn(id, null)來達到myGetById(id)的目的
這樣Repository中只有myGetByIdAndNameIn方法,調用者可通過 myGetByIdAndNameIn(id, null) 來達到 myGetById(id) 的目的。
當然,此法要求要么傳null、要么須至少有一個元素,而不能為空列表。
進階:
使用 (?2) is null or name in (?2) 可否?實踐證明不行,因為在optionalNames有多個元素時最終轉換成類似"(xx1, xx2) is null" 的sql,這時報?2 is null 會報sql Operand should contain 1 column(s) 的錯。
另外,如果傳了optionalNames(即非null)則要求至少有一個元素,否則轉成sql后"name in ()"也會報錯。
JPA Repository的save(xxx)方法
通過show-sql=true參數打印sql語句,可以發現其內部先是按被保存的Entity的主鍵查出該Entity,若存在則更新並保存、否則插入。
save方法以Entity為參數用於保存。其效果可能是插入新數據或修改已有數據,在執行時會根據Entity參數自動判斷:如果該Entity參數中的主鍵值(可能是聯合主鍵)在DB中已存在則是更新、否則為插入。因此,在某些場景下會有問題。
舉個例子,我們有這么個場景:Entity為CourseDeveloperEntity,該Entity中有單獨的id字段作為主鍵、且有course_id、developer_id聯合唯一索引、還有個is_delete用於邏輯刪除。
在該場景下,若我們刪除了一個CourseDeveloperEntity,則該Entity被刪除了(is_delete標記為true以邏輯刪除,數據庫中實際上還在,但上層業務查不到),因此若之后再插入(course_id、developer_id)一樣的Entity但沒指定id一樣,則會因違背聯合唯一索引而duplicate key的錯。相關代碼:
// 保存 CourseDeveloperEntity courseDeveloperEntity = courseDeveloperRepository.getByCourseIdAndDeveloperId(courseId, developerEntity.getId());//查不到被邏輯刪除的記錄 if (null == courseDeveloperEntity) { courseDeveloperEntity = new CourseDeveloperEntity(); } courseDeveloperEntity.setCourseId(courseId); courseDeveloperEntity.setDeveloperId(developerEntity.getId()); courseDeveloperEntity.setRole(developerRoleInTheCourse); return courseDeveloperRepository.save(courseDeveloperEntity);//由於主鍵自動新生成故courseDeveloperEntity被當成新的,故是執行insert而不是update,從而報錯
JPA Repository的刪除操作
方法名包含條件的刪除操作,如 Integer deleteByNameAndSId(String name, String uuid); ,其執行時與save類似,也是先根據條件查出目標Entity再執行刪除操作。對於 void delete(T entity); 則直接根據Entity的主鍵操作而不用先查。
邏輯刪除
借助org.hibernate.annotations(所以不是JPA的標准)旳 @Where、@SQLDelete、@SQLDeleteALL 這三個注解來實現。
1、定義一個字段用於標識記錄是否被邏輯刪除。這里通過JPA的@MappedSuperclass定義各Entity共有的字段(該注解修飾的Entity不會對應數據庫表,但其內定義的字段會被繼承該Entity的子Entity對應到數據庫字段),包含is_delete:
@Data @NoArgsConstructor @MappedSuperclass public abstract class BaseEntity { @Setter(value = AccessLevel.PRIVATE) @Temporal(TemporalType.TIMESTAMP) @CreationTimestamp @Column(name = "create_time", nullable = false) private Date createTime; @Setter(value = AccessLevel.PRIVATE) @Temporal(TemporalType.TIMESTAMP) @UpdateTimestamp @Column(name = "update_time", nullable = false) private Date updateTime; @Getter(value = AccessLevel.PRIVATE) @Setter(value = AccessLevel.PRIVATE) @Column(name = constant.ISDELETE_COLUMN_NAME, nullable = false) private Boolean isDelete = false; }
2、通過@Where、@SQLDelete、@SQLDeleteALL 三個注解修飾對應數據庫表的Entity來實現邏輯刪除:
@SQLDelete(sql = "update " + StudentEntity.tableName + " set " + constant.ISDELETE_COLUMN_NAME + " =true where sid=?") // 對非nativeQuery 旳delete起作用,包括形如deleteByName等,下同 @SQLDeleteAll(sql = "update " + StudentEntity.tableName + " set " + constant.ISDELETE_COLUMN_NAME + " =true where sid=?") @Where(clause = constant.ISDELETE_COLUMN_NAME + " = false") // 對非nativeQuery的select起作用(如count、非nativeQuery的String myGetNameByName等,前者本質上也是select) @Data @Entity @Table(name = StudentEntity.tableName) public class StudentEntity extends BaseEntity { public static final String tableName = "student"; @Id @Column(name = "sid", length = 36) private String sId; @Column(name = "name", length = 36) private String name; @Column(name = "age") private Integer age; }
需要注意的是:
- @Where會自動在查詢語句后拼接@Where中指定的條件;該注解對所有的非nativeQuery的查詢其作用,如getByName、count、自己寫查詢語句但非nativeQuery的myGetByName等。
- @SQLDelete會自動將刪除語句替換為@SQLDelete中指定的sql操作;該注解對所有非nativeQuery的刪除操作其作用,如delete(StudenEntity entity)、deleteBySId、deleteByName等,但由於指定的sql操作中條件不得不寫死,所以要按期望起作用的話@SQLDelete中的sql操作應以Entity的主鍵為條件,且刪除語句按上述前兩者寫法寫(對於delete(StudenEntity entity)會自動取entity的主鍵給sid),而不能用第三種(會將name參數值傳給sid)
- 通過JPQL的方法名指定刪除操作(如 Integer deleteByName(String name))時背后是先根據條件查出Entity然后根據Entity的主鍵刪除該Entity。所以通過@SQLDelete、@SQLDeleteALL實現邏輯刪除時,由於其語句是寫死的,故:
- @SQLDelete、@SQLDeleteALL同時存在時會按后者來執行軟刪除邏輯
- @SQLDeleteALL並不會批量執行軟刪除邏輯(因為一來不知具體有幾個數據,二來in中只指定了一個元素)而是一個個刪,即有多條待刪除時會一條條執行軟刪除邏輯,每條語句中in中只有一個元素。故其效果與@SQLDelete的一樣,然而in操作效率比=低,故綜上,最好用前者。
關於軟刪除:對於關聯表(一對一、一對多、多對多),若要啟用軟刪除,則須為多對多關聯表定義額外的主鍵字段而不能使用聯合外鍵作為主鍵,否則軟刪除場景下刪除關聯關系再重新關聯時會主鍵沖突。另外,特殊情況下多對多關聯表可以不啟用軟刪除(被關聯表、一對多或多對一關聯表則需要,因為它們側重的信息往往不在於關聯關系而是重要的業務信息)
指定時間或日期字段的返回格式
通過jackson的@JsonFormat注解指定,示例:(也可以注在get方法上)
@Basic @Column(name = "customer_expiretime") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = CommonUtil.DATETIME_PATTERN_WITH_TIMEZONE) private Timestamp customerExpireTime;
指定關系數據庫的存儲引擎
SpringBoot 2.0后使用JPA、Hibernate來操作MySQL,Hibernate默認使用MyISM存儲引擎而非InnoDB,前者不支持外鍵故會忽略外鍵定義,可通過如下SpringBoot配置指定用InnoDB:
spring:
jpa:
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect #不加這句則默認為myisam引擎
復雜Query
注:軟刪除對Criteria API不起作用,但對Specification、QBE查詢會起作用
1. 復雜的操作(復雜查詢、批量更新、批量刪除、SQL函數使用等)
Criteria API用於動態查詢(在運行時才確定查詢條件等,動態產生查詢邏輯)。更多可參閱:見Criteria API:https://en.wikibooks.org/wiki/Java_Persistence/Criteria#CriteriaUpdate_.28JPA_2.1.29
示例:
// update。直接生成db操作語句,而非像named querxy那樣先查出來再根據ID更新 CriteriaUpdate<CourseEntity> update = criteriaBuilder.createCriteriaUpdate(CourseEntity.class); Root<CourseEntity> root = update.from(CourseEntity.class); update.set("tag", "test"); update.where(criteriaBuilder.isNotNull(root.get("versionDescription"))); Query query = entityManager.createQuery(update); int rowCount = query.executeUpdate(); System.err.println(rowCount); // delete。直接生成db操作語句,而非像named query那樣先查出來再根據ID刪除 CriteriaDelete<CourseEntity> delete = criteriaBuilder.createCriteriaDelete(CourseEntity.class); root = delete.from(CourseEntity.class); delete.where(criteriaBuilder.isNull(root.get("versionDescription"))); query = entityManager.createQuery(delete); rowCount = query.executeUpdate(); System.err.println(rowCount); // select。軟刪除對Criteria同樣有效 CriteriaQuery<CourseEntity> select = criteriaBuilder.createQuery(CourseEntity.class); root = select.from(entityManager.getMetamodel().entity(CourseEntity.class)); select.where(criteriaBuilder.isNotNull(root.get("versionDescription"))); query = entityManager.createQuery(select); List<CourseEntity> res = query.getResultList(); System.err.println(res);
2. 復雜條件(多條件和多表)查詢和分頁:Specification(Specification是Spring對Criteria的封裝)
@Override public Page<CourseDeveloperEntity> listJoinedDevelopersOfGivenCourse(Integer page, Integer size, String courseId, String optionalExcludedDeveloperId, String optionalEmailOrNickName) { PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Order.desc("developer.nickName"), Sort.Order.asc("role"))); return courseDeveloperRepository.findAll((Specification<CourseDeveloperEntity>) (root, query, criteriaBuilder) -> { List<Predicate> predicateList = new ArrayList<Predicate>(); predicateList.add(criteriaBuilder.equal(root.get("course").get("id"), courseId)); if (null != optionalExcludedDeveloperId) {// 排除指定開發者 predicateList.add(criteriaBuilder.notEqual(root.get("developer").get("id"), optionalExcludedDeveloperId)); } if (null != optionalEmailOrNickName && optionalEmailOrNickName.trim().length() > 0) { // Predicate p1 = criteriaBuilder.equal(root.get("developer").get("nickName"), optionalEmailOrNickName); // Predicate p2 = criteriaBuilder.equal(root.get("developer").get("email"), optionalEmailOrNickName); Predicate p1 = criteriaBuilder.like(root.get("developer").get("nickName"), "%" + optionalEmailOrNickName + "%"); Predicate p2 = criteriaBuilder.like(root.get("developer").get("email"), "%" + optionalEmailOrNickName + "%"); predicateList.add(criteriaBuilder.or(p1, p2)); } return criteriaBuilder.and(predicateList.toArray(new Predicate[predicateList.size()])); }, pageRequest); }
Spring還提供了QBE(query by example)查詢,示例:
AclDomainSubject domainSubjectExample = AclDomainSubject.builder().domain(domainOptional.get()).build(); List<AclDomainSubject> subjects = domainSubjectRepository.findAll(Example.of(domainSubjectExample)); Example<AclDomainSubject> example = Example.of(domainSubjectExample, ExampleMatcher.matching() .withMatcher("domain.id", new ExampleMatcher.GenericPropertyMatcher().exact()));//更加復雜的example,指定匹配規則 subjects = domainSubjectRepository.findAll(example);
可參閱:https://blog.wuwii.com/jpa-specification.html
定義公共Repository
可以將業務中用到的公共方法抽離到公共Repository中,示例:
@NoRepositoryBean //避免Spring容器為此接口創建實例。不被Service層直接用到的Repository(如base repository)均應加此聲明
public interface BaseRepository<T, ID> { @Modifying @Query("update #{#entityName} set isDelete='N' where id in ?1 ") Integer myUpdateAsNotDeleted(Collection<String> ids); }
通過JPA定義表結構的關聯關系(如共用部分字段等)
這里以實際項目中課程、實驗、步驟與其翻譯數據的表結構關聯方案設計為例:
更多可參閱:https://en.wikibooks.org/wiki/Java_Persistence/Inheritance
對於關聯表的定義
最好定義個外鍵變量、同時定義該外鍵對應的被關聯表實體的一個變量,而不是只定義后者。只定義后者的話要獲取被關聯對象的主鍵時會做數據庫查詢被關聯對象的操作,顯然多了次IO
盡可能避免定義一對一關聯關系(如目前的customer_schedule),即是業務真的是一對一,也可以按照一對多甚至多對多設計,利於后期擴展。當然也有不得不為一對一的,如developer與user等
Map類型的關聯屬性
JPA1.0起就支持Map類型的關聯屬性,可通過 @MapKey 定義。示例:
@Entity public class Employee { @Id private long id; ... @OneToMany(mappedBy="owner") @MapKey(name="type") private Map<String, PhoneNumber> phoneNumbers; ... } @Entity public class PhoneNumber { @Id private long id; @Basic private String type; // Either "home", "work", or "fax". ... @ManyToOne private Employee owner; ... }
In JPA 1.0 the map key must be a mapped attribute of the collection values. The
@MapKeyannotation or<map-key>XML element is used to define a map relationship. If theMapKeyis not specified it defaults to the target object'sId.
更多可參閱:https://en.wikibooks.org/wiki/Java_Persistence/Relationships#Maps
JPA Repository save方法
具體實現如下:(SimpleJpaRepository)
@Transactional public <S extends T> S save(S paraEntity) { if (entityInformation.isNew(paraEntity)) {//根據id是否存在判斷是否為new。這里的new是相對於Persistence Context而言的,而非針對db是否有該記錄 em.persist(paraEntity); //將該entity變為managed的entity return entity; } else { return em.merge(paraEntity);//將該entity的數據更新到Persistence Context。若Persistence Context里尚未有同id的該entity,則會根據id執行一次db查詢 } }
要明確下,這里的save的語義並不是保存到db,而是保存到Persistence Context(保存到db會在事務提交時或Persistence Context flush時做)。save時會根據paraEntity的id是否存在來判斷paraEntity在Persistence Context中是否為新的:
1、若是新的,則persist:paraEntity直接保存到Persistence Context(即entity變為managed狀態)。flush時產生sql insert操作
2、若否,則merge:認為Persistence Context中已有同id的entity,假設為existEntity(這里的'同id'當然不僅是值,還有entity類型,為便於表述只說id):
2.1、若Persistence Context中確有同id entity,則將paraEntity的數據更新到對應existEntity。flush時產生sql update操作(且字段值有發生變化才會產生sql update操作!)
此時,paraEntity與existEntity可能是同一個對象,也可能不是。
2.2、若Persistence Context中實際上尚未有同id的entity,則會根據id執行一次db查詢:若查不到則同1,否則同2.1
可見:
1、save的執行實際上是將entity的數據和狀態更新到Persist Context,只有當flush或事務提交時,才會將entity的數據insert或update到數據庫。"At flush-time the entity state transition is materialized into a database DML statement."
2、調用save方法並不一定會產生db操作:數據字段值未發生改變時。如查出來再直接保存
3、另外,執行JPQL時(即下面說的結論對nativeQuery無效),只要JPA認為最后是insert或update操作,被@UpdateTimestamp修飾的字段就會自動更新為當前時間。同理只要認為需要執行insert,@CreationTimestamp修飾的字段會更新為當前值。
4、jpa persist操作對應sql insert 操作、jpa merge操作對應sql insert或sql update操作。
While a
savemethod might be convenient in some situations, in practice, you should never callmergefor entities that are either new or already managed. As a rule of thumb, you shouldn’t be usingsavewith JPA. For new entities, you should always usepersist, while for detached entities you need to callmerge. For managed entities, you don’t need anysavemethod because Hibernate automatically synchronizes the entity state with the underlying database record.
JPA事務內Entity變更自動更新到數據庫(自動提交)
automatic dirty checking mechanism:
若啟用了事務,則對於managed狀態的entity,若在事務內該entity有字段的值發生了變化,則即使未調save方法,該entity的變化最后也會被自動同步到數據庫,即sql update操作。即相當於在Persist Context flush時自動對各engity執行 save 方法。(org.hibernate.event.internal.AbstractFlushingEventListener中)
詳情可參閱:https://vladmihalcea.com/the-anatomy-of-hibernate-dirty-checking/
Spring Data JPA官方文檔閱讀note
Defining Repository Interfaces
Null Handling of Repository Method's Return:返回單實例時可能為null(可以用各種Optional包裝類作為返回類型以避免null),為collections, collection alternatives, wrappers, and streams are guaranteed never to return null but rather the corresponding empty representation
Using Repositories with Multiple Spring Data Modules:Spring Data modules accept either third-party annotations (such as JPA’s @Entity) or provide their own annotations (such as @Document for Spring Data MongoDB and Spring Data Elasticsearch).
Defining Query Methods
趟過的坑
在一個事務內,save后再查詢出來的數據實際上還是內存的數據(用saveAndFlush也會這樣),因此如果數據庫時間字段啟用了CURRENT_TIMESTAMP ON UPDATE,則返回給調用者的時間實際上與數據庫中的時間不一樣。故,最好最好不要用自動更新時間,而是業務邏輯中手動設置更新時間。
另外,如MySQL的DATETIME類型,默認是精確到秒的,故存入的時間戳的毫秒會被舍棄並根據四舍五入加入到秒(如1s573ms變成2s、1s473ms變成1s),從而保存進去與查出來的也會不一致。
進階
EntityManager

Entity生命周期及相關Event
Entity的生命周期由EntityManager管理,其生命周期在persistence context內。EntityManager的生命周期(三種): a transaction、request、a users session
兩個相關概念:persistence context(操作:persist、merge、detach、remove、clear、flush等)、transaction(操作:commit、rollback等)
Persistence Context:Acts as a Map of entities,the Map key is formed of the entity type (its class) and the entity identifier.
Entity生命周期的四個狀態:new、managed、detached、removed。其間轉換關系如下:

Entity生命周期的四個基本操作(CRUD):(更多詳情可參閱:https://en.wikibooks.org/wiki/Java_Persistence/Persisting、https://vladmihalcea.com/a-beginners-guide-to-jpa-hibernate-entity-state-transitions/、https://vladmihalcea.com/jpa-persist-and-merge/)
- persist(Insert)
- removed(Delete)
- merge(Update)
- find(Select)
這些操作為EntityManager的方法,所有的操作都先在persistence context進行,除非persistence context flush或transaction commit了
相關事件:
- @PrePersist/@PostPersist
- @PreRemove/@PostRemove
- @PreUpdate/@PostUpdate
- @PostLoad
The pre- and post-persist methods are useful for setting timestamps for auditing or setting default values.
詳情參閱:https://dzone.com/articles/jpa-entity-lifecycle
實踐踩坑:
實際使用過程中常會遇到“identifier of an instance of com.sensetime.sensestudy.common.entity.CourseEntity was altered from to” 之類的錯
復現:在一個事務內修改entity的主鍵,此后執行其他任意查詢或更新操作就會產生該錯
原因:在entity處於managed生命周期時修改了entity主鍵導致的
解決:修改前先將該entity detach
Transaction
JPA提供兩種transaction機制:Resource Local Transactions、JTA Transactions。詳情參閱:https://en.wikibooks.org/wiki/Java_Persistence/Transactions
When a transaction commit fails, the transaction is automatically rolled back, and the persistence context cleared, and all managed objects detached.
Not only is there no way to handle a commit failure, but if any error occurs in an query before the commit, the transaction will be marked for rollback, so there is no real way to handle any error.
Cache
參閱:https://en.wikibooks.org/wiki/Java_Persistence/Caching
更多參考資料:
