Spring Boot技术栈(Spring Data JPA)
本篇介绍 通过Spring Data JPA对数据库进行常见的操作:删除、修改、添加、条件查询、分页查询、单表查询、多表查询。
1.Spring Data JPA介绍
- Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套 JPA 应用框架,可使开发者用极简的代码即可实现对数据的访问和操作。它提供了包括增删改查等在内的常用功能,且易于扩展!学习并使用 Spring Data JPA 可以极大提高开发效率!
- Spring Data JPA 让我们解脱了 DAO 层的操作,基本上所有 CRUD 都可以依赖于它来实现。
2.开发环境搭建
1.添加依赖
1 | <dependency> |
2.修改application.properties
添加配置
1 | #数据库连接URL |
配置文件说明
- spring.jpa.properties.hibernate.hbm2ddl.auto : 自动创建 | 更新 | 验证数据库表结构
- create : 每次加载hibernate,如果数据库中存在表,将所有表删除,然后重新生成表
- update 每次hibernate 时根据 model 类自动更新表结构,如果是第一次则创建表结构。之前表数据不会删除
- validate : 设置为validate:加载hibernate时,验证创建数据库表结构,这样 spring在加载之初,如果model层和数据库表结构不同,就会报错,这样有助于技术运维预先发现问题。例如:ProductInfoEntity这个实体有property1这个属性,而对应的数据库表product没有property1这个字段,就会在tomcat启动的时候报错:错误可能如下:Missing column: property1 in wjs.product
- create-drop : 如果一开始数据库没有表,启动tomcat的时候会生成表,当把tomcat关闭之后生成的表又会消除
- spring.jpa.properties.hibernate.dialect : 告诉Hibernate,将HQL翻译成哪种数据库的SQL,常见的有
- mysql : org.hibernate.dialect.MySQLDialect
- oracle : org.hibernate.dialect.OracleDialect
- sqlserver : org.hibernate.dialect.SQLServerDialect
3.创建实体类
- 注意:
- 实体类在命名时候不要用数据库中的关键字比如Order,可以定义成Orders
- Entity 中不映射成列的字段得加 @Transient 注解,不加注解也会映射成列
- 实体类在命名时候不要用数据库中的关键字比如Order,可以定义成Orders
1 | /** |
4.创建DAO
Dao 只要继承 JpaRepository 类就可以,几乎可以不用写方法,还有一个特别有个性的功能非常赞,就是可以根据方法名来自动的生产 SQL,如 findByUserName 会自动生产一个以 userName 为参数的查询方法,如 findAll 自动会查询表里面的所有数据,如自动分页等等
1 | public interface AccountRepository extends JpaRepositoryAccount,Long { |
- 说明:JpaRepository<T,ID>这个接口只是一个空的接口,目的是为了统一所有Repository的类型,其接口类型使用了泛型,泛型参数中T代表实体类型,ID则是实体中id的类型
5.编写测试代码
1 | @RunWith(SpringRunner.class) |
6.运行时报错及解决方案
可能出现异常:
- Caused by: java.lang.IllegalArgumentException: Not a managed type: class com.nsun.study.dao.model.UserInfo
at org.hibernate.jpa.internal.metamodel.MetamodelImpl.managedType(MetamodelImpl.java:210) - Consider defining a bean of type ‘*Repository’ in your configuration.
原因:启动时没有扫描实体类和DAO,导致没有注入
解决方案:在Application 增加注解(测试类也添加)
1 | @SpringBootApplication |
3.常见数据库操作
基本查询也分为两种,一种是 Spring Data 默认已经实现,一种是根据查询的方法来自动解析成 SQL。
1.基本查询
Spring Data JPA 默认预先生成了一些基本的 CURD 的方法,如增加,删除,修改,查询
继承 JpaRepository:
1 | public interface AccountRepository extends JpaRepository<Account,Long> { |
测试
1 | @RunWith(SpringRunner.class) |
2.自定义简单查询
自定义的简单查询就是根据方法名来自动生成 SQL,主要的语法是 findXXBy、readAXXBy、queryXXBy、countXXBy、getXXBy 后面跟属性名称:
1 | User findByUsername(String userName); |
也可以加一些关键字 And、Or:
1 | User findByUsernameOrAddress(String username, String address); |
修改、删除、统计也是类似语法:
1 | Long deleteById(Long id); |
基本上 SQL 体系中的关键词都可以使用,如 LIKE、IgnoreCase、OrderBy。
1 | List<User> findByAddressLike(String address); |
具体的关键字,使用方法和生产成 SQL 如下表所示:
Keyword | Sample | JPQL snippet |
---|---|---|
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals | findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age ⇐ ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull | findByAgeIsNull | … where x.age is null |
IsNotNull,NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection |
… where x.age in ?1 |
NotIn | findByAgeNotIn(Collection |
… where x.age not in ?1 |
TRUE | findByActiveTrue() | … where x.active = true |
FALSE | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
3.分页查询
分页查询在实际使用中非常普遍了,Spring Data JPA 已经帮我们实现了分页的功能,在查询的方法中,需要传入参数 Pageable,当查询中有多个参数的时候 Pageable 建议做为最后一个参数传入:
在AccountRepository类中添加
1 | PageAccount findAllByAddressLike(String address, Pageable pageable); |
测试代码
1 | @Test |
输出结果
1 | 一共有2页 |
4.查询前N条数据
有时候我们只需要查询前 N 个元素,或者只取前一个实体。可以在定义接口的时候使用firstBy,topBy,first4By,top4By
1.在AccountRepository接口中添加新方法
1 | /**查询前三个*/ |
2.测试代码
@Test
public void testTop(){
accountRepository.deleteAll();
accountRepository.save(new Account("张三丰","123456","男","山西太原",5000.5));
accountRepository.save(new Account("张四丰","123456","男","山西临汾",1000));
accountRepository.save(new Account("张五丰","123456","男","山西忻州",1000));
accountRepository.save(new Account("小明","123456","女","河北承德",3000));
accountRepository.save(new Account("小丽","123456","女","河北保定",9000.5));
//查询前3条记录
List<Account> accounts = accountRepository.findTop3ByOrderByBalanceDesc();
for (Account account: accounts) {
System.out.println(account.getBalance());
}
/**查询余额最高的*/
Account firstAccount = accountRepository.findFirstByOrderByBalanceDesc();
System.out.println(firstAccount);
/**查询余额最高的*/
Account topAccount = accountRepository.findTopByOrderByBalanceDesc();
System.out.println(topAccount);
}
5.自定义 SQL 查询
Spring Data JPA 也可以完美支持自定义SQL语句操作:
1.在 SQL 的查询方法上面使用 @Query 注解。
2.1如果需要传参可以在SQL中通过”:变量名”设置占位符,然后配合注解@Param(“变量名”)一起使用
2.2也可以使用?占位符,但是后面要跟1表示第一个参数如果多个问号只需要在每个?后面跟对应的数字就行了,从1开始
3.特别注意这里的 SQL 是 HQL,需要写类的名和属性,这块很容易出错。
1.在AccountRepository接口中添加新方法(使用 “:变量名” 的方式)
1 | @Query("select a from Account a where a.username = :name") |
2.在AccountRepository接口中添加新方法(使用 “?” 的方式)
1 | @Query("select a from Account a where a.address like ?1") |
3.测试代码
1 | @Test |
6.自定义SQL 更新或删除数据
提示:更新或者删除的时候需要添加@Modifying否则报 Not supported for DML operations 异常,也需要添加@Transactional事物注解,否则报InvalidDataAccessApiUsageException异常
1.在AccountRepository接口中添加方法
1 | @Query("update Account a set password = :password where username=:username") |
2.测试
1 | @Test |
3.控制台结果
1 | 更新了1行数据 |
7.一对多关系(oneToMany)
以班级和学生为例
班级和学生,一个个班级对应多个学生,班级就是一的一方,学生就是多的一方
需要在代表一的地方添加@OneToMany和@JoinColumn来标注(原理是在多的一方的表中增加一个外键列来保存关系)。代表多的实体不需要使用任何映射标注。
###班级类(Classes)
班级代码 代表一的一方
1 | @Entity |
###学生类(Student)
学生代码 代表多的一方
1 | @Entity |
测试代码
测试代码:保存班级的同时保存学生ClassesRepository和StudentRepository此处省略..直接继承JpaRepository即可
1 | @Test |
生成表的SQL
1 | Hibernate: create table classes (id integer not null auto_increment, name varchar(255), primary key (id)) |
插入数据的SQL
1 | Hibernate: insert into classes (name) values (?) |
8.多对一关系(ManyToOne)
继续修改刚才的代码,在多的一方添加@ManyToOne
这里只需要在Student类中添加属性private Classes classes;生成get/set方法即可,并且在该属性上添加@ManyToOne(cascade={CascadeType.ALL})
CascadeType.ALL表示级联删除,级联更新,级联新建,级联新建,比如删除主表,则从表也随之删除,详情见附录
###学生类(Student)
修改学生代码
1 | @Entity |
###测试代码
测试代码:查询所有学生对应的班级
1 | @Test |
###可能遇到的异常
在controller返回数据到统一json转换的时候,出现了json infinite recursion stackoverflowerror的错误
具体的情况如下:
Classes类中,有个属性:private List
解决方法:
在Student类中classes的setter方法上加注解@JsonBackReference
9.多对多关系(ManyToMany)
以书和作者为例:
书(book)和作者(publisher)的可以看作多对多的关系,多对多关系需要一张中间表(book_publisher)来维护关系
关系图:
###书类(Book)
@JoinTable会生成第三张中间表,表名叫book_publisher,joinColumns表示当前实体(Book)的主键,inverseJoinColumns表示另一个实体(Publisher)的主键
1 | /** |
###作者类(Publisher)
mappedBy = “publishers”表示关系由另一个实体(Book)维护,mappedBy 的值是Book类中的Set
属性的名字
1 | /** |
测试代码
BookRepository和PublisherRepository直接继承JpaRepository即可
1 | /** |
##10.一对一关系
以人和身份证为例,一个人(Person)对应一个(IDCard),关系由人(Person)来维护
人类(Person)
因为关系由Person维护,所以这里使用@JoinColumn声明person表中外键的名字(外建名=另一个表的表名+”_”+另一个表的主键名)
1 | import javax.persistence.*; |
###身份证类(IDCard)
mappedBy = “idCard”表示关系由另一个实体(Person)维护,mappedBy 的值是Person类中的IDCard属性的名字
1 | import javax.persistence.*; |
测试类
PersonRepository和IDCardRepository直接继承JpaRepository
1 | @RunWith(SpringRunner.class) |
附录
为方便阅读,下面是本文涉及到的注解的相关说明,在阅读文章时对注解如有问题可以查阅此附录
##@ManyToMany注解
BookRepository extends JpaRepository是属性或方法级别的注解,用于定义源实体与目标实体是多对多的关系。
参数 | 类型 | 描述 |
---|---|---|
targetEntity | Class | 源实体关联的目标实体类型,默认是该成员属性对应的集合类型的泛型的参数化类型。 |
mappedBy | String | 用在双向关联中。如果关系是双向的,则需定义此参数(与 @JoinColumn互斥,如果标注了 @JoinColumn注解,不需要再定义此参数)。 |
cascade | CascadeType[] | 定义源实体和关联的目标实体间的级联关系。当对源实体进行操作时,是否对关联的目标实体也做相同的操作。默认没有级联操作。该参数的可选值有:CascadeType.PERSIST(级联新建)CascadeType.REMOVE(级联删除)CascadeType.REFRESH(级联刷新)CascadeType.MERGE(级联更新)CascadeType.ALL(包含以上四项) |
fetch | FetchType | 定义关联的目标实体的数据的加载方式。可选值:FetchType.LAZY(延迟加载,默认)FetchType.EAGER(立即加载)延迟加载:只有在第一次访问源实体关联的目标实体的时候才去加载。立即加载:在加载源实体数据的时候同时去加载好关联的目标实体的数据。 |
@OneToOne注解
是属性或方法级别的注解,用于定义源实体与目标实体是一对一的关系。
参数 | 类型 | 描述 |
---|---|---|
targetEntity | Class | 源实体关联的目标实体类型,默认是该成员属性对应的类型,因此该参数通常可以缺省。 |
mappedBy | String | 用在双向关联中。如果关系是双向的,只能有一方作为主体端,另一方则需声明此参数以表明将表间的这种关联关系转交给对方来维护。 |
cascade | CascadeType[] | 定义源实体和关联的目标实体间的级联关系。当对源实体进行操作时,是否对关联的目标实体也做相同的操作。默认没有级联操作。该参数的可选值有:CascadeType.PERSIST(级联新建)CascadeType.REMOVE(级联删除)CascadeType.REFRESH(级联刷新)CascadeType.MERGE(级联更新)CascadeType.ALL(包含以上四项) |
fetch | FetchType | 定义关联的目标实体的数据的加载方式。可选值:FetchType.LAZY(延迟加载)FetchType.EAGER(立即加载,默认)延迟加载:只有在第一次访问源实体关联的目标实体的时候才去加载。立即加载:在加载源实体数据的时候同时去加载好关联的目标实体的数据。 |
optional | boolean | 源实体关联的目标实体是否允许为 null,默认为 true。 |
orphanRemoval | boolean | 当源实体关联的目标实体被断开(如给该属性赋予另外一个实例,或该属性的值被设为 null。被断开的实例称为孤值,因为已经找不到任何一个实例与之发生关联)时,是否自动删除断开的实例(在数据库中表现为删除表示该实例的行记录),默认为 false。 |
@OneToMany注解
@OneToMany 是属性或方法级别的注解,用于定义源实体与目标实体是一对多的关系。
参数 | 类型 | 描述 |
---|---|---|
targetEntity | Class | 源实体关联的目标实体类型,默认是该成员属性对应的集合类型的泛型的参数化类型。 |
mappedBy | String | 用在双向关联中。如果关系是双向的,则需定义此参数(与 @JoinColumn 互斥,如果标注了 @JoinColumn注解,不需要再定义此参数)。 |
cascade | CascadeType[] | 定义源实体和关联的目标实体间的级联关系。当对源实体进行操作时,是否对关联的目标实体也做相同的操作。默认没有级联操作。该参数的可选值有:CascadeType.PERSIST(级联新建)CascadeType.REMOVE(级联删除)CascadeType.REFRESH(级联刷新)CascadeType.MERGE(级联更新)CascadeType.ALL(包含以上四项) |
fetch | FetchType | 定义关联的目标实体的数据的加载方式。可选值:FetchType.LAZY(延迟加载,默认)FetchType.EAGER(立即加载)延迟加载:只有在第一次访问源实体关联的目标实体的时候才去加载。立即加载:在加载源实体数据的时候同时去加载好关联的目标实体的数据。 |
orphanRemoval | boolean | 当源实体关联的目标实体被断开(如给该属性赋予另外一个实例,或该属性的值被设为 null。被断开的实例称为孤值,因为已经找不到任何一个实例与之发生关联)时,是否自动删除断开的实例(在数据库中表现为删除表示该实例的行记录),默认为 false。(如果用CascadeType.REMOVE设置则必须要调用delete()方法才会删除) |
@ManyToOne注解
@ManyToOne 是属性或方法级别的注解,用于定义源实体与目标实体是多对一的关系。
参数 | 类型 | 描述 |
---|---|---|
targetEntity | Class | 源实体关联的目标实体类型,默认是该成员属性对应的类型,因此该参数通常可以缺省。 |
cascade | CascadeType[] | 定义源实体和关联的目标实体间的级联关系。当对源实体进行操作时,是否对关联的目标实体也做相同的操作。默认没有级联操作。该参数的可选值有:CascadeType.PERSIST(级联新建)CascadeType.REMOVE(级联删除)CascadeType.REFRESH(级联刷新)CascadeType.MERGE(级联更新)CascadeType.ALL(包含以上四项) |
fetch | FetchType | 定义关联的目标实体的数据的加载方式。可选值:FetchType.LAZY(延迟加载)FetchType.EAGER(立即加载,默认)延迟加载:只有在第一次访问源实体关联的目标实体的时候才去加载。立即加载:在加载源实体数据的时候同时去加载好关联的目标实体的数据。 |
optional | boolean | 源实体关联的目标实体是否允许为 null,默认为 true。 |
@JoinTable注解
与 @Table 注解相类似,不同的是,@JoinTable 注解是用于定义关联表,它只能标注在实体类型的成员属性或方法上,常用于多对多或多对一的关联映射。如果没有声明,则使用该注解的默认值。
多对一时慎用,因为会生成第三张表
参数 | 类型 | 描述 |
---|---|---|
name | String | 连接表的名称。 |
catalog | String | 默认为数据库系统缺省的 catalog。 |
schema | String | 默认为用户缺省的 schema。 |
joinColumns | JoinColumn[] | 连接表中的外键列,通过使用 @JoinColumn 注解来声明,该外键参照源实体的主键。 |
inverseJoinColumns | JoinColumn[] | 与 joinColumns 参数作用类似,只不过该外键参照的是目标实体的主键。 |
uniqueConstraints | UniqueConstraint[] | 表的唯一约束(除了由 @Column 和 @JoinColumn注解指定的约束以及主键的约束之外的约束),通过使用 @UniqueConstraint注解来声明,仅在允许自动更新数据库表结构的场景中起到作用,默认没有其他额外的约束条件。 |
indexes | Index[] | 表的索引,通过使用 @Index注解来声明,仅在允许自动更新数据库表结构的场景中起到作用,默认没有其他额外的索引。 |
foreignKey | ForeignKey | 用于生成表时定义 joinColumns 参数的外键约束。 |
inverseForeignKey | ForeignKey | 用于生成表时定义 inverseJoinColumns 参数的外键约束。 |
##@JoinColumn注解
与 @Column 注解相类似,不同的是,@JoinColumn 注解是用于定义外键列,它只能标注在实体类型的成员属性或方法上,如果没有声明,则使用该注解的默认值。与 @Column 注解相类似,不同的是,@JoinColumn 注解是用于定义外键列,它只能标注在实体类型的成员属性或方法上,如果没有声明,则使用该注解的默认值。
参数 | 类型 | 描述 |
---|---|---|
name | String | 外键列的名称,默认为:属性的名称 + _ + 属性对应的实体的主键列的名称 (Hibernate 映射列时,若遇到驼峰拼写,会自动添加 _ 连接并将大写字母改成小写)。 |
unique | boolean | 外键列的值是否是唯一的。这是 @UniqueConstraint 注解的一个快捷方式,实质上是在声明唯一约束。默认值为 false。 |
nullable | boolean | 外键列的值是否允许为 null。默认为 true。 |
insertable | boolean | 外键列是否包含在 INSERT 语句中,默认为 true。 |
updatable | boolean | 外键列是否包含在 UPDATE 语句中,默认为 true。 |
columnDefinition | String | 生成外键列的 DDL 时使用的 SQL 片段。默认使用推断的类型来生成 SQL 片段以创建此列。 |
table | String | 外键列所属的表的名称。默认值:如果是外键 @OneToOne 或 @ManyToOne 关联,则为源实体的表的名称;如果是单向外键 @OneToMany 关系,则为目标实体的表的名称;如果是 @ManyToMany 、@OneToOne 、双向 @ManyToOne 、双向 @OneToMany 关联,则为连接表的名称; |