前言
在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> {
}
- 这里的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);
- 查询出全表的信息
- 查询出全表的信息同时排序
- 相当于一个in查询接受一个主键字段的数组
- 批量保存记录
- 保存单个记录
- 通过传入一个主键字段的数组达到批量删除的目睹
- 删除全表
- 通过主键查询一条记录
- 通过一个Example构建一个查询条件返回一个list对象
- 通过一个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中还有许多其他的操作,在实际的开发中我会不断的探索并分享给大家