前言

在java项目开发中orm层框架首屈一指的当属mybatis,尤其在亚洲这个框架的使用率更是将一众其他框架甩在身后。但是我们也可以在一些公众号或者资讯上看到,在欧美国家的开发中hibernate、jpa这些框架的使用率比mybatis更高一筹。我们姑且不谈地域的问题,可以肯定的是hibernate、jpa这类全自动orm一定存在着mybatis所没有的优点。

spring data jpa 很好的整合了hibernate和jpa两者为java orm开发提供了新鲜血液。就我个人的使用角度来说spring data jpa提供了java的写法来操作mysql,保留了代码的完整性,从视觉上看,代码比较整洁,对应单表的操作可以做到拿来即用,提高了一些简单接口的开发效率。

下面就来感受一下spring data jpa 的开发风格。

框架搭建

首先在pom文件中加入依赖

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

在application.yml文件中添加如下的配置

jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    database-platform: org.hibernate.dialect.MySQL8Dialect
    database: mysql
    properties:
      hibernate:
        format_sql: true

第一个ddl-auto主要是我们在spring data jpa中会使用正向工程来创建数据库表。

什么是正向工程,与之相对应的就是逆向工程。逆向工程就是平时我们使用mybatis的插件通过数据表映射到java实体对象的过程。而正向工程就是我们通过java实体对象映射到数据表的过程。

这个update是指在程序启动如果启动时表格式不一致则更新表,原有数据保留。

show-sql指的是日志上输出sql。

database-platform和database是指定了sql的方言,这里是使用了mysql8的语法。

format-sql是开启sql的格式化。

实体创建设计

首先我们需要创建一个java的实体对象。例如

@Data
@Entity
@Table(name = "tb_book")
@org.hibernate.annotations.Table(appliesTo = "tb_book",comment = "电子书")
@EntityListeners(AuditingEntityListener.class)
public class BookPO implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "book_id",nullable = false,columnDefinition = "int(11) comment '书籍id'")
    private Integer bookId;
    @Column(name = "book_name",nullable = false,columnDefinition = "varchar(45) comment '书籍名称'")
    private String bookName;
    @Column(name = "publication_date",nullable = false,columnDefinition = "date comment '出版时间'")
    private LocalDate publicationDate;
    @Column(name = "price",nullable = false,columnDefinition = "decimal(6,2) comment '价格'")
    private BigDecimal price;
    @CreatedDate
    @Column(name = "create_time",nullable = false,columnDefinition = "datetime default current_timestamp")
    private LocalDateTime createTime;
    @LastModifiedDate
    @Column(name = "update_time",nullable = false,columnDefinition = "datetime default current_timestamp on update current_timestamp")
    private LocalDateTime updateTime;
}

@Data是lombok的插件简化getter、setter的方法。

@Entity是标记该类为一个实体对象

@Table指的是设置映射的表的名称

hibernate的table注解也是指定表名称同时指定了表的注释

@EntityListeners是一个实体监听操作,配合下面的@CreateDate和@LastModifiedDate两个注解,可以实现在insert时刷新创建时间,在更新时刷新更新时间

@Id注解是标记这个字段是主键

@GeneratedValue是指定了这个主键的生成策略。

@Column是这里的核心字段,它配置了我们数据表中的字段名称是否允许为空,是否唯一约束,长度,精度等。复杂的表达可以直接写在columnDefinition中

创建完实体类之后,我们需要声明一个repository,这个接口可以为我们的实体创建许多的通用方法

例如

public interface BookRepository extends JpaRepository<BookPO,Integer>, JpaSpecificationExecutor<BookPO> {
}
  1. 这里的JpaRepository和JpaSepcificationExecutor一个是提供sql简单方法的一个是提供一些相对复杂的方法的,具体如下
List<T> findAll();

    List<T> findAll(Sort var1);

    List<T> findAllById(Iterable<ID> var1);

    <S extends T> List<S> saveAll(Iterable<S> var1);

    <S extends T> S save(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);
  1.  查询出全表的信息
  2.  查询出全表的信息同时排序
  3. 相当于一个in查询接受一个主键字段的数组
  4. 批量保存记录
  5. 保存单个记录
  6. 通过传入一个主键字段的数组达到批量删除的目睹
  7. 删除全表
  8. 通过主键查询一条记录
  9. 通过一个Example构建一个查询条件返回一个list对象
  10. 通过一个Example构建一个查询条件,加上一个排序参数返回一个list对象

下面介绍下提到的Example,这个主要是构建单表操作时的条件的,ExampleMatcher是主要的一个api口子,

public List<BookPO> getList(String name) {
        BookPO bookPO = new BookPO();
        bookPO.setBookName(name);
        ExampleMatcher exampleMatcher = ExampleMatcher.matching()
                .withMatcher("bookName", ExampleMatcher.GenericPropertyMatchers.startsWith());
        return bookRepository.findAll(Example.of(bookPO,exampleMatcher));
    }

example主要是作用单表中的字段匹配的,例如字符串以什么开头,以什么结尾,忽略大小写等,在匹配上的功能比较强一些。

另一种jpa中比较通用的是JpaSpecificationExecutor这个的条件构造器,它的条件构造不止于匹配

比如排序

PageRequest pageRequest = PageRequest.of(pageNum - 1, pageSize, Sort.by(Sort.Direction.DESC, "publicationDate"));

可以用这个jpa的pageRequest指定起始页,每页大小,和多条件的排序。

或者组合一个动态条件查询

public List<BookPO> queryBookList(String bookName, BigDecimal minPrice, BigDecimal maxPrice) {
        Specification<BookPO> specification = (Specification<BookPO>) (root, criteriaQuery, criteriaBuilder) -> {
            List<Predicate> andList = new ArrayList<>();
            if (StringUtils.isNotBlank(bookName)){
                andList.add(criteriaBuilder.like(root.get("bookName"),"%"+bookName+"%"));
            }
            if (null != minPrice){
                andList.add(criteriaBuilder.greaterThanOrEqualTo(root.get("price"),minPrice));
            }
            if (null != maxPrice){
                andList.add(criteriaBuilder.lessThanOrEqualTo(root.get("price"),maxPrice));
            }
            return criteriaBuilder.and(andList.toArray(new Predicate[andList.size()]));
        };
        return bookRepository.findAll(specification);
    }

这里首先声明一个List<Predicate>的list可以动态的存放条件,

然后我们可以通过if的判断来构造我们的动态条件,最后通过CriteriaBuilder来组合list里面的条件,如果是出现and和or相混合的,那么需要分成多个list再进行组合。

投影,通常在执行sql的时候我们为了io性能,会有选择性的查询出某些字段出来。

public List<OrderDTO> queryByCustomerId(Integer customerId) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Tuple> query = criteriaBuilder.createTupleQuery();
        Root<OrderPO> root = query.from(OrderPO.class);
        query.multiselect(root.get("orderId"), root.get("price"))
                .where(
                        criteriaBuilder.and(criteriaBuilder.equal(root.get("customerId"), customerId))
                );
        List<OrderDTO> collect = entityManager.createQuery(query).getResultList()
                .stream()
                .map(tuple -> {
                    OrderDTO orderDTO = new OrderDTO();
                    orderDTO.setOrderId(tuple.get(0, Integer.class));
                    orderDTO.setPrice(tuple.get(1, BigDecimal.class));
                    return orderDTO;
                })
                .collect(Collectors.toList());
        return collect;
    }

 这里用到了一个元组查询也就是把字段映射出来。然后可以通过一个map把字段映射出去。

QueryDsl使用

如果对于构造条件的灵活度有更高要求的可以选择配合queryDsl使用。这个是提供了query的动态脚本语言。

首先在pom中添加依赖

<dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
        </dependency>

然后加入插件,指定对象生成的目录,然后可以运行一个maven命令,来生成queryDsl的对象。

<plugin>
                <groupId>com.mysema.maven</groupId>
                <artifactId>apt-maven-plugin</artifactId>
                <version>${mysema.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>process</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>target/generated-sources/java</outputDirectory>
                            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                        </configuration>
                    </execution>
                </executions>
                <dependencies>
                    <dependency>
                        <groupId>com.querydsl</groupId>
                        <artifactId>querydsl-apt</artifactId>
                        <version>${querydsl.version}</version>
                    </dependency>
                </dependencies>
              </plugin>

然后运行该命令之后会在对应的目录下生成一系列Q开头的类。

然后添加一个配置类

@Configuration
public class JpaQueryFactoryConfig {

    @Bean
    public JPAQueryFactory jpaQueryFactory(EntityManager entityManager){
        return new JPAQueryFactory(entityManager);
    }
}

后面使用queryDsl方法可以使用这个jpaQueryFactory 

这里介绍join

首先是join的写法,这个主要用于多表操作。举个例子,订单表和订单详情表,是个一对多的关系,在一些业务场景中需要实现关联查询。

QOrderDetailPO orderDetailPO = QOrderDetailPO.orderDetailPO;
        QOrderPO orderPO = QOrderPO.orderPO;
        List<OrderDetailPO> orderDetailPOList = jpaQueryFactory.selectFrom(orderDetailPO)
                .innerJoin(orderPO).on(orderDetailPO.orderId.eq(orderPO.orderId))
                .where(orderPO.customerId.eq(1))
                .fetch();

先声明用到的表所对应的对象,

然后通过selectFrom声明主表

通过innerJoin声明从表

通过on方法指明关联的字段。

可以发现通过queryDsl实现多表关联的功能还是比较简单的。

另一种spring data jpa中比较有特点的查询方法就是在repository中直接定义查询方法

例如

List<BookPO> findBookPOSByBookNameEquals(String name);

按照spring 的语法定义你的方法名即能达到查询的效果。

尽管已经配置上了queryDsl,但是如果真的遇上了非常复杂的sql,还是需要通过原生的sql来解决。

那么在spring data jpa中通常可以在repository中定义一个自己的方法,然后使用@Query注解

public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.emailAddress = ?1")
  User findByEmailAddress(String emailAddress);
}

这个是使用JPQL的语法设计的,如果要使用原生的sql可以这样写

在@Query中设置nativeQuery属性为true

public interface UserRepository extends JpaRepository<User, Long> {

  @Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
    countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",
    nativeQuery = true)
  Page<User> findByLastname(String lastname, Pageable pageable);
}

这种写法就有点mybatis 中的注解写法的意思了,可以比较灵活地编写sql,和mybatis一样。

上面总结了开发中常用的一些spring data jpa 的操作,当然jpa中还有许多其他的操作,在实际的开发中我会不断的探索并分享给大家