SpringBoot中使用JPA
Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套 JPA 应用框架,底层使用了 Hibernate 的 JPA 技术实现,可使开发者用极简的代码即可实现对数据的访问和操作。它提供了包括增删改查等在内的常用功能,且易于扩展!学习并使用SpringDataJPA可以极大提高开发效率!让我们解脱DAO层的操作,基本上所有CRUD都可以依赖于它来实现
本文将介绍如何在SpringBoot中的使用案例和原理分析
一、SpringBoot使用
1、导入依赖
swagger和common为附加包,不使用的话可以不需要导的哈
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>cn.gjing</groupId>
<artifactId>tools-starter-swagger</artifactId>
<version>1.0.9</version>
</dependency>
<dependency>
<groupId>cn.gjing</groupId>
<artifactId>tools-common</artifactId>
<version>1.0.4</version>
</dependency>
2、配置文件
server:
port: 8082
spring:
application:
name: jpa-demo
datasource:
url: jdbc:mysql://127.0.0.1:3306/jpa_demo?characterEncoding=utf8&useSSL=false&serverTimezone=UTC
password: root
username: root
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum-idle: 5
maximum-pool-size: 10
idle-timeout: 30000
connection-timeout: 20000
jpa:
# 是否打印sql
show-sql: false
hibernate:
# 开启自动建表功能,一般选update,每次启动会对比实体和数据表结构是否相同,不相同会更新
ddl-auto: update
# 设置创表引擎为Innodb,不然默认为MyiSam
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
2、创建实体类
/**
* @author Gjing
**/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "user")
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_name", columnDefinition = "varchar(10) not null comment '用户名'")
private String userName;
@Column(name = "user_age", columnDefinition = "tinyint(1) not null comment '年龄'")
private Integer userAge;
@Column(name = "user_phone", columnDefinition = "varchar(11) not null comment '手机号'")
private String userPhone;
@Column(name = "create_time", columnDefinition = "datetime")
@CreatedDate
private Date createTime;
@Column(name = "update_time", columnDefinition = "datetime")
@LastModifiedDate
private Date updateTime;
}
3、定义Dao层
/**
* @author Gjing
**/
@Repository
public interface UserRepository extends JpaRepository<User,Long> {
/**
* 通过手机号查询
* @param userPhone 手机号
* @return user
*/
User findByUserPhone(String userPhone);
/**
* 分页查询
* @param pageable 分页对象
* @return Page<User>
*/
@Override
Page<User> findAll(Pageable pageable);
}
4、定义service层
/**
* @author Gjing
**/
@Service
public class UserService {
@Resource
private UserRepository userRepository;
/**
* 保存用户
* @param userDto 用户传输对象
* @return t / f
*/
public boolean saveUser(UserDto userDto) {
User userDb = userRepository.findByUserPhone(userDto.getUserPhone());
if (userDb == null) {
User user = userRepository.saveAndFlush(User.builder().userName(userDto.getUserName())
.userAge(userDto.getUserAge())
.userPhone(userDto.getUserPhone())
.build());
return user != null;
}
return false;
}
/**
* 分页查询用户列表
* @param pageable 分页条件
* @return PageResult
*/
public PageResult listForUser(Pageable pageable) {
List<Map<String, Object>> data = new LinkedList<>();
Page<User> userPage = userRepository.findAll(pageable);
userPage.getContent().forEach(e->{
Map<String,Object> map = new HashMap<>(16);
map.put("userId", e.getId());
map.put("userName", e.getUserName());
map.put("userAge", e.getUserAge());
map.put("userPhone", e.getUserPhone());
data.add(map);
});
return PageResult.of(data, userPage.getTotalPages());
}
/**
* 删除用户
* @param userId 用户id
*/
public void deleteUser(Long userId) {
userRepository.findById(userId).ifPresent(u -> userRepository.delete(u));
}
}
5、定义接口
/**
* @author Gjing
**/
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@PostMapping("/user")
@ApiOperation(value = "增加用户", httpMethod = "POST")
@NotNull
public ResponseEntity saveUser(@RequestBody UserDto userDto) {
boolean saveUser = userService.saveUser(userDto);
if (saveUser) {
return ResponseEntity.ok("successfully added");
}
return ResponseEntity.badRequest().body("User already exist");
}
@DeleteMapping("/user/{user_id}")
@ApiOperation(value = "删除用户", httpMethod = "DELETE")
public ResponseEntity deleteUser(@PathVariable("user_id") Long userId) {
userService.deleteUser(userId);
return ResponseEntity.ok("ok");
}
@GetMapping("/list")
@ApiOperation(value = "分页查询用户列表", httpMethod = "GET")
@ApiImplicitParams({
@ApiImplicitParam(name = "page", value = "页数(0开始)", required = true, dataType = "int", paramType = "query", defaultValue = "0"),
@ApiImplicitParam(name = "size", value = "条数", required = true, dataType = "int", paramType = "query", defaultValue = "5")
})
@NotNull
public ResponseEntity listForUser(Integer page, Integer size) {
PageResult result = userService.listForUser(PageRequest.of(page, size));
return ResponseEntity.ok(result);
}
}
这样启动后就能对数据进行增删查的功能了,其中我们dao层的方法并没有定义过多方法,因为JPA内部已经帮我们提供了基本的CRUD功能
二、原理分析
为啥我们可以使用一些我们并没有定义的功能呢,接下来就一起一探究竟吧
1、执行过程
我们启动之前的案例,对其中任意一个方法打个端点,然后执行的时候
debug.jpg
会发现我们自己定义的userRepository
被注入了一个代理类,也就是jpaRepository
的实现类simpleJpaRepository
,继续debug,发现,当进入使用dao层方法的时候,会进入到这个代理类,然后经过一些拦截器,最终进入到QueryExecutorMethodInterceptor.doInvoke
这个方法中,这个拦截器主要做的事情就是判断方法类型,然后执行对应的操作,我们定义的findByUserPhone
属于自定义查询,继续debug会发现进入了getExecution()
获取查询策略方法,在执行execute()
会先选择一个对应的策略,
protected JpaQueryExecution getExecution() {
if (this.method.isStreamQuery()) {
return new StreamExecution();
} else if (this.method.isProcedureQuery()) {
return new ProcedureExecution();
} else if (this.method.isCollectionQuery()) {
return new CollectionExecution();
} else if (this.method.isSliceQuery()) {
return new SlicedExecution(this.method.getParameters());
} else if (this.method.isPageQuery()) {
return new PagedExecution(this.method.getParameters());
} else {
return (JpaQueryExecution)(this.method.isModifyingQuery() ? new ModifyingExecution(this.method, this.em) : new SingleEntityExecution());
}
}
如上述代码所示,根据method
变量实例化时的查询设置方式,实例化不同的JpaQueryExecution
子类实例去运行。我们的findByUserPhone
最终落入了SingleEntityExecution
返回单个实例的Execution
, 继续debug会发现,进入了createQuery()
方法,正是在这个方法里进行了Sql的拼装。
仔细观看这个类的代码时,会发现在构造方法中,有JpaQueryMethod
类,这其实就是接口中带有@Query注解方法的全部信息,包括注解,类名,实参等的存储类。前面提到的QueryExecutorMethodInterceptor
类,里面出现了一个private final Map<Method, RepositoryQuery> queries;
,查看RepositoryQuery
会发现里面有个QueryMethod
,由此可以得出,一个RepositoryQuery
代表了Repository
接口中的一个方法,根据方法头上注解不同的形态,将每个Repository
接口中的方法分别映射成相对应的RepositoryQuery
实例。
实例所有类型:
-
NamedQuery:使用
javax.persistence.NamedQuery
注解访问数据库的形式,内部就会根据此注解选择创建一个NamedQuery实例; -
NativeJpaQuery:方法头上@Query注解的
nativeQuery
属性如果显式的设置为true
,也就是使用原生SQL,此时就会创建NativeJpaQuery实例; - PartTreeJpaQuery:方法头上未进行@Query注解,就会使用JPA识别的方式进行sql语句拼接,此时内部就会创建一个PartTreeJpaQuery实例;
-
SimpleJpaQuery:方法头上@Query注解的
nativeQuery
属性缺省值为false
,也就是使用JPQL,此时会创建SimpleJpaQuery实例; -
StoredProcedureJpaQuery:在Repository接口的方法头上使用
org.springframework.data.jpa.repository.query.Procedure
注解,也就是调用存储过程的方式访问数据库,此时在jpa内部就会根据@Procedure注解而选择创建一个StoredProcedureJpaQuery实例;
2、启动流程
在启动的时候会实例化一个Repositories
,它会去扫描所有的class,然后找出由我们定义的、继承自org.springframework.data.repository.Repositor
的接口,然后遍历这些接口,针对每个接口依次创建如下几个实例:
- SimpleJpaRepository:进行默认的 DAO 操作,是所有 Repository 的默认实现;
- JpaRepositoryFactoryBean:装配 bean,装载了动态代理Proxy,会以对应的DAO的beanName为key注册到DefaultListableBeanFactory中,在需要被注入的时候从这个bean中取出对应的动态代理Proxy注入给DAO;
-
JdkDynamicAopProxy:动态代理对应的InvocationHandler,负责拦截DAO接口的所有的方法调用,然后做相应处理,比如
findByUserPhone()
被调用的时候会先经过这个类的invoke方法;
在JpaRepositoryFactoryBean.getRepository()
方法被调用的过程中,还是在实例化QueryExecutorMethodInterceptor
这个拦截器的时候,spring 会去为我们的方法创建一个PartTreeJpaQuery
,在它的构造方法中同时会实例化一个PartTree
对象。PartTree
定义了一系列的正则表达式,全部用于截取方法名,通过方法名来分解查询的条件,排序方式,查询结果等等,这个分解的步骤是在进程启动时加载 Bean 的过程中进行的,当执行查询的时候直接取方法对应的PartTree用来进行sql的拼装,然后进行DB的查询,返回结果。
简要概括就是:
在启动的时候扫描所有继承自 Repository 接口的 DAO 接口,然后为其实例化一个动态代理,同时根据它的方法名、参数等为其装配一系列DB操作组件,在需要注入的时候为对应的接口注入这个动态代理,在 DAO 方法被调用的时会走这个动态代理,然后经过一系列的方法拦截路由到最终的 DB 操作执行器JpaQueryExecution,然后拼装 sql,执行相关操作,返回结果。
三、JPA相关知识点
1、基本查询
JPA查询主要有两种方式,继承JpaRepository
后使用默认提供的,也可以自己自定义。以下列举了一部分
- 默认的
List<T> findAll();
List<T> findAll(Sort var1);
List<T> findAllById(Iterable<ID> var1);
<S extends T> List<S> saveAll(Iterable<S> var1);
void flush();
<S extends T> S saveAndFlush(S var1);
void deleteInBatch(Iterable<T> var1);
void deleteAllInBatch();
T getOne(ID var1);
<S extends T> List<S> findAll(Example<S> var1);
<S extends T> List<S> findAll(Example<S> var1, Sort var2);
- 自定义
User findByUserPhone(String userPhone);
自定义的关键字如下:
关键字 | 例子 | JPQL |
---|---|---|
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 ages) | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection 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) |
2、分页查询
分页查询在实际使用中非常普遍了,jpa已经帮我们实现了分页的功能,在查询的方法中,需要传入参数Pageable
,当查询中有多个参数的时候Pageable建议做为最后一个参数
传入。Pageable是Spring封装的分页实现类,使用的时候需要传入页数、每页条数和排序规则
,以之前编写的为例:
/**
* @author Gjing
**/
@Repository
public interface UserRepository extends JpaRepository<User,Long> {
/**
* 分页查询
* @param pageable 分页对象
* @return Page<User>
*/
@Override
Page<User> findAll(Pageable pageable);
/**
* 根据用户名分页查询
* @param userName 用户名
* @param pageable 分页对象
* @return page
*/
Page<User> findByUserName(String userName, Pageable pageable);
}
接口
@GetMapping("/list")
@ApiOperation(value = "分页查询用户列表", httpMethod = "GET")
@ApiImplicitParams({
@ApiImplicitParam(name = "page", value = "页数(0开始)", required = true, dataType = "int", paramType = "query", defaultValue = "0"),
@ApiImplicitParam(name = "size", value = "条数", required = true, dataType = "int", paramType = "query", defaultValue = "5")
})
@NotNull
public ResponseEntity listForUser(Integer page, Integer size) {
PageResult result = userService.listForUser(PageRequest.of(page, size, Sort.Direction.DESC,"id"));
return ResponseEntity.ok(result);
}
由上可看出,我们使用了PageRequest.of()
构建了pageable
对象,并指定了按id
进行排序
3、自定义SQL
大部分的SQL都可以根据方法名定义的方式来实现,但是由于某些原因我们想使用自定义的SQL来查询,JPA也是完美支持的;在SQL的查询方法上面使用@Query注解,如涉及到删除和修改在需要加上@Modifying
。也可以根据需要添加@Transactional
对事务的支持,查询超时的设置等
/**
* @author Gjing
**/
@Repository
public interface UserRepository extends JpaRepository<User,Long> {
/**
* 通过手机号查询
* @param userPhone 手机号
* @return user
*/
User findByUserPhone(String userPhone);
/**
* 分页查询
* @param pageable 分页对象
* @return Page<User>
*/
@Override
Page<User> findAll(Pageable pageable);
/**
* 根据用户名分页查询
* @param userName 用户名
* @param pageable 分页对象
* @return page
*/
Page<User> findByUserName(String userName, Pageable pageable);
/**
* 根据用户id更新用户
* @param userId 用户id
* @param userName 用户名
* @return int
*/
@Query("update User u set u.userName = ?1 where u.id = ?2")
@Modifying
@Transactional(rollbackFor = Exception.class)
int updateById(String userName, Long userId);
/**
* 查询指定用户
* @param userPhone 用户号码
* @return user
*/
@Query("select u from User u where u.userPhone = ?1")
User findUserByUserPhone(String userPhone);
}
4、动态查询
JPA极大的帮助了我们更方便的操作数据库,但是,在实际场景中,往往会碰到复杂查询的场景,前端会动态传一些参数请求接口,这时候就需要使用到动态查询了。
首先需要在继承一个接口JpaSpecificationExecutor
,需要传入一个泛型,填写你的具体实体对象即可,接下来在service层实现一个动态的查询方法
/**
* 动态查询
* @param age 岁数
* @param userName 用户名
* @return list
*/
public List<User> dynamicFind(Integer age, String userName) {
Specification<User> specification = (Specification<User>) (root, criteriaQuery, criteriaBuilder) -> {
List<Predicate> predicateList = new ArrayList<>();
if (ParamUtil.isNotEmpty(age)) {
predicateList.add(criteriaBuilder.greaterThan(root.get("userAge"), age));
}
if (ParamUtil.isNotEmpty(userName)) {
predicateList.add(criteriaBuilder.equal(root.get("userName"), userName));
}
return criteriaBuilder.and(predicateList.toArray(new Predicate[0]));
};
return userRepository.findAll(specification);
}
这里定义了根据年龄和名称动态查询用户列表,这里只举了个栗子,更多玩法需要自己去拓展研究
5、相关注解
注解 | 作用 |
---|---|
@Entity | 表明这是个实体bean |
@Table | 指定实体对应的数据表,里面可配置数据表名和索引,该索引可以不使用,默认表名为实体类名 |
@Column | 字段,可设置字段的相关属性 |
@Id | 指明这个字段为id |
@GeneratedValue | ID生成策略 |
@Transient | 指定该字段不用映射到数据库 |
@Temporal | 指定时间精度 |
@JoinColumn | 一对一:本表中指向另一个表的外键。一对多:另一个表指向本表的外键 |
@OneToOne、@OneToMany、@ManyToOne | 对应hibernate配置文件中的一对一,一对多,多对一 |
@Modifying | 搭配@Query使用,查询操作除外 |
@Query | 自定义SQL注解,默认JPQL,如果要使用原生sql,可以指定nativeQuery==true
|
6、扩展
1、如果想在往数据库插入数据时候,自动带上添加时间可以在对应字段标注
@CreatedDate
,想在更新的时候自动添加更新时间可以在对应字段使用@LastModifiedDate
,另外必须在启动类使用@EnableJpaAuditing
,否则无效
2、在@Column中定义字段的默认值,在默认情况下JPA是不会进行默认插值的,这时候,可以在实体类上加个注解@DynamicInsert
本文到此结束啦,篇幅比较长,如果哪里写的有误可以在评论区留言哈,也希望各位可以关注我哦,我会持续发布更多新的文章。本Demo的源代码地址:SprignBoot-Demo