原文地址:https://www.cnblogs.com/z-sm/p/9176636.html
博主注:个人比较推崇Spring全家桶,Spring Boot + Spring Data JPA + Spring thymeleaf 。一个完整的Spring Boot + MySQL的增删改查例子,一篇博文就能讲的清道的明。这个 Spring Data JPA 将原本就是抽象规范的 JPA 又封装了一遍,更加的简单易用,可见我的此文,作大概了解。
软件开发无非以实现业务为第一要务,满足性能为第二,何苦纠缠于繁杂的各种技术实现框架(Struts Hibernate MyBatis Freemark)中去呢,学习各种流行框架它就不费时间不费脑子么?有这个时间和女票聊天看剧,甚至打游戏,它就不香么?人生苦短,过于执拗徒增白发(徒掉黑发)。
框架或中间件是用来帮助你的,而不是代替你去思考和工作的。
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.
如上面所述,JPA是Java标准中的一套规范。其为我们提供了:
关系图:
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定义的Object至少需要@Entity、@Id注解,示例:
View Code
这些注解的位置可以有两种(Access Type):Field(在变量上)上、Property(在变量的get方法)上。一个Ojbect内的JPA注解要么在Field上要么在Property上(当然可以在类上),不能两者同时有。详情可参阅:https://en.wikibooks.org/wiki/Java_Persistence/Mapping#Access_Type
get
and set
methods 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 标注并列使用
**@id**
@Id 标注用于声明一个实体类的属性映射为数据库的一个主键列
@Id标注也可置于属性的getter方法之前。以下注解也一样可以标注于getter方法前。
若同时指定了下面的@GeneratedValue则存储时会自动生成主键值,否则在存入前用户需要手动为实体赋一个主键值。主键值类型可能是:
指定联合主键,有@IdClass、@EmbeddedId两种方法,可参阅:https://en.wikibooks.org/wiki/Java_Persistence/Identity_and_Sequencing#Composite_Primary_Keys
**@IdClass**
修饰在实体类上,指定联合主键。如:@IdClass(StudentExperimentEntityPK.class),主键类StudentExperimentEntityPK需要满足:
示例:
CustomerCourseEntityPK
CustomerCourseEntity
**@EmbeddedId**
功能与@IdClass一样用于指定联合主键。不同的在于其是修饰实体内的一个主键类变量,且主键类应该被@Embeddable修饰。
此外在主键类内指定的字段在实体类内可以不再指定,若再指定则需为@Column加上insertable = false, updatable = false属性
**@GeneratedValue**
@GeneratedValue 用于标注主键的生成策略,通过 strategy 属性指定。默认情况下,JPA 自动选择一个最适合底层数据库的主键生成策略:SqlServer 对应 identity,MySQL 对应 auto increment
更多详情可参阅: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属性指定关系,有三种策略:
更多详情可参阅: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定义即可。此法下不同子类拥有父类的公共字段且该字段在不同子类中对应的数据库列定义可不同。
实践示例:
View Code
(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
Serializable
attribute that is not a relationship or a basic type (String, Number, temporal, primitive), will be serialized to aBLOB
field.
法2:JPA 2.1起可通过 @Convert 指定属性与数据库列间的映射逻辑,其可将任意对象映射到数据库的一个列(详见后文)。在这之前没有@Convert,可以通过get、set方法实现类似效果,示例:
View Code
如@Id、@Column等注解指定Entity的字段与数据库字段对应关系时,注解的位置可以在Field(属性)或Property(属性的get方法上),两者统一用其中一种,不能两者均有。推荐用前者。
详情可参阅:https://en.wikibooks.org/wiki/Java_Persistence/Mapping
基本用法:通过方法名来指定查询逻辑,而不需要自己实现查询的SQL逻辑,示例:List
方法名解析原理:对方法名中除了保留字(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进行查询)
查询字段解析原理:默认会查出Entity的所有字段且返回类型为该Entity类型,有两种情况可查询部分字段(除此外都会查出所有字段):
1、通过@Query写的自定义查询逻辑中只查部分字段。这种不属于直接通过方法名指定查询,这里先不讨论(见后面查询指定部分字段的条目)。
2:返回类型为自定义接口或该接口列表,接口中仅包含部分字段的get方法,此时会根据接口方法名查询部分字段。示例:
//CourseRepository.java
List 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 userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;
NotIn --- 等价于 SQL 中的 "not in",比如 findByUsernameNotIn(Collection userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;
Containing --- 包含指定字符串
StargingWith --- 以指定字符串开头
EndingWith --- 以指定字符串结尾
更多见 官方文档Spring Data JPA
List
使用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 myGetBySchoolIdOrderByJobNumber(String schoolId, int startIndex, Integer size,
String searchNameOrJobnumOrBzOrPhoneOrEmai);// nativeQuery返回类型可以声明为Entity,会自动进行匹配,要求查回与Entitydb中字段对应的所有db中的字段
通常可以在@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)。后者一般少用,除非非常确定字段很少访问到。
只有有在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
静态方式:直接在方法体现(如 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 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 myGetByNicknameOrPhoneOrEmailOrBz(String searchNicknameOrPhoneOrEmailOrBz, Pageable pageable);
如 UserEntity u=userRepository.save(userEntity) ,其中UserEntity包含成员变量private SchoolEntity schoolEntity。Repository的save方法会返回被save的entity,但若是第一次保存该entity(即新建一条记录)时u.schoolEntity的值会为null,解决:用saveAndFlush
1、通过@Query注解
对于只返回一个字段的查询:
@Query(value = "select languageType from CourseTranslationEntity where courseId=?1") Set myGetLanguageTypeByCourseId(String courseId);
对于返回多个字段的查询:
对于nativeQuery,直接select部分字段即可,结果默认会自动包装为Map。为了便于理解可以直接将结果声明为Map。示例:
View Code
其可以达到目的,但缺点是sql里用的直接是数据库字段名,导致耦合大,数据库字段名一变,所有相关sql都得相应改变。
对于非nativeQuery:(sql里的字段名是entity的字段名,数据库字段名改动只要改变entity中对应属性的column name即可,解决上述耦合大的问题)
当Repository返回类型为XXEntity或List
也可以自定义一个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不太好,更好的是一个类:我们可以自定义一个接口,然后将返回类型声明为接口列表即可。接口get方法名中的字段与上面as后的字段名对应。(当然,也可以不用接口而是用自定义包含部分字段的类,此时new时需要用类的全限定名)示例:
// JPQL查询部分字段时,默认返回List类型,可通过new map指定返回map,此时map key默认为顺序0 1 2 等,可通过as指定key名
@Query("select new map(id as idx, languageType as languageType) from CourseEntity where id in ?1 ")
List myGetLanguagesTypeByCourseIdIn(Collection courseIdCollection);
// 以下为为了查询Entity部分字段而定义的返回类型
public interface IdAndLanguageType {
public String getIdx(); //get方法中的字段名须与上面通过as取的key别名对应
public String getLanguageType();
}
@Query用于查询时返回值总结 :
若是查询多个字段则返回时默认将这些字段包装为Object[]、若返回有多条记录则包装成List
select中仅有一个字段时(可以是表中的一个列名、Entity中的一个字段名、或一个Entity对象名,如language_type、languageType、courseEntity),方法返回类型须声明为该字段的类型或类型列表(如String、CourseEntity、List
select中有至少两个字段时,默认会将每条记录的几个字段结果包装为数组。可以手动指定包装为map,此时map的key为字段序号,故最通过as指定key为字段名。可参阅:https://en.wikibooks.org/wiki/Java_Persistence/Querying#Query_Results
2、不通过@Query注解,直接通过方法名签名指定部分字段查询
实际上,在前面介绍JPA查询时介绍了直接通过方法签名实现只查询部分字段的方法。上面的查询不用@Query注解的等价写法如下:
List
其内部原理是根据接口的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取别名
(需要加@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
Integer countByName(String name);
通过@ManyToOne、@OneToMany、@OneToOne、@JoinColumn等实现,用法及区别见:https://www.cnblogs.com/chiangchou/p/mappedBy.html
通过(2)指定数量对应关系时,须在多的一方标注(@ManyToOne),一的一方注不注均可。(以下以School、Student为例,为一对多关系)
总结:通常用法示例如下
//StudentEntity
@Column(name = "school_id")
private String schoolId;
//get set ...
private SchoolEntity schoolBySchoolId;
// 多个字段对应数据库同一字段时会报错,通过添加insertable = false, updatable = false即可
@ManyToOne
@JoinColumn(name = "school_id", referencedColumnName = "id", nullable = false, insertable = false, updatable = false)//指定当前Entity的school_id引用SchoolEntity的id列
public SchoolEntity getSchoolBySchoolId() {
return schoolBySchoolId;
}
public void setSchoolBySchoolId(SchoolEntity schoolBySchoolId) {
this.schoolBySchoolId = schoolBySchoolId;
}
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/
对于单字段的可以直接在方法名加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 getByCustomerId(String customerId, String searchSchoolnameOrBz, Pageable pageable);
借助JPA converter to map your Entity to the database.
在要被映射的字段上加上注解: @Convert(converter = JpaConverterJson.class)
实现JpaConverterJson:
public class JpaConverterJson implements AttributeConverter
需要注意的是,若Entity字段是一个 JavaBean 或 JavaBean 列表(如 TimeSlice 或 List
View Code
参考资料:https://stackoverflow.com/questions/25738569/jpa-map-json-column-to-java-object
本质上就是将数据序列化成基本数据类型如String。如要把List
法1:可以在业务层写代码将gradeIdList序列化成String: String res=objectMapper.writeValueAsString(gradeIdList);// 借助objectMapper.writeValueAsString(data); ,之后保存即可。从数据库中读取时: List
法2:实现一个AttributeConverter,并应用于Entity字段。此法相当于指定了AttributeConverter后让框架去自动做转换
View Code
枚举示例
@Column(name = "sex")
@Enumerated(EnumType.ORDINAL)//持久化为0,1
private Sex sex;
@Column(name = "type")
@Enumerated(EnumType.STRING)//持久化为字符串
private Role role;
示例:
View Code
通过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:
View Code
2、通过@Where、@SQLDelete、@SQLDeleteALL 三个注解修饰对应数据库表的Entity来实现逻辑删除:
View Code
需要注意的是:
通过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引擎
注:软删除对Criteria API不起作用,但对Specification、QBE查询会起作用
1. 复杂的操作(复杂查询、批量更新、批量删除、SQL函数使用等)
Criteria API用于动态查询(在运行时才确定查询条件等,动态产生查询逻辑)。更多可参阅:见Criteria API:https://en.wikibooks.org/wiki/Java_Persistence/Criteria#CriteriaUpdate_.28JPA_2.1.29
示例:
View Code
2. 复杂条件(多条件和多表)查询和分页:Specification(Specification是Spring对Criteria的封装)
View Code
Spring还提供了QBE(query by example)查询,示例:
View Code
可参阅:https://blog.wuwii.com/jpa-specification.html
可以将业务中用到的公共方法抽离到公共Repository中,示例:
@NoRepositoryBean //避免Spring容器为此接口创建实例。不被Service层直接用到的Repository(如base repository)均应加此声明
public interface BaseRepository {
@Modifying
@Query("update #{#entityName} set isDelete='N' where id in ?1 ")
Integer myUpdateAsNotDeleted(Collection ids);
}
这里以实际项目中课程、实验、步骤与其翻译数据的表结构关联方案设计为例:
多语言表(翻译表)与原表(主表)关联方案设计,需求:字段(列)复用以免重复代码定义、同一个列的定义如是否为空在不同表中可不一样(如有些字段主表中非空但翻译表中可空),有如下方案:
更多可参阅:https://en.wikibooks.org/wiki/Java_Persistence/Inheritance
JPA1.0起就支持Map类型的关联属性,可通过 @MapKey 定义。示例:
View Code
In JPA 1.0 the map key must be a mapped attribute of the collection values. The
@MapKey
annotation orXML element is used to define a map relationship. If the
MapKey
is not specified it defaults to the target object'sId
.
更多可参阅:https://en.wikibooks.org/wiki/Java_Persistence/Relationships#Maps
具体实现如下:(SimpleJpaRepository)
@Transactional
public 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
save
method might be convenient in some situations, in practice, you should never callmerge
for entities that are either new or already managed. As a rule of thumb, you shouldn’t be usingsave
with JPA. For new entities, you should always usepersist
, while for detached entities you need to callmerge
. For managed entities, you don’t need anysave
method because Hibernate automatically synchronizes the entity state with the underlying database record.
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/
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
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/)
这些操作为EntityManager的方法,所有的操作都先在persistence context进行,除非persistence context flush或transaction commit了
相关事件:
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
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.
参阅:https://en.wikibooks.org/wiki/Java_Persistence/Caching
更多参考资料:
Spring Data JPA 官方文档
Java Persistenc-wikibooks