Spring Boot简明教程之数据访问(二):JPA(超详细)
Spring Boot简明教程之数据访问(二):JPA(超详细)
文章目录
创建项目
创建的过程和我们的第一篇文章:SpringBoot简明教程之快速创建第一个SpringBoot应用大致相同,差别只是我们在挑选所需要的组件时,除了Web组件外,我们需要添加如下三个组件:JPA、MySQL、JDBC
或者,我们按照第一次创建完成后,手动在pom.xml文件中加入以下配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
我们发现,与我们直接使用JDBC template不同的是,这次我们引入了一个新的包:spring-boot-starter-data-jpa
,我们进一步查看,就会发现,其主要是引入了一个spring-data-jpa
的包。
Spring Data简介
Spring Data是为了简化构建基于 Spring 框架应用的数据访问技术,包括关系、非关系数据库、
云数据服务等等。SpringData为我们提供使用统一的API来对数据访问层进行操作,让我们在使用关系型或者非关系型数据访问技术时都基于Spring提供的统一标准,标准包含了CRUD(创建、获取、更新、删除)、查询、
排序和分页的相关操作,同时为我们提供了统一的数据访问类的模板,例如:MongoTemplate、RedisTemplate等。
JPA简介
JPA(Java Persistence API)定义了一系列对象持久化的标准,它的出现主要是为了简化现有的持久化开发工作和整合 ORM 技术,目前实现了这一规范的有 Hibernate、TopLink、JDO 等 ORM 框架。
Spring Data 与JPA
我们可以将Spring-data-jpa理解为Spring Boot对于JPA的再次封装,使得我们通过Spring-data-jpa即实现常用的数据库操作:
- JpaRepository实现基本功能: 编写接口继承JpaRepository既有crud及分页等基本功能
- 定义符合规范的方法命名: 在接口中只需要声明符合规范的方法,即拥有对应的功能
- 支持自定义查询: @Query自定义查询,定制查询SQL
- Specifications查询(Spring Data JPA支持JPA2.0的Criteria查询)
使用Spring Data JPA的基本流程
创建实体类(entity)
package cn.newtol.springboot07.entity;
import javax.persistence.*;
/**
* @Author: 公众号:Newtol
* @Description: JPA使用示例:使用JPA注解配置映射关系
* @Date: Created in 18:45 2018/9/24
*/
@Entity //表示一个实体类,和数据表进行映射
@Table(name = "t_user") //所映射的表的名字,可省略,默认为实体类名
public class User {
@Id //设置为主键
@GeneratedValue(strategy = GenerationType.IDENTITY) //定义为自增主键
private Integer id;
@Column(name = "t_name",nullable = false) //设置该字段在数据表中的列名,并设置该字段设置为不可为空
private String name;
@Column //默认则字段名就为属性名
private Integer phone;
@Transient //增加该注解,则在数据表中不会进行映射
private String address;
// //省略getter settet方法、构造方法,创建时自行补上
}
创建Dao层(Repository)
package cn.newtol.springboot07.repository;
import cn.newtol.springboot07.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @Author: 公众号:Newtol
* @Description:
* @Date: Created in 19:35 2018/9/24
*/
//定义为接口
public interface UserRepository extends JpaRepository<User,Integer>{
//直接继承即可,不用编写任何方法
}
编写配置文件
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/springboot?useSSL=false
username: root
password:
jpa:
hibernate:
ddl-auto: create
show-sql: true
创建Controller
package cn.newtol.springboot07.controller;
import cn.newtol.springboot07.entity.User;
import cn.newtol.springboot07.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;
/**
* @Author: 公众号:Newtol
* @Description:
* @Date: Created in 19:37 2018/9/24
*/
@RestController
public class UserController {
@Autowired
UserRepository userRepository;
//根据Id查询用户
@GetMapping("/user/{id}")
public User getUser(@PathVariable("id") Integer id){
User user = userRepository.getOne(id);
return user;
}
//插入用户
@GetMapping("/user")
public User addUser(User user){
userRepository.save(user);
return user; //返回插入的对象及其自增ID
}
}
直接启动项目,看到Spring Boot自动帮我们在数据库中自动创建了t_user表:
并且因为我们之前对address
属相使用了@Transient注解,不进行映射,所以我们看到表中没有address
字段。现在我们将@Transient注释后,再次启动项目:
address
就也被成功的创建。
接着,我们尝试着往数据库中插入一条数据,我们在浏览器中输入:localhost:8080/user?name=zhangsan&phone=123&address=beijing
再到数据库中进行查看:
插入成功。
现在我们来查询刚刚插入的数据,浏览器输入:http://localhost:8080/user/1
成功查询到刚才插入的数据,并且我们可以在控制台看到Spring Boot为我们打印的刚才执行的查询语句:
JPA常用注解说明
虽然我们在上面的示例中已经使用了常用了的注解,但是为了方便和理解,我们在这个地方归纳一下关于JPA的一些常用的注解及其对应的参数。
-
@Entity:表示该类是一个的实体类。@Entity标注是必需的 ,name属性为可选;需要注意的是:@Entity标注的实体类至少需要有一个无参的构造方法。
-
@Table:表示该实体类所映射的数据表信息,需要标注在类名前,不能标注在方法或属性前。参数如下:
参数 说明 name 实体所对应表的名称,默认表名为实体名 catalog 实体指定的目录名 schema 表示实体指定的数据库名 uniqueConstraints 该实体所关联的唯一约束条件,一个实体可以有多个唯一的约束,默认没有约束条件。创建方式:uniqueConstraints = {@uniqueConstraint(columnNames = {“name”,“phone”})} indexes 该实体所关联的索引。创建方式:indexes = { @Index(name = “index_name”, columnList = “t_name”)} -
@Column:表示该属性所映射的字段,此标记可以标注在Getter方法或属性前。它有如下参数:
参数 说明 unique 字段是否为唯一标识,默认为false nullable 字段是否可以为null值,默认为true insertable 使用“INSERT” SQL语脚本插入数据时,是否需要插入该字段的值。 updatable 使用“UPDATE”脚本插入数据时,是否需要更新该字段的值 table 当映射多个表时,指定表中的字段。默认值为主表的表名。 length 当字段的类型为varchar时的字段的长度,默认为255个字符。 precision 用于表示精度,数值的总长度 scale 用于表示精度,小数点后的位数。 columnDefinition 用于创建表时添加该字段所需要另外执行的SQL语句 -
@Id:表示该属性为主键,每一个实体类至少要有一个主键(Primary key)。
参数 说明 strategy 表示生成主键的策略 ,有4种类型:GenerationType.TABLE 、 GenerationType.SEQUENCE 、 GenerationType.IDENTITY 、 GenerationType.AUTO 默认为:AUTO,表示自动生成。 generator 生成规则名,不同的策略有不同的配置 -
@Basic: 实体属性设置加载方式为惰性加载
参数 说明 fetch 表示获取值的方式,它的值定义的枚举型,可选值为LAZY(惰性加载)、EAGER(即时加载),默认为即时加载。 optional 属性是否可以为null,不能用于java基本数据型( byte、int、short、long、boolean、char、float、double )
自定义查询
我们刚刚在上面使用的是JPA已经封装好的一些默认的查询方式,但是我们在实际的项目中,可能需要自定义一些符合实际需求的查询,那么我们就需要用到自定义查询。
关键字查询
JPA已经帮我们做好了关键字,我们只需要将这些关键字组成方法名,就可以实现相对应的查询。
关键字 | 示例 | 同功能JPQL |
---|---|---|
And |
findByLastnameAndFirstname |
where x.lastname = ?1 and x.firstname = ?2 |
Or |
findByLastnameOrFirstname |
where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals |
findByFirstname,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 (参数前面加 %) |
EndingWith |
findByFirstnameEndingWith |
where x.firstname like ?1 (参数后面加 %) |
Containing |
findByFirstnameContaining |
where x.firstname like ?1 (参数两边加 %) |
OrderBy |
findByAgeOrderByLastnameDesc |
where x.age = ?1 order by x.lastname desc |
Not |
findByLastnameNot |
where x.lastname <> ?1 |
In |
findByAgeIn(Collection<Age> ages) |
where x.age in ?1 |
NotIn |
findByAgeNotIn(Collection<Age> age) |
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) |
我们除了使用find
关键字以外,还可以使用count
、delete
、get
等。
例如,我们在Controller中加入如下方法得:
/**
* @Author: 公众号:Newtol
* @Description: 自定义关键字查询
* @Date: Created in 19:37 2018/9/24
*/
@GetMapping("/test/{address}/{phone}")
public List<User> getUserByAddressAndPhone (@PathVariable("address") String address, @PathVariable("phone") Integer phone){
return userRepository.findByAddressEqualsAndPhoneNot(address,phone);
}
在userRepository中鸡加入如下方法:
/**
* @Author: 公众号:Newtol
* @Description:
* @Date: Created in 19:35 2018/9/24
*/
public interface UserRepository extends JpaRepository<User,Integer>{
public List<User> findByAddressEqualsAndPhoneNot (String address, Integer phone);
}
在数据库中插入下面两条数据:
INSERT INTO `t_user` VALUES ('2', 'beijing', 'zhangsi', '456');
INSERT INTO `t_user` VALUES ('3', 'beijing', 'wangwu', '123');
在浏览器输入:http://localhost:8080/test/beijing/456
我们就可以看到返回了如下数据,查询成功。
自定义SQL语句
通常如果使用JPA提供给我们的关键字组成的查询方法仍然无法满足需求,那么我们就可以使用@Query
来实现自定的SQL语句。
原生SQL
JPA中支持使用原生的SQL语句,例如:
在userRepository中加入下面的方法:
//自定义SQL查询
@Query(value = "select * from t_user WHERE phone = ? " ,nativeQuery = true)
List<User> getAllUser (Integer phone);
在controller中加入以下方法:
//自定义SQL查询
@GetMapping("/test/{phone}")
public List<User> getAllUser(@PathVariable("phone") Integer phone){
return userRepository.getAllUser(phone);
}
在浏览器中输入:http://localhost:8080/test/123
返回数据,查询成功。
但是,如果我们需要执行UPDATE
、DELETE
操作时,除了使用@Query
外,还需要使用@Modifying
、
@Transactional
两个注解。
例如:
在userRepository中加入下面的方法:
//自定义SQL删除
@Query(value = "delete from t_user WHERE phone = ? ",nativeQuery = true)
@Modifying
@Transactional
void deleteUser(Integer phone);
在controller中加入以下方法:
//自定义SQL删除
@GetMapping("/del/{phone}")
public void deleteUser(@PathVariable("phone") Integer phone){
userRepository.deleteUser(phone);
}
在浏览器中输入:http://localhost:8080/del/456
,我们就会发现数据库中phone为456的数据就已经被删除了。
JPQL查询
在userRepository中加入下面的方法:
//JPQL查询
@Query("select u from User u where u.name = ?1")
User getUserByName(String name);
在controller中加入以下方法:
//JPQL查询
@GetMapping("/get/{name}")
public User getUserByName(@PathVariable("name") String name){
return userRepository.getUserByName(name);
}
浏览器输入:http://localhost:8080/get/zhangsan
,数据返回,查询成功:
复杂查询
分页查询
我们经常会遇到进行分页查询的情况,而JPA就很好的替我们解决了这一个问题。只需要几行代码就可以解决:
例如:在controller中加入以下方法:
//分页查询
@GetMapping("/userList/{page}/{size}")
public Page<User> getUserList(@PathVariable("page") Integer page, @PathVariable("size") Integer size) {
Sort sort = new Sort(Sort.Direction.DESC,"id");
PageRequest pageRequest = PageRequest.of(page,size,sort);
return userRepository.findAll(pageRequest);
}
浏览器输入:http://localhost:8080/userList/0/3
返回的数据格式为:
{
"content": [
{
"id": 3,
"name": "wangwu",
"phone": 123,
"address": "beijing"
},
{
"id": 2,
"name": "lisi",
"phone": 789,
"address": "shanghai"
},
{
"id": 1,
"name": "zhangsan",
"phone": 123,
"address": "beijing"
}
],
"pageable": {
"sort": {
"sorted": true,
"unsorted": false
},
"offset": 0,
"pageSize": 3,
"pageNumber": 0,
"paged": true,
"unpaged": false
},
"totalPages": 1,
"last": true,
"totalElements": 3,
"number": 0,
"size": 3,
"sort": {
"sorted": true,
"unsorted": false
},
"numberOfElements": 3,
"first": true
}
限制查询
除了限制查询以外,对于排行榜类的数据,或许我们只需要取出前面几名即可,Jpa也对这种情况做了很好的支持:
例如:在userRepository中加入下面的方法:
//限制查询
List<User> findFirst2ByAddressOrderByIdDesc (String address);
在controller中加入以下方法:
@GetMapping("/top/{address}")
public List<User> getTop2User(@PathVariable("address") String address){
return userRepository.findFirst2ByAddressOrderByIdDesc(address);
}
在浏览器输入:http://localhost:8080/top/beijing
返回的数据为:
[{"id":3,"name":"wangwu","phone":123,"address":"beijing"},{"id":1,"name":"zhangsan","phone":123,"address":"beijing"}]
常见问题及其解决方案:
-
如果提示:No identifier specified for entity: xxx
解决方案:这是因为在创建实体类时,导入的jar包错误导致的。我们将
import org.springframework.data.annotation.*;
更改为:import javax.persistence.*;
即可 -
如果提示: No default constructor for entity: :
解决方案:这是因为在实体类中创建了含参数的构造方法造成的,这是因为在使用类反射机制 Class.newInstance()方法创建实例时,需要有一个默认的无参数构造方法,否则会执出实例化异常(InstantiationException),所以将构造方法更改为无参数即可。
-
如果提示:Caused by: org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException: Executing an update/delete query
解决方案:这是因为在JPA中执行DELETE/UPDATE操作时,需要使用
@Modifying
、
@Transactional
两个注解,补上即可。 -
如果提示:Can not issue data manipulation statements with executeQuery()。
解决方案:如果提示这个错误,就需要查询检查自己的SQL语句是否书写正确,如果正确,检查是否加上
@Modifying
、@Transactional
两个注解 -
如果提示:Ambiguous handler methods mapped for HTTP path
解决方案:这是由于URL映射重复引起的,将你所请求的URL重新进行定义即可。
-
如果在使用PageRequest方法显示过期
解决方案:将构造方法更改为:PageRequest.of即可。在2.0版本以后,就使用of(…) 方法代替 PageRequest(…)构造器。
总结
我们首先介绍了spring Data
,然后演示了一遍使用JPA的基本流程,最后详细的介绍了我们主要使用的几个注解、注意事项等;以及除了使用默认的查询方式外,如何去使用自定义查询和复杂查询。