Spring Data JPA学习笔记(一)

1,480 阅读8分钟

网上关于到底是使用mybatis还是JPA的争论较大,但与我无关,新公司用的JPA,那我就得学。

翻看了一些网上的文章,发现国内确实用的人很少,大部分文章都是好几年前的,而且错误很多,也没有一个统一的大而全的版本,那我也只能自己来写了。

什么是JPA、Spring Data JPA?

Jpa (Java Persistence API) 是 Sun 官方提出的 Java 持久化规范。它为 Java 开发人员提供了一种对象/关联映射工具来管理 Java 应用中的关系数据。它的出现主要是为了简化现有的持久化开发工作和整合 ORM 技术,结束现在 Hibernate,TopLink,JDO 等 ORM 框架各自为营的局面。

总的来说JPA是ORM规范,Hibernate、TopLink等是JPA规范的具体实现。而Spring Data Jpa则是在JPA之上添加另一层抽象(Repository层的实现),基于JPA的标准数据进行操作。简化了操作持久层的代码,它提供了包括增删改查等在内的常用功能,且易于扩展,也就是说它可以在实现了JPA规范的ORM框架下方便切换。

image.png

如何使用?

学习任何新东西,先会用,对它有个大概的了解,再去深究原理和细节,有的文章搞得本末倒置,不知所云,看起来很是头疼。

都2021年了,肯定得用springboot

核心jar包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

yml配置文件

server:
  port: 8080
  servlet:
    context-path: /
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
    username: root
    password: mysql123
  jpa:
    database: MySQL
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    show-sql: true
    hibernate:
     #每次运行程序,没有表时会创建表,如果对象发生改变会更新表结构,原有数据不会清空,只会更新
      ddl-auto: update

这里注意: jpa:hibernate:ddl-auto: update是hibernate的配置属性,其主要作用是:自动创建、更新、验证数据库表结构。该参数的几种配置如下:

1.create:每次加载hibernate时都会删除上一次的生成的表,然后根据你的model类再重新来生成新表,哪怕两次没有任何改变也要这样执行,这就是导致数据库表数据丢失的一个重要原因。

2.create-drop:每次加载hibernate时根据model类生成表,但是sessionFactory一关闭,表就自动删除。

3.update:最常用的属性,第一次加载hibernate时根据model类会自动建立起表的结构(前提是先建立好数据库),以后加载hibernate时根据model类自动更新表结构,即使表结构改变了但表中的行仍然存在不会删除以前的行。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,是要等应用第一次运行起来后才会。

4.validate:每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,字段不同会报错。不会创建新表,但是会插入新值。

5.none: 禁用ddl处理

使用JPA创建一个简单的查询

实体类

//@Entity说明这个class是实体类,并且使用默认的orm规则,即class名对应数据库表中表名,class字段名即表中的字段名。
(如果想改变这种默认的orm规则,就要使用@Table来改变class名与数据库中表名的映射规则,@Column来改变class中字段名与db中表的字段名的映射规则)
@Entity
//Table用来定义entity主表的name,catalog,schema等属性。
@Table(name = "tb_user")
@Data
public class User {

    @Id
    @GenericGenerator(name = "idGenerator", strategy = "uuid")
    @GeneratedValue(generator = "idGenerator")
    private String id;

    @Column(name = "username", unique = true, nullable = false, length = 64)
    private String username;

    @Column(name = "password", nullable = false, length = 64)
    private String password;

    @Column(name = "email", length = 64)
    private String email;

}

一般情况下,我们通过@id和@GeneratedValue来指定id主键和id策略,@GeneratedValue的strategy属性有四种策略,分别是:

  • TABLE:使用一个特定的数据库表格来保存主键。
  • SEQUENCE:根据底层数据库的序列来生成主键,条件是数据库支持序列。
  • IDENTITY:主键由数据库自动生成(主要是自动增长型,如Mysql)
  • AUTO:主键由程序控制(也是默认的,在指定主键时,如果不指定主键生成策略,默认为AUTO)

这里主键采用了UUID策略 @GenericGenerator是Hibernate提供的主键生成策略注解,注意下面的@GeneratedValue(JPA注解)使用generator = "idGenerator"引用了上面的name = "idGenerator"主键生成策略

uuid是Hibernate的拓展id策略,除uuid以外还有不少其它策略,用法和上面类似,具体可以看这篇文章: blog.csdn.net/qq_34531925…

Repository接口

public interface UserRepository extends JpaRepository<User, String> {
}

可以用IDEA去看相关的继承树,如图所示

E5@_X{HAFCS)PFY$P`{8K.png

我们继承的是JpaRepository,从源码不难看出,从CrudReposity开始,这些接口内都定义了一些基本的CRUD方法,根据方法名很容易看出其用法

jpa还可以自定义简单查询

以下简单地机翻自spring data jpa的文档,英语好可以自己去看文档

docs.spring.io/spring-data…

解析查询方法名称分为主语和谓语。第一部分 ( find…By, exists…By) 定义查询的主题,第二部分构成谓词。介绍从句(主语)可以包含进一步的表达。find(或其他引入关键字)和之间的任何文本都By被认为是描述性的,除非使用结果限制关键字之一,例如Distinct在要创建的查询上设置不同的标志或Top/First以限制查询结果。

image.png

JPA在这里遵循Convention over configuration(约定大约配置)的原则,遵循spring 以及JPQL定义的方法命名。Spring提供了一套可以通过命名规则进行查询构建的机制。这套机制会把方法名首先过滤一些关键字,比如 find…By, read…By, query…By, count…By 和 get…By 。系统会根据关键字将命名解析成2个子语句,第一个 By 是区分这两个子语句的关键词。这个 By 之前的子语句是查询子语句(指明返回要查询的对象),后面的部分是条件子语句。如果直接就是 findBy… 返回的就是定义Respository时指定的领域对象集合,同时JPQL中也定义了丰富的关键字:and、or、Between等等,下面我们来看一下JPQL中有哪些关键字:

And----findByLastnameAndFirstname----where x.lastname = ?1 and

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)

And----findByLastnameAndFirstname----where x.lastname = ?1 and

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)

当然我们也可以自己编写SQL,但我觉得既然用了jpa就不该这么做

   /**
     * 查询根据参数位置
     * @param name
     * @return
     */
    @Query(value = "select * from person  where name = ?1",nativeQuery = true)
    Person findPersonByName(String Name);
 
    /**
     * 查询根据Param注解
     * @param name
     * @return
     */
    @Query(value = "select p from person p where p.uname = :name")
    Person findPersonByNameTwo(@Param("name") String name);

根据参数位置,?号后的数字就代表第几个参数,和底下的形参对应 根据@param注解,直接引用:加注解内定义的名字即可

@Query有nativeQuery=true,表示可执行的原生sql,原生sql指可以直接复制sql语句给参数赋值就能运行 @Query无nativeQuery=true, 表示不是原生sql,查询语句中的表名则是对应的项目中实体类的类名

注意:在@Query注解中编写JPQL实现DELETE和UPDATE操作的时候必须加上@modifying注解,以通知Spring Data 这是一个DELETE或UPDATE操作,且需加上@Transactional以表明事务操作

复杂查询

通过上面的继承树我们可以看到有个JpaSpecificationExecutor接口,里面提供了这些方法:

public interface JpaSpecificationExecutor<T> {

    T findOne(Specification<T> spec);

    List<T> findAll(Specification<T> spec);

    Page<T> findAll(Specification<T> spec, Pageable pageable);

    List<T> findAll(Specification<T> spec, Sort sort);

    long count(Specification<T> spec);
}

方法中的Specification就是需要我们传进去的参数,它是一个接口,也是我们实现复杂查询的关键,其中只有一个方法toPredicate


public interface Specification<T> {

    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}

也就是说我们只需要继承Specification接口,重写这个toPredicate方法即可,具体实现需要按照JPA 2.0 criteria api写好查询条件即可

这里找了个例子,参考 segmentfault.com/a/119000003…


    public List<Flow> queryFlows(int pageNo, int pageSize, String status, String userName, Date createTimeStart, Date createTimeEnd) {
        List<Flow> result = null;

        // 构造自定义查询条件
        Specification<Flow> queryCondition = new Specification<Flow>() {
            @Override
            public Predicate toPredicate(Root<Flow> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                List<Predicate> predicateList = new ArrayList<>();
                if (userName != null) {
                    predicateList.add(criteriaBuilder.equal(root.get("currentOperator"), userName));
                }
                if (status != null) {
                    predicateList.add(criteriaBuilder.equal(root.get("status"), status));
                }
                if (createTimeStart != null && createTimeEnd != null) {
                    predicateList.add(criteriaBuilder.between(root.get("createTime"), createTimeStart, createTimeEnd));
                }
                if (orderId!= null) {
                    predicateList.add(criteriaBuilder.like(root.get("orderId"), "%" + orderId+ "%"));}
                return criteriaBuilder.and(predicateList.toArray(new Predicate[predicateList.size()]));
            }
        };

        // 分页和不分页,这里按起始页和每页展示条数为0时默认为不分页,分页的话按创建时间降序
       
        if (pageNo == 0 && pageSize == 0) {
            result = flowRepository.findAll(queryCondition);
        } else {
            result = flowRepository.findAll(queryCondition, PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Direction.DESC, "createTime"))).getContent();
        }
       
        return result;
    }

其实仔细看看也很简单,Specification的泛型就是你要查询的类,predicateList装的是查询条件,就是常用的equals、between、like这种,然后就是加了个分页,分页传入PageRequest对象(注意用的是PageRequest.of来构造对象)。分页中有一个getContent(),可以不加,不加的话还会返回页数/总条数等一些分页的参数,加这个方法就只返回list集合.

到这里一些基本的查询都可以实现了,一些深入的用法后面再慢慢写。