丰富的线上&线下活动,深入探索云世界
做任务,得社区积分和周边
最真实的开发者用云体验
让每位学生受益于普惠算力
让创作激发创新
资深技术专家手把手带教
遇见技术追梦人
技术交流,直击现场
海量开发者使用工具、手册,免费下载
极速、全面、稳定、安全的开源镜像
开发手册、白皮书、案例集等实战精华
为开发者定制的Chrome浏览器插件
JPA(JavaPersistenceAPI)是Java标准中的一套ORM规范(提供了一些编程的API接口,具体实现由ORM厂商实现,如Hiernate、TopLink、Eclipselink等都是JPA的具体实现),借助JPA技术可以通过注解或者XML描述【对象-关系表】之间的映射关系,并将实体对象持久化到数据库中(即ObjectModel与DataModel间的映射)。
JPA是Java持久层API,由Sun公司开发,希望规范、简化Java对象的持久化工作,整合ORM技术,整合第三方ORM框架,建立一种标准的方式,目前也是在按照这个方向发展,但是还没能完全实现。在ORM框架中,Hibernate框架做了较好的JPA实现,已获得Sun的兼容认证。
JPA的优势:
1.开发者面向JPA规范的接口,但底层的JPA实现可以任意切换:觉得Hibernate好的,可以选择HibernateJPA实现;觉得TopLink好的,可以选择TopLinkJPA实现。
2.开发者可以避免为使用Hibernate学习一套ORM框架,为使用TopLink又要再学习一套ORM框架。
JPA为我们提供了以下规范:
Hibernate介绍
Hibernate对数据库结构提供了较为完整的封装,Hibernate的O/RMapping实现了POJO和数据库表之间的映射,以及SQL的自动生成和执行。往往只需定义好了POJO到数据库表的映射关系,即可通过Hibernate提供的方法完成持久层操作。甚至不需要对SQL的熟练掌握,Hibernate/OJB会根据制定的存储逻辑,自动生成对应的SQL并调用JDBC接口加以执行。
Hibernate框架(3.2及以上版本)对JPA接口规范做了较好的实现,主要是通过以下三个组件来实现的:
hibernate对JPA的支持,不是另提供了一套专用于JPA的注解。一些重要的注解如@Column,@OneToMany等,hibernate并没有提供,这说明JPA的注解已经是hibernate的核心,hibernate只提供了一些补充,而不是两套注解。JPA和hibernate都提供了的注解(例如@Entity),若JPA的注解够用,就直接用,若JPA的注解不够用,直接使用hibernate的即可。
SpringDataJPA介绍
SpringDataJPA是在实现了JPA规范的基础上封装的一套JPA应用框架(CriteriaAPI还是有些复杂)。虽然ORM框架都实现了JPA规范,但是在不同的ORM框架之间切换仍然需要编写不同的代码,而使用SpringDataJPA能够方便的在不同的ORM框架之间进行切换而不需要更改代码。SpringDataJPA旨在通过统一ORM框架的访问持久层的操作,来提高开发人的效率。
SpringDataJPA是一个JPA数据访问抽象。也就是说SpringDataJPA不是一个实现或JPA提供的程序,它只是一个抽象层,主要用于减少为各种持久层存储实现数据访问层所需的样板代码量。但是它还是需要JPA提供实现程序,其实SpringDataJPA底层就是使用的Hibernate实现。
SpringDataJPA其实并不依赖于Spring框架。
SpringDataJPA通过Repository来支持上述功能,默认提供的几种Repository已经满足了绝大多数需求:
JPA与springDataJpa、Hibernate之间的关系
Querydsl-JPA介绍
Springdata-JPA是对JPA使用的封装,Querydsl-JPA也是基于各种ORM之上的一个通用查询框架,使用它的API类库可以写出“Java代码的sql”,不用去手动接触sql语句,表达含义却如sql般准确。更重要的一点,它能够构建类型安全的查询,这比起JPA使用原生查询时有很大的不同,可以不必再对恶心的“Object[]”进行操作了。SpringDataJPA+Querydsl-JPA联合使用方案是使用JPA操作数据库的最佳方案,它们之间有着完美的相互支持,以达到更高效的编码。
指定对象与数据库字段映射时注解的位置:如@Id、@Column等注解指定Entity的字段与数据库字段对应关系时,注解的位置可以在Field(属性)或Property(属性的get方法上),两者统一用其中一种,不能两者均有。推荐用前者。
@Entity(必需)
标注在实体类上。
映射实体类。指出该Java类为实体类,将映射到指定的关系数据库表。
应用了此注解后,将会自动将类名映射作为数据库表名、将类内的字段名映射为数据库表的列名。映射策略默认是按驼峰命名法拆分将类名或字段名拆分成多部分,然后以下划线连接,如StudentEntity->student_entity、studentName->student_name。若不按默认映射,则可通过@Table、@Column指定,见下面。
@Table(可选)
映射数据库表名。当实体类与其映射的数据库表名不同名时需要使用@Table标注说明,该标注与@Entity标注并列使用
@DynamicInsert(可选)
设置为true,表示insert对象的时候,生成动态的insert语句,如果这个字段的值是null就不会加入到insert语句中,默认false。
@DynamicUpdate(可选)
设置为true,表示执行update对象时,在生成动态的update语句前,会先查询该表在数据库中的字段值,并对比更新使用的对象中的字段值与数据库中的字段值是否相同,若相同(即该值没有修改),则该字段就不会被加入到update语句中。
默认false,表示无论更新使用实体类中的字段值与数据库中的字段值是否一致,都加入到update语句中,即都使用对象中所有字段的值覆盖数据库中的字段值。
比如只想更新某个属性,但是却把整个属性都更改了,这并不是我们希望的结果,我们希望的结果是:我更改了哪写字段,只要更新我修改的字段就够了。
注意:
@DynamicUpdate的动态更新的含义是,比较更新要使用的实体类中的所有字段值与从数据库中查询出来的所有字段值,判断其是否有修改,不同则加入到update语句中更新字段值。看这个例子,数据库中id=1的记录所有字段都是非空的,但是实体类中只有name有值,也就是所有字段都变了,只是其他字段被更新为了新的空值。
所以jpa更新数据库字段值,无论是否有@DynamicUpdate注解,均需要手动先select对象,然后通过set更新对象的属性值,然后再save对象,实现更新操作
@Id(必需)
标注在实体类成员变量或getter方法之上。
若同时指定了下面的@GeneratedValue则存储时会自动生成主键值,否则在存入前用户需要手动为实体赋一个主键值。
主键值类型可以是:
指定联合主键,有@IdClass、@EmbeddedId两种方法。
@GeneratedValue
@GeneratedValue用于标注主键的生成策略,通过strategy属性指定。
默认情况下,JPA自动选择一个最适合底层数据库的主键生成策略:SqlServer对应identity,MySQL对应autoincrement
@Column(可选)
映射表格列。当实体的属性与其映射的数据库表的列不同名时需要使用@Column标注说明。
类的字段名在数据库中对应的字段名可以通过此注解的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】
@Column注解一共有10个属性,这10个属性均为可选属性,各属性含义分别如下:
@Basic(可选)
表示一个简单的属性到数据表的字段的映射,对于没有任何标注的属性或getter方法,默认标注为@Basic
@Transient:忽略属性
定义暂态属性。表示该属性并非一个到数据库表的字段的映射,ORM框架将忽略该属性。
如果一个属性并非数据库表的字段映射,就必须将其标识为@Transient,否则ORM框架默认为其注解@Basic,例如工具方法不需要映射。
标注在实体类成员变量或getter方法之上。可选。
在JavaAPI中没有定义Date类型的精度,而在数据库中表示Date类型的数据类型有Date(年月日),Time(时分秒),TimeStamp(年月日时分秒)三种精度,进行属性映射的时候可以使用@Temporal注解调整精度。
目前此注解只能用于修饰JavaAPI中的【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以免该字段被更新privateDatecreateTime;@Temporal(TemporalType.TIMESTAMP)@UpdateTimestamp//org.hibernate.annotations.UpdateTimestamp,用于在JPA执行update操作时自动更新该字段值privateDateupdateTime;
1、Hibernate的注解:
用法:
标注为@MappedSuperclass的类将不是一个完整的实体类,将不会映射到数据库表,但是其属性都将映射到其子类的数据库字段中。
标注为@MappedSuperclass的类不能再标注@Entity或@Table注解,也无需实现序列化接口。
允许多级继承
注解的类继承另一个实体类或标注@MappedSuperclass类,可以使用@AttributeOverride或@AttributeOverrides注解重定义其父类属性映射到数据库表中字段。
指定联合主键类。如:@IdClass(StudentExperimentEntityPK.class)
主键类StudentExperimentEntityPK需要满足:
/***实体类*/@Data@Entity@Table(name="customer_course")@IdClass(CustomerCourseEntityPK.class)//指定联合主键类publicclassCustomerCourseEntity{@Id@Column(name="customer_id",length=ColumnLengthConstrain.LEN_ID_MAX)privateStringcustomerId;@Id@Column(name="course_id",length=ColumnLengthConstrain.LEN_ID_MAX)privateStringcourseId;@Column(name="max_number")privateIntegermaxNumber;@ManyToOne@JoinColumn(name="course_id",referencedColumnName="id",nullable=false,insertable=false,updatable=false)privateCourseEntitycourseByCourseId;}/***联合主键类*/@DatapublicclassCustomerCourseEntityPKimplementsSerializable{privatestaticfinallongserialVersionUID=1L;privateStringcustomerId;privateStringcourseId;}
标注在实体类成员变量或getter方法上。
功能与@IdClass一样用于指定联合主键。不同的是其标注在实体内的主键类变量上,且主键类应该标注@Embeddable注解。
此外在主键类内指定的字段在实体类内可以不再指定,若再指定则需为@Column加上insertable=false,updatable=false属性
@Data@Entity@Table(name="customer_course")@IdClass(CustomerCourseEntityPK.class)//指定联合主键类publicclassCustomerCourseEntity{@EmbeddedIdprivateCustomerCourseEntityPKid;@Column(name="max_number")privateIntegermaxNumber;}/***联合主键类*/@Data@EmbeddablepublicclassCustomerCourseEntityPKimplementsSerializable{privatestaticfinallongserialVersionUID=1L;@Column(name="customer_id",length=ColumnLengthConstrain.LEN_ID_MAX)privateStringcustomerId;@Column(name="course_id",length=ColumnLengthConstrain.LEN_ID_MAX)privateStringcourseId;}
用于表结构复用。指定被该注解修饰的类被子类继承后子类和父类的表结构的关系。
通过strategy属性指定关系,有三种策略:
@Inheritance与@MappedSuperclass的区别:
@Inheritance、@MappedSuperClass可用于定义Inheritance关系。这些方式的一个缺点是子类中无法覆盖从父类继承的字段的定义(如父类中name是notnull,但子类中允许为null)。
除了@Inheritance、@MappedSuperClass外,还有一种Inheritance方法(此法可解决上述不足):先定义一个JavaPOJO(干净的POJO,没有任何对该类使用任何的ORM注解),然后不同子类继承该父类,并分别在不同子类中进行ORM定义即可。此法下不同子类拥有父类的公共字段且该字段在不同子类中对应的数据库列定义可不同。
当一个实体类要在多个不同的实体类中进行使用,而其不需要生成数据库表
@Embeddable:标注在类上,表示此类是可以被其他类嵌套
@Embedded:标注在属性上,表示嵌套被@Embeddable注解的同类型类
使用此注解映射枚举字段,以String类型存入数据库
注入数据库的类型有两种:EnumType.ORDINAL(Interger)、EnumType.STRING(String)
TableGenerator定义一个主键值生成器,在@GeneratedValue的属性strategy=GenerationType.TABLE时,generator属性中可以使用生成器的名字。生成器可以在类、方法或者属性上定义。
生成器是为多个实体类提供连续的ID值的表,每一行为一个类提供ID值,ID值通常是整数。
属性说明:
@EntitypublicclassEmployee{@Id@Column(name="id")@TableGenerator(name="hf_opert_id_gen",//此处的名字要和下面generator属性值一致table="mcs_hibernate_seq",//主键保存到数据库的表名pkColumnName="sequence_name",//表里用来保存主键名字的字段valueColumnName="sequence_next_hi_value",//表里用来保存主键值的字段pkColumnValue="user_id",//表里名字字段对应的值allocationSize=1)//自动增长,设置为1@GeneratedValue(strategy=GenerationType.TABLE,generator="hf_opert_id_gen")privateIntegerid;}
@JoinColumn:指定外键
如果在实体类的某个属性上定义了联表关系(OneToOne或OneTOMany等),则使用@JoinColumn注解来定义关系的属性。JoinColumn的大部分属性和Column类似。
@Data@EntitypublicclassPerson{...//Person和Address是一对一关系。Address表中名为id_address的列作为外键指向Person表中名为address_id的列@OneToOne@JoinColumn(name="address_id",referencedColumnName="id_address",unique=true)privateAddressaddress;}@Data@EntitypublicclassAddress{@Id@column(name="id_address")privateIntegeridAddress;}
@JoinColumns
如果在实体类的某个属性上定义了联表关系(OneToOne或OneTOMany等),并且关系存在多个JoinColumn,则使用@JoinColumns注解定义多个JoinColumn的属性。
@Data@EntitypublicclassCustom{//Custom和Order是一对一关系。Order表中一个名为CUST_ID的列作为外键指向Custom对应表中名为ID_CUST的列,另一名为CUST_NAME的列作为外键指向Custom对应表中名为NAME_CUST的列@OneToOne@JoinColumns({@JoinColumn(name="CUST_ID",referencedColumnName="ID_CUST"),@JoinColumn(name="CUST_NAME",referencedColumnName="NAME_CUST")})privateOrderorder;}
@OneToOne
描述一个一对一的关联
@Data@EntitypublicclassPerson{...//Person和Address是一对一关系。Address表中名为id_address的列作为外键指向Person表中名为address_id的列@OneToOne@JoinColumn(name="address_id",referencedColumnName="id_address",unique=true)privateAddressaddress;}
@OneToMany
描述一个一对多的关联,该属性应该为集体类型,在数据库中并没有实际字段。
例如:实体User和Order是OneToMany的关系,则实体User被删除时,其关联的实体Order也应该被全部删除
@ManyToOne
表示一个多对一的映射,该注解标注的属性通常是数据库表的外键
@ManyToMany
描述一个多对多的关联。多对多关联上是两个一对多关联,但是在ManyToMany描述中,中间表是由ORM框架自动处理
两个实体间相互关联的属性必须标记为@ManyToMany,并相互指定targetEntity属性,需要注意的是,有且只有一个实体的@ManyToMany注解需要指定mappedBy属性,指向targetEntity的集合属性名称,利用ORM工具自动生成的表除了User和Book表外,还自动生成了一个User_Book表,用于实现多对多关联
@JsonFormat
spring.jackson.date-format=yyyy-MM-ddHH:mm:ssspring.jackson.time-zone=GMT+8
@DateTimeFormat
通过annotation来映射hibernate实体,基于annotation的hibernate主键标识@Id,由@GeneratedValue设定其生成规则。
用于标注主键的生成策略,通过strategy属性指定。默认情况下,JPA自动选择一个最适合底层数据库的主键生成策略:SqlServer对应identity,MySQL对应autoincrement。
源码定义:
@Target({METHOD,FIELD})@Retention(RUNTIME)public@interfaceGeneratedValue{GenerationTypestrategy()defaultAUTO;Stringgenerator()default"";}其中GenerationType:
publicenumGenerationType{TABLE,SEQUENCE,IDENTITY,AUTO}JPA提供的四种标准用法为:
1、AUTO用法
//使用示例@Id@GeneratedValue(strategy=GenerationType.AUTO)在指定主键时,如果不指定主键生成策略,默认为AUTO。
//使用示例@Id此时主键生成策略,为默认值AUTO。
以下指定主键生成策略为AUTO,效果同上:
//使用示例@Id@GeneratedValue(strategy=GenerationType.AUTO)
2、IDENTITY用法
//使用示例@Id@GeneratedValue(strategy=GenerationType.IDENTITY)
3、TABLE用法
//使用示例。@Id@GeneratedValue(strategy=GenerationType.TABLE,generator="pk_gen")@TableGenerator(name="pk_gen",table="tb_generator",pkColumnName="gen_name",valueColumnName="gen_value",pkColumnValue="PAYABLEMOENY_PK",allocationSize=1)使用此策略需要数据库存在相应的表。此处应用表tb_generator,定义为:
CREATETABLEtb_generator(idNUMBERNOTNULL,gen_nameVARCHAR2(255)NOTNULL,gen_valueNUMBERNOTNULL,PRIMARYKEY(id))插入纪录,供生成主键使用
INSERTINTOtb_generator(id,gen_name,gen_value)VALUES(1,PAYABLEMOENY_PK',1);在主键生成后,这条纪录的value值,按allocationSize递增。
//@TableGenerator的源码定义:@Target({TYPE,METHOD,FIELD})@Retention(RUNTIME)public@interfaceTableGenerator{Stringname();Stringtable()default"";Stringcatalog()default"";Stringschema()default"";StringpkColumnName()default"";StringvalueColumnName()default"";StringpkColumnValue()default"";intinitialValue()default0;intallocationSize()default50;UniqueConstraint[]uniqueConstraints()default{};}以上属性说明如下:
4、SEQUENCE用法
//使用示例@Id@GeneratedValue(strategy=GenerationType.SEQUENCE,generator="aaa")@SequenceGenerator(name="aaa",sequenceName="seq_payment")@SequenceGenerator源码定义:
@Target({TYPE,METHOD,FIELD})@Retention(RUNTIME)public@interfaceSequenceGenerator{Stringname();StringsequenceName()default"";intinitialValue()default0;intallocationSize()default50;}以上属性说明如下:
hibernate提供多种主键生成策略,有点是类似于JPA,基于Annotation的方式通过@GenericGenerator实现
以下是hibernate特有的:
对于这些hibernate主键生成策略和各自的具体生成器之间的关系,在org.hibernate.id.IdentifierGeneratorFactory中指定了:
static{GENERATORS.put("uuid",UUIDHexGenerator.class);GENERATORS.put("hilo",TableHiLoGenerator.class);GENERATORS.put("assigned",Assigned.class);GENERATORS.put("identity",IdentityGenerator.class);GENERATORS.put("select",SelectGenerator.class);GENERATORS.put("sequence",SequenceGenerator.class);GENERATORS.put("seqhilo",SequenceHiLoGenerator.class);GENERATORS.put("increment",IncrementGenerator.class);GENERATORS.put("foreign",ForeignGenerator.class);GENERATORS.put("guid",GUIDGenerator.class);GENERATORS.put("uuid.hex",UUIDHexGenerator.class);//uuid.hexisdeprecatedGENERATORS.put("sequence-identity",SequenceIdentityGenerator.class);}//上面十二种策略,加上native,hibernate一共默认支持十三种生成策略。
使用示例:
hibernate每种主键生成策略提供接口org.hibernate.id.IdentifierGenerator的实现类,如果要实现自定义的主键生成策略也必须实现此接口。
IdentifierGenerator提供一generate方法,generate方法返回产生的主键。
//源码展示publicinterfaceIdentifierGenerator{/***Theconfigurationparameterholdingtheentityname*/publicstaticfinalStringENTITY_NAME="entity_name";/***Generateanewidentifier.*@paramsession*@paramobjecttheentityortoplevelcollectionforwhichtheidisbeinggenerated**@returnanewidentifier*@throwsHibernateException*/publicSerializablegenerate(SessionImplementorsession,Objectobject)throwsHibernateException;}
由@GenericGenerator实现
@Target({PACKAGE,TYPE,METHOD,FIELD})@Retention(RUNTIME)public@interfaceGenericGenerator{/***uniquegeneratorname*/Stringname();/***GeneratorstrategyeitherapredefinedHibernate*strategyorafullyqualifiedclassname.*/Stringstrategy();/***Optionalgeneratorparameters*/Parameter[]parameters()default{};}以上属性说明如下:
通过@GenericGenerator自定义主键生成策略
@Query:自定义JPQL或原生Sql查询,摆脱命名查询的约束
@Query("selectufromUseruwhereu.firstname=:firstname")//JPQLUserfindByLastnameOrFirstname(@Param("lastname")Stringlastname);@Query(value="SELECT*FROMUSERSWHEREX=1",nativeQuery=true)//原生sqlUserfindByEmailAddress(StringX);关于@Query中参数的占位符:
@Modifying:DELETE和UPDATE操作必须加上@modifying注解,以通知SpringData这是一个DELETE或UPDATE操作。
@Transactional:UPDATE或者DELETE操作需要使用事务
@Async:异步操作
@NoRepositoryBean:避免Spring容器为此接口创建实例。可用于定义公共Repository
基本使用步骤
依赖
Entity类
@Data@Builder@AllArgsConstructor@NoArgsConstructor@Table(name="CUSTOMERS")@Entity@DynamicInsert@DynamicUpdatepublicclassCustomer{@Id@GeneratedValue(strategy=GenerationType.AUTO)@Column(name="id",insertable=false,updatable=false,length=32)privateIntegerid;@Column(name="name",nullable=false,length=10)privateStringname;@Column(name="age")privateIntegerage;@Temporal(TemporalType.TIMESTAMP)@CreationTimestamp@Column(name="create_date",columnDefinition="timestamp(6)")privateDatecreateDate;@Temporal(TemporalType.TIMESTAMP)@UpdateTimestamp@Column(name="update_date",columnDefinition="timestamp(6)")privateDateupdateTime;}Respository接口
//Customer为该respository对应的实体类,Long为实体类的主键的类型publicinterfaceCustomerRespositoryextendsJpaRespository
注解扫描
在SpringBoot中:
JPA和JDBC常用配置
在利用SpringBoot框架进行开发时,大部分服务避不开用数据库进行数据存储和使用。SpringBoot里面一般有两种方式进行数据表的创建和数据存储。
配置模板:
spring:datasource:#driver-class-name:com.mysql.jdbc.Driverdriver-class-name:com.mysql.cj.jdbc.Driverurl:jdbc:mysql://localhost:3306/testuseUnicode=true&characterEncoding=utf8&serverTimezone=UTCusername:rootpassword:roottype:com.alibaba.druid.pool.DruidDataSource#数据源配置schema:classpath:db/schema.sql#建表语句脚本的存放路径data:classpath:db/data.sql#数据库初始化数据的存放路径sql-script-encoding:UTF-8#设置脚本的编码jpa:database:mysql#配置数据库方言。使用JPA访问数据库时,必需配置。hibernate:ddl-auto:none#每次程序启动时的数据库初始化策略database-platform:org.hibernate.dialect.MySQL5InnoDBDialect#配置数据库引擎,不配置则默认为myisam引擎show-sql:true#日志打印执行的SQLproperties:hibernate:#show_sql:true#日志打印执行的SQL。与spring.jpa.show-sql配置效果相同,两者使用其一即可。format_sql:true#格式化sql语句配置说明:
spring.datasource.xxx
spring.datasource.driver-class-name:配置driver的类名,默认是从JDBCURL中自动探测
spring.datasource.url:配置数据库JDBC连接串
spring.datasource.username:配置数据库连接用户名
spring.datasource.password:配置数据库连接用户名对应的密码
spring.datasource.type:连接池配置
spring.datasource.schema:使用脚本创建表的语句的存放路径,classpath/db表示在工程的resource层级下的db目录中存放
spring.datasource.data:使用脚本初始化数据库数据的语句的存放路径
spring.datasource.sql-script-encoding:设置脚本的编码,默认常用设置为UTF-8
使用上述方式建表时,spring.jpa.hibernet.ddl-auto设置成none,否则有啥问题,我也没尝试过。这样配置可以避免两种方式一起使用
spring.jpa.xxx
spring.jpa.hibernet.ddl-auto值说明:
spring.jpa.database:配置数据库方言,使用JPA访问数据库时,必需配置。
spring.jpa.database-platform:配置数据库引擎。SpringBoot2.0后使用JPA、Hibernate来操作MySQL,Hibernate默认使用MyISM存储引擎而非InnoDB,前者不支持外键故会忽略外键定义。
使用JPA访问数据库的注意事项:
通过方法名来指定查询逻辑,而不需要自己实现查询的SQL逻辑,示例:List
方法名解析原理:
对方法名中除了保留字(findBy、top、within等)外的部分以and为分隔符提取出条件单词,然后解析条件获取各个单词并看是否和Entity中的属性对应(不区分大小写进行比较)。
注意:get/find与by之间的会被忽略,所以getNameById与getById是等价的,会根据id查出整个Entity而不会只查name字段(指定部分字段的查询见后面条目)。
查询条件解析原理:
假设School和Student是一对多关系,Student中有个Schoolschool字段、School有个StringaddressCode字段,以如下查询为例:
//查询student表StudetngetByNameAndSchoolAddressCode(StringstudentName,StringaddressCode)//JPA会自动生成条件studentName和关联条件student.school.addressCode进行查询//查询student表,推荐写法StudetngetByNameAndSchool_AddressCode(StringstudentName,StringaddressCode)
查询字段解析原理:
默认会查出Entity的所有字段且返回类型为该Entity类型,有两种情况可查询部分字段(除此外都会查出所有字段):
JPA集合类型查询参数
List
查询关键字
在查询时,通常需要同时根据多个属性进行查询,且查询的条件也格式各样(大于某个值、在某个范围等等),SpringDataJPA为此提供了一些表达条件查询的关键字,官方文档如下:
Example查询翻译过来叫”按例查询(QBE)”。是一种用户界面友好的查询技术。它允许动态创建查询,并且不需要编写包含字段名称的查询。而且按示例查询不需要使用特定的数据库的查询语言来编写查询语句。
优势:
劣势:
Example(动态实例)查询的原理
从生成的SQL语句可以看到,它的判断条件是根据实体的属性来生成查询语句的。如果实体的属性是null,它就会忽略它;如果不是,就会取其值作为匹配条件。
注意:如果一个字段是不是包装类型,而是基本类型,它也会参与where条件中,其值是默认值。所以在定义实体时,基本数据类型的字段应尽量使用包装类型。
@Testpublicvoidtest01(){Useruser=User.builder().name("Bob").build();Example
Example(动态实例)查询的概念定义介绍:
//示例:Example对象,由customer和matcher共同创建Example
自定匹配器规则
ExampleMatcher,不传时会使用默认的匹配器。
StringMatcher参数
说明:
总结
ExampleMatcher的使用:
实体对象中,基本数据类型无论是否传值,都会参与查询(因为有默认值),故应避免使用基本数据类型,采用包装器类型(默认值是null)。如果已经采用了基本类型,而这个属性查询时若不需要进行过滤,则需把它添加到忽略列表(ignoredPaths)中。
当条件值为null时,默认是忽略此过滤条件,一般业务也是采用这种方式就可满足。当需要查询数据库表中属性为null的记录时,可将值设为include,这时,对于不需要参与查询的属性,都必须添加到忽略列表(ignoredPaths)中,否则会出现查不到数据的情况(见上面实例test04)。
若属性值为null,默认忽略该过滤条件;若属性值为基本数据类型,默认参与查询,若需忽略,则需添加至则需把它添加到忽略列表(ignoredPaths)中。
默认创建匹配器时,字符串采用的是精确匹配、不忽略大小写,可以通过操作方法改变这种默认匹配,以满足大多数查询条件的需要,如将“字符串匹配方式”改为CONTAINING(包含,模糊匹配),这是比较常用的情况。对于个别属性需要特定的查询方式,可以通过配置“属性特定查询方式”来满足要求(见上面实例test02)。
忽略大小的生效与否,是依赖于数据库的。例如MySql数据库中,默认创建表结构时,字段是已经忽略大小写的,所以这个配置与否,都是忽略的。如果业务需要严格区分大小写,可以改变数据库表结构属性来实现,具体可百度(见上面实例test03)。
JPQL是专门为Java应用程序访问和导航实体实例设计的。JavaPresistenceQueryLanguage(JPQL),java持久性查询语言。它是JPA规范的重要组成部分,其实它就是一种查询语言,语法类似于SQL语法,但是有着本质的区别。
JPQL与SQL的区别
JPQL是面向对象的查询语言,因此它可以完全理解继承、多态和关联等特征。而且JPQL内置了大量函数,极大地方便了JPQL查询的功能。当然JPQL底层依然是基于SQL的,但JPQL到SQL的转换无须开发者关心,JPQL解析器会负责完成这种转换,并负责执行这种转换的SQL语句来更新数据库。
SQL是面向关系数据库的查询语言,因此SQL操作的对象是数据表、数据列;而JQPL操作的对象是实体对象,对象属性。
代码对比
//原生的SQL语句。对t_usertable表执行查询,查询name、age、user_id三个数据列selectname,age,user_idfromt_user//面向对象的JPQL语句。对User实体执行查询,查询的是User实体的name、age、userId属性selectname,age,userIdfromUser比较项SQLJPQL面向处理关系数据处理JPA实体关联实体的方式内连接、外连接、左连接、右连接内连接和左外连接支持的操作增(Insert)、删(Delete)改(Update)、查(Select)Delete(remove)Update(merge)、Select(find)
JPQL基本语法
select实体别名.属性名,实体别名.属性名……from实体名[as]实体别名where实体别名.实体属性op比较值使用@Query注解创建查询,将该注解标注在Repository的方法上,然后提供一个需要的JPQL语句即可,如:
@Query("selectpfromPersonpwherenamelike%1%")PersonfindByName(Stringname);JPQL查询时,可以使用SpEL表达式:#{#entityName}(取数据库实体名称)
好处是当修改类名后,不需要再单独去修改JPQL中的实体类名称
@Query("selectpfrom#{#entityName}pwherenamelike%1%")PersonfindByName(Stringname;SpEL表达式了解:
SpEL(SpringExpressionLanguage),即Spring表达式语言。它是一种类似JSP的EL表达式、但又比后者更为强大有用的表达式语言。SpEL表达式可以在spring容器内实时查询和操作数据,尤其是操作List列表型、Array数组型数据。所以使用SpEL可以有效缩减代码量,优化代码结构。
@Query注解查询时候,条件查询如何使用占位符:
(1)?+数字
若使用这种方式,则参数列表中参数的入参顺序必须与@Query注解当中标注的顺序相同
@Query("SELECTsfromStudentswheres.email=1ands.age=2")StudentfindStudentByEmailAndAge(Stringemail,Integerage);(2):+参数名称
这种方式可以自定义参数的名称。需要在参数列表当中用@Param注解标注参数名称。不用考虑顺序,是根据参数名称进行绑定
@Query("SELECTsfromStudentswheres.email=:emailands.age=:age")StudentfindStudentByEmailAndAge2(@Param("age")Integerage,@Param("email")Stringemail);
nativeQuery(原生SQL查询)
nativeQuery返回Entity
使用nativeQuery时SQL语句查询的字段名若没有取别名,则默认是数据库中的字段名(例如school_id),而API返回值通常是schoolId,可以在SQL里通过school_idasschoolId取别名返回。
然而若查询很多个字段值则得一个个通过as取别名,很麻烦,可以直接将返回值指定为数据库表对应的Entity,不过此法要求查询的是所有字段名,如:
排序查询
静态方式:直接在方法体现(如getByNameOrderByIdDesc),也可以在JPQL的@Query的逻辑中使用orderby进行排序
动态方式:可以在Repository的方法的最后加一个Sort或者Pageable类型的参数,便可动态生成排序或分页语句(编译后会自动在语句后加orderby或limit语句)
List
publicinterfaceUserRepositoryextendsJpaRepository
分页查询
动态方式:在Repository的方法的最后加一个Pageable类型的参数,便可动态生成分页语句(编译后会自动在语句后加limit语句)
查询一个表的部分字段,称为投影(Projection)
@Query(value="selectp.namefromPserSonpwherep.id=1")StringfindNameById(Stringid);@Query(value="selectnamefromPserSonwhereage=1")Set
1.使用自定义接口来映射结果集
直接通过方法名命名指定返回对象为包含部分字段getter方法的自定义接口(interface),只需要定义属性的getter方法,jdk动态代理封装数据。
注意:如果不用@Query则需要确保接口中getter方法名中的字段与Entity中的一致,而如果使用@Query则不需要,因为可以通过as取别名
/***Repository自定义查询方法*/List
2.使用自定义对象来接收结果集
返回对象为自定义的class,可定义toString方法,打印日志方便。
自定义的class须包含查询的字段的属性,且要封装的字段由公开的构造方法确定,对象属性不要求名称一致,只需要构造方法参数位置正确。
JPQL语法须为:selectnew+对象全限定类名
/***Repository自定义查询方法*/@Query(selectnewcom.xx.yy.PersonResult(p.id,p.name,p.age)fromPersonp)List
3.使用Map
注意:指定为Map时,实际类型是org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap,该类型只能读不能改或写
(1)nativeQuery查询,即原生SQL查询
直接select部分字段即可,结果集默认会自动包装为Map。
/***Repository自定义查询方法*/@Query(value="selectg.id,g.school_idasschoolId,g.namefromgradeg"+"leftjoinstudentsong.name=s.grade"+"whereg.school_id=(selecta.school_idfromadminawherea.id=1)"+"and(4isnullorg.namelike%4%org.bzlike%4%)"+"groupbyg.idlimit2,3",nativeQuery=true)List
可以手动指定包装为map,此时map的key为字段序号,故最通过as指定key为字段名。
默认会将结果包装为List而不是Map,可以手动指定包装为map,此时map的key为字段序号(0、1、2...),也可以通过as指定key为字段名。
/***Repository自定义查询方法*注意是'map',不是jdk中的'Map'!*/@Query("selectnewmap(g.nameasname,count(s.id)asstuCount)fromGradeEntityg,StudentEntitys"+"whereg.name=s.gradeandg.schoolId=1groupbyg.id")List
动态投影
/***Repository自定义查询方法*动态投影方式。泛型根据需要传入Entity实体类或封装部分字段的自定义接口*/publicinterfacePersonRepositoryextendsRepository
count查询
IntegercountByName(Stringname);
In查询
不管是否是@Query都可以用in查询,如:
原因:运行时动态生成sql语句,如果id列表为null或空列表,则最终生成的sql语句中"whereidinnull"不符合sql语法。
1、Entity内未定义关联实体时的联表查询,示例:
@Query("selectcdfromCourseDeveloperEntitycdjoinDeveloperdwhered.nickName='stdeveloper'")2、Entity内定义的关联实体的关联查询,示例:
@Query("selectcd,dfromCourseDeveloperEntitycdjoincd.developerdwhered.nickName='stdeveloper'")||(等价于)@Query("selectcd,cd.developerfromCourseDeveloperEntitycdwherecd.developer.nickName='stdeveloper'")若将一个对象的关联对象指定为延迟加载LAZY,则每次通过该对象访问关联对象时(如courseDeveloper.developer)都会执行一次SQL来查出被关联对象,显然如果被关联对象访问频繁则此时性能差。
解决:
延迟加载与立即加载(FetchType)
默认情况下,@OneToOne、@ManyToOne是LAZY,@OneToMany、@ManyToMany是EAGER。但不绝对,看具体需要。
有两个地方用到延迟加载:relationship(@OneToMany等)、attribute(@Basic)。后者一般少用,除非非常确定字段很少访问到。
Repository方法核心方法
//添加or修改数据Ssave(Sentity)底层逻辑为:当entity的id为null,则直接新增,不为null,则先select,如果数据库存在,则update。如果不存在,则insert
注意:若JPA启用了逻辑删除(软删除)功能,使用save方法则可能会出现主键冲突或唯一索引冲突等问题
原因:若数据库启用了逻辑删除功能,记录逻辑删除后,该条记录实际仍存在于数据库中,但是JPA根据Entity的主键查询数据库判断该执行insert还是update时,查询语句会自动加上逻辑删除的判断条件,从而查不到数据而最终执行insert,进而可能会导致报主键冲突或唯一索引冲突。
update
1、删除记录
Repository接口核心方法
voiddelete(Tentity)
Repository自定义删除方法
需要加@Modefying、@Transactional
也正因为这样,软删除功能中指定@SQLDelete("updatestudentsetis_delete='Y'whereid=")即可对所有非nativeQuery起作用。
方法名包含条件的删除操作(例如IntegerdeleteByNameAndSId(Stringname,Stringuuid);),其执行时与save类似,也是先根据条件查出目标Entity再执行删除操作。对于voiddelete(Tentity);,则直接根据Entity的主键操作而不用先查。
2、逻辑删除
使用org.hibernate.annotations(不是JPA的标准)的@Where、@SQLDelete、@SQLDeleteALL三个注解来实现。
//对非nativeQuery旳delete起作用,包括形如deleteByName等,下同。@SQLDelete(sql="update"+StudentEntity.tableName+"set"+constant.ISDELETE_COLUMN_NAME+"=truewheresid=")@SQLDeleteAll(sql="update"+StudentEntity.tableName+"set"+constant.ISDELETE_COLUMN_NAME+"=truewheresid=")//对非nativeQuery的select起作用(如count、非nativeQuery的StringmyGetNameByName等,前者本质上也是select)@Where(clause=constant.ISDELETE_COLUMN_NAME+"=false")@Data@Entity@Table(name=StudentEntity.tableName)publicclassStudentEntityextendsBaseEntity{publicstaticfinalStringtableName="student";...@Column(name=constant.ISDELETE_COLUMN_NAME,nullable=false)privateBooleanisDelete=false;}需要注意的是:
关于软删除:对于关联表(一对一、一对多、多对多),若要启用软删除,则须为多对多关联表定义额外的主键字段而不能使用联合外键作为主键,否则软删除场景下删除关联关系再重新关联时会主键冲突。另外,特殊情况下多对多关联表可以不启用软删除(被关联表、一对多或多对一关联表则需要,因为它们侧重的信息往往不在于关联关系而是重要的业务信息)
原生的saveAll()方法可以保证程序的正确性,但是如果数据量比较大时效率低。
源码逻辑原理是:for循环集合调用save方法;save方法逻辑为,当entity的id为null,则直接新增,不为null,则先select,如果数据库存在,则update。如果不存在,则insert。
@TransactionalpublicsaveAll(Iterableentities){Assert.notNull(entities,"Entitiesmustnotbenull!");Listresult=newArrayList();Iteratorvar3=entities.iterator();while(var3.hasNext()){Sentity=var3.next();result.add(this.save(entity));//save方法是核心逻辑}returnresult;}@Transactionalpublic
优化方案为:当保存大量数据时,直接使用em进行持久化插入,省了一步查询操作。并且考虑到如果最后才提交所有数据,数据库的负载可能会比较大,故每100条记录就提交(flush)一次。
@AutowiredprivateEntityManagerentityManager;privatefinalintBATCH_SIZE=1000;@Transactional(rollbackFor=Exception.class)publicvoidaddBatch(Listlist){intnum=0;for(Ss:list){entityManager.persist(s);//insert插入操作(变成托管状态)intnum+=1;if(i%BATCH_SIZE==0){entityManager.flush();//变成持久化状态entityManager.clear();//变成游离状态}}}在确保数据已经存在的情况下,如果是批量更新可以如下代码代替上面的entityManager.persist(projectApplyDO);语句:
entityManager.merge(projectApplyDO);//update更新操作
JPA事务内Entity变更会自动更新到数据库
若启用了事务,则对于managed状态的entity,若在事务内该entity有字段的值发生了变化,则即使未调save方法,该entity的变化最后也会被自动同步到数据库,即sqlupdate操作。即相当于在PersistContextflush时自动对各engity执行save方法。(org.hibernate.event.internal.AbstractFlushingEventListener中)
1.写一个与Repository接口同名的类,加上后缀Impl,标注@Repository注解;这个类不需要实现任何接口,可以自动被扫描到。
2.在Repository接口中加入自定义的方法,比如:
publicinterfaceMyRepositoryextendsJpaRespository
@RepositorypublicclassMyRepositoryImpl{@AutowiredprivateEntityManagerem;//实现在Repository接口中加入的自定义方法publicPage
Repository方式调用存储过程需要基于Entity实体类,在实体类上使用@NamedStoredProcedureQuery注解(需要数据库中有对应的表,可自动映射结果集)。详解HibernateEntityManger专题-JPA调用存储过程条目。
importjava.util.List;importorg.springframework.data.jpa.repository.JpaRepository;importorg.springframework.data.jpa.repository.query.Procedure;importorg.springframework.data.repository.query.Param;importorg.springframework.stereotype.Repository;importcom.labofjet.entity.A;importcom.labofjet.entity.APK;@RepositorypublicinterfaceARepositoryextendsJpaRepository{//方式1。若用这种方式,方法名要与存储过程名一样。【推荐】@ProcedureIntegerplus1inout(Integerarg);@ProcedureObject[]mytest();//方式2。Procedure的name为实体类上@NamedStoredProcedureQuery注解中name的值@Procedure(name="User.plus1")Integeralias2(@Param("arg")IntegerargAlias);//@Param必须匹配@StoredProcedureParameter注释的name参数//方式3。Procedure的procedureName参数必须匹配实体类上@NamedStoredProcedureQuery的procedureName的值@Procedure(procedureName="plus1inout")Integeralias3(Integerarg);}注意:返回类型必须匹配。in_only类型的存储过程返回是void,in_and_out类型的存储过程返回相应数据类型
springdatajpa提供了JpaSpecificationExecutor接口,只要简单实现toPredicate方法就可以实现复杂的动态查询。
Specification是Spring对Criteria的封装。
JpaSpecificationExecutor提供了以下接口
publicinterfaceJpaSpecificationExecutor
关于JPA2.0criteriaapi的介绍和使用,欢迎参考:
Repository继承JpaSpecificationExecutor接口
publicinterfaceTaskResposityextendsJpaRespository
调用
publicclassTaskSpec{//封装查询条件的静态方法publicstaticSpecification