boot spring 怎么执行hql_如何在SpringBoot项目中使用JPA

Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套 JPA 应用框架,底层使用了 Hibernate 的 JPA 技术实现,可使开发者用极简的代码即可实现对数据的访问和操作。它提供了包括增删改查等在内的常用功能,且易于扩展!学习并使用SpringDataJPA可以极大提高开发效率!让我们解脱DAO层的操作,基本上所有CRUD都可以依赖于它来实现

本文将介绍如何在SpringBoot中的使用案例和原理分析

一、SpringBoot使用

1、导入依赖

swagger和common为附加包,不使用的话可以不需要导的哈

org.springframework.boot

spring-boot-starter-data-jpa

org.springframework.boot

spring-boot-starter-web

mysql

mysql-connector-java

runtime

cn.gjing

tools-starter-swagger

1.3.0

cn.gjing

tools-common

1.2.5

2、配置文件

server:

port: 8082

spring:

application:

name: jpa-demo

datasource:

url: jdbc:mysql://127.0.0.1:3306/demo?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2b8

password: root

username: root

driver-class-name: com.mysql.cj.jdbc.Driver

type: com.zaxxer.hikari.HikariDataSource

hikari:

minimum-idle: 1

maximum-pool-size: 5

idle-timeout: 30000

connection-timeout: 20000

jpa:

# 是否打印sql

show-sql: true

hibernate:

# 开启自动建表功能,一般选update,每次启动会对比实体和数据表结构是否相同,不相同会更新

ddl-auto: update

# 设置创表引擎为Innodb,不然默认为MyiSam

database-platform: org.hibernate.dialect.MySQL5InnoDBDialect

3、创建实体类

/**

* @author Gjing

**/

@Data

@Builder

@NoArgsConstructor

@AllArgsConstructor

@Entity

@Table(name = "jpa_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 '用户名'")

@ApiModelProperty(name = "userName",value = "用户名")

private String userName;

@Column(name = "user_age", columnDefinition = "int not null comment '年龄'")

@ApiModelProperty(name = "userAge",value = "年龄")

private Integer userAge;

@Column(name = "user_phone", columnDefinition = "varchar(11) not null comment '手机号'")

@ApiModelProperty(name = "userPhone", value = "手机号")

private String userPhone;

@Column(name = "create_time", columnDefinition = "datetime")

@CreatedDate

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")

private Date createTime;

@Column(name = "update_time", columnDefinition = "datetime")

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

@LastModifiedDate

private Date updateTime;

}

4、定义Repository层

/**

* 通过手机号查询

* @param userPhone 手机号

* @return user

*/

User findByUserPhone(String userPhone);

/**

* 根据用户名分页查询

* @param userName 用户名

* @param pageable 分页对象

* @return page

*/

Page findByUserName(String userName, Pageable pageable);

/**

* 根据用户id更新用户

* @param userId 用户id

* @param userName 用户名

*/

@Query("update User u set u.userName = ?1 where u.id = ?2")

@Modifying

@Transactional(rollbackFor = Exception.class)

void updateById(String userName, Long userId);

/**

* 查询指定用户

* @param userPhone 用户号码

* @return user

*/

@Query("select u from User u where u.userPhone = ?1")

User findUserByUserPhone(String userPhone);

5、定义service层

/**

* @author Gjing

**/

@Service

public class UserService {

@Resource

private UserRepository userRepository;

/**

* 保存用户

*

* @param userDto 用户传输对象

*/

public void saveUser(UserDto userDto) {

User userDb = userRepository.findByUserPhone(userDto.getUserPhone());

if (userDb != null) {

throw new ServiceException("用户已存在");

}

User user = userRepository.saveAndFlush(User.builder().userName(userDto.getUserName())

.userAge(userDto.getUserAge())

.userPhone(userDto.getUserPhone())

.build());

}

/**

* 分页查询用户列表

*

* @param pageable 分页条件

* @return PageResult

*/

public PageResult> listForUser(Pageable pageable) {

Page userPage = userRepository.findAll(pageable);

return PageResult.of(userPage.getContent(), userPage.getTotalPages());

}

/**

* 删除用户

*

* @param userId 用户id

*/

public void deleteUser(Long userId) {

userRepository.findById(userId).ifPresent(u -> userRepository.delete(u));

}

/**

* 更新用户

*

* @param userName 用户名

* @param userId 用户id

*/

public void updateUser(String userName, Long userId) {

userRepository.updateById(userName, userId);

}

/**

* 根据手机号查询

*

* @param userPhone 手机号

* @return user

*/

public User findByUserPhone(String userPhone) {

return userRepository.findUserByUserPhone(userPhone);

}

/**

* 动态查询

*

* @param age 岁数

* @param userName 用户名

* @return list

*/

public List dynamicFind(Integer age, String userName) {

Specification specification = (Specification) (root, criteriaQuery, criteriaBuilder) -> {

List predicateList = new ArrayList<>();

if (ParamUtils.isNotEmpty(age)) {

predicateList.add(criteriaBuilder.equal(root.get("userAge"), age));

}

if (ParamUtils.isNotEmpty(userName)) {

predicateList.add(criteriaBuilder.equal(root.get("userName"), userName));

}

return criteriaBuilder.and(predicateList.toArray(new Predicate[0]));

};

return userRepository.findAll(specification);

}

6、定义接口

/**

* @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) {

userService.saveUser(userDto);

return ResponseEntity.ok("添加成功");

}

@DeleteMapping("/user/{user_id}")

@ApiOperation(value = "删除用户", httpMethod = "DELETE")

public ResponseEntity deleteUser(@PathVariable("user_id") Long userId) {

userService.deleteUser(userId);

return ResponseEntity.ok("删除成功");

}

@GetMapping("/user_page")

@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) {

return ResponseEntity.ok(userService.listForUser(PageRequest.of(page, size, Sort.Direction.DESC,"id")));

}

@GetMapping("/user/{user_phone}")

@ApiOperation(value = "根据手机号查询", httpMethod = "GET")

public ResponseEntity findUser(@PathVariable("user_phone") String userPhone) {

return ResponseEntity.ok(userService.findByUserPhone(userPhone));

}

@PutMapping("/user")

@ApiOperation(value = "更新用户信息", httpMethod = "PUT")

@ApiImplicitParams({

@ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "long", paramType = "Query"),

@ApiImplicitParam(name = "userName", value = "用户名", required = true, dataType = "String", paramType = "Query")

})

@NotNull

public ResponseEntity updateUser(Long userId, String userName) {

userService.updateUser(userName, userId);

return ResponseEntity.ok("更新成功");

}

@GetMapping("/user_list")

@ApiOperation(value = "动态查询用户", httpMethod = "GET")

@ApiImplicitParams({

@ApiImplicitParam(name = "userAge", value = "用户年龄", dataType = "int", paramType = "Query"),

@ApiImplicitParam(name = "userName", value = "用户名", dataType = "String", paramType = "Query")

})

public ResponseEntity> userList(Integer userAge, String userName) {

return ResponseEntity.ok(userService.dynamicFind(userAge, userName));

}

}

这样启动后就能对数据进行增删查的功能了,其中我们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 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 findAll();

List findAll(Sort var1);

List findAllById(Iterable var1);

ListsaveAll(Iterablevar1);

void flush();

S saveAndFlush(S var1);

void deleteInBatch(Iterable var1);

void deleteAllInBatch();

T getOne(ID var1);

ListfindAll(Examplevar1);

ListfindAll(Examplevar1, 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 {

/**

* 分页查询

* @param pageable 分页对象

* @return Page

*/

@Override

Page findAll(Pageable pageable);

/**

* 根据用户名分页查询

* @param userName 用户名

* @param pageable 分页对象

* @return page

*/

Page 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) {

return ResponseEntity.ok(userService.listForUser(PageRequest.of(page, size, Sort.Direction.DESC,"id")));

}

由上可看出,我们使用了PageRequest.of()构建了pageable对象,并指定了按id进行排序

3、自定义SQL

大部分的SQL都可以根据方法名定义的方式来实现,但是由于某些原因我们想使用自定义的SQL来查询,JPA也是完美支持的;在SQL的查询方法上面使用@Query注解,如涉及到删除和修改在需要加上@Modifying。也可以根据需要添加@Transactional对事务的支持,查询超时的设置等

/**

* @author Gjing

**/

@Repository

public interface UserRepository extends JpaRepository {

/**

* 通过手机号查询

* @param userPhone 手机号

* @return user

*/

User findByUserPhone(String userPhone);

/**

* 分页查询

* @param pageable 分页对象

* @return Page

*/

@Override

Page findAll(Pageable pageable);

/**

* 根据用户名分页查询

* @param userName 用户名

* @param pageable 分页对象

* @return page

*/

Page 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 dynamicFind(Integer age, String userName) {

Specification specification = (Specification) (root, criteriaQuery, criteriaBuilder) -> {

List predicateList = new ArrayList<>();

if (ParamUtil.isNotEmpty(age)) {

predicateList.add(criteriaBuilder.equal(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、返回指定的VO

很多时候,我们有些字段并不需要,所有我们可以定义一个VO去接JPA查询回来的结果

定义个VO

/**

* @author Gjing

**/

@Getter

@Setter

@ToString

@AllArgsConstructor

public class UserVO {

private Long id;

private String userName;

private Integer userAge;

private String userPhone;

}

在Repository接口增加一个接口

/**

* @author Gjing

**/

@Repository

public interface UserRepository extends JpaRepository , JpaSpecificationExecutor {

/**

* 分页查询用户

* @param pageable 分页条件

* @return Page

*/

@Query("select new com.gj.domain.vo.UserVO(u.id,u.userName,u.userAge,u.userPhone) from User as u")

Page findAllUser(Pageable pageable);

}

这里可以看到,我们采用了自定义HQL,语句中我们new了一个我们定义的VO去获取结果,这里有个注意的地方就是,你的VO一定要有构造方法与之对应,这样我们就能返回个VO啦,pageable参数JPA也会帮我们自动去分页查询,是不是很方便呢,那就快用起来吧

7、知识点扩展

1、如果想在往数据库插入数据时候,自动带上添加时间可以在对应字段标注@CreatedDate,想在更新的时候自动添加更新时间可以在对应字段使用@LastModifiedDate,另外必须在启动类使用@EnableJpaAuditing注解以及在对应的实体类使用@EntityListeners(AuditingEntityListener.class),否则无效。

2、在@Column中定义字段的默认值,在默认情况下JPA是不会进行默认插值的,这时候,可以在实体类上加个注解@DynamicInsert

本文到此结束啦,篇幅比较长,如果哪里写的有误可以在评论区留言哈,也希望各位可以关注我哦,我会持续发布更多新的文章。本Demo的源代码地址:SprignBoot-Demo


版权声明:本文为weixin_39812224原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。