SpringBoot如何使用JPA操作数据库?

SpringDataJPA是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
会发现我们自己定义的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注解以及在对应的实体类使用@EntityListeners(AuditingEntityListener.class)注解,否则无效
2、在@Column中定义字段的默认值,在默认情况下JPA是不会进行默认插值的,这时候,可以在实体类上加个注解@DynamicInsert


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

猜你喜欢

转载自yq.aliyun.com/articles/708635