写在前面:
之前一直写MyBatis,去年开始做的这个新项目用的是JPA,整理了一些基本使用方法
1、 集成方法:
1.1 引入依赖
<!--spring data 依赖-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<!--数据库连接-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
1.2 配置文件
配置数据库信息:
//数据库连接信息
spring.datasource.url= url
spring.datasource.username=username
spring.datasource.password=<password>
//Java代码实体字段命名与数据库表结构字段之间的名称映射策略
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
//下面配置开启后,会禁止将驼峰转为下划线
//spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
//是否打印运行时的sql
spring.jpa.show-sql=false
//控制是否可以基于程序中Entity的定义自动创建或者修改DB中表结构
//create -> 重启后删除上一次的表,重新生成
//create-drop -> sessionFactory关闭时,创建的表就自动删除,服务启动后重新创建
//validate -> 验证创建数据库表结构,不同就报错
//update none
//等价于spring.jpa.hibernate.ddl-auto
spring.jpa.properties.hibernate.hbm2ddl.auto=update
1.3 入口注解
没有特殊需求可以什么都不加
@SpringBootApplication
@EntityScan("path") //指定实体的目录
@EnbaleJpaRepositories(basePackages = {"com.veezean.demo.repository"}) //指定扫描的表repository目录
@EnableJpaAuditing//开启JPA auditing能力,可以自动赋值一些字段,比如创建时间、最后一次修改时间etc
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2、基本用法
2.1 实体映射类 Entity
实体类编写比较简单,只需要在普通的Java数据类上添加一些注解,用来描述字段的一些附加信息
@Data
@Entity
@Table(name = "user")
@EntityListeners(value = AuditingEntityListener.class)//对实体属性变化的跟踪
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String workId;
private String userName;
@ManyToOne(optional = false)
@JoinColumn(name = "department")
private DepartmentEntity department;
@CreatedDate
private Date createTime;
@LastModifiedDate
private Date updateTime;
}
2.2 常用注解
2.3 Repository
2.3.1 三种 Repository
Spring Data 扩展了Repository接口,提供了一些便于操作数据库的子类,对主体repository层级提供的主要方法进行简单的梳理:
- CrudRepository: 提供基本的CRUD操作。
- PagingAndSortingRepository: 在父类的基础上提供分页、排序的能力
- JpaRepository: 在父类的基础上,提供了查询列表、批量删除、强制同步以及Example查询等能力
3.3.2 自定义Repository类
自定义Repository时,继承JpaRepository需要传入两个泛型:
1、需要操作的具体Entity对象;
2、Entity的主键数据类型
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
List<UserEntity> findAllByDepartment(DepartmentEntity department);
UserEntity findFirstByWorkId(String workId);
List<UserEntity> findAllByDepartmentInAndUserNameLike(List<DepartmentEntity> departmentIds, String userName);
}
3.4 查询
如上所述,简单的操作只需要基于SpringData JPA的命名规范进行接口方法的命名即可,无需关注具体实现,也不需要提供实现类
但是面对复杂的操作,这样的定义方式就不再适用了,那么应该如何做呢?
不固定查询字段的场景(Example)
例:需要做一个用户搜索的能力,要求支持根据用户名、工号、部门、性别、年龄、职务等等若干个字段中的1个或者多个的组合来查询符合条件的用户信息。
🌟 使用 Example查询
public Page<UserEntity> queryUsers(Request request, UserEntity queryParams) {
// 查询条件构造出对应Entity对象,转为Example查询条件
Example<UserEntity> example = Example.of(queryParams);
// 构造分页参数
Pageable pageable = PageHelper.buildPageable(request);
// 按照条件查询,并分页返回结果
return userRepository.findAll(example, pageable);
}
🔸查询条件复杂时为了不使方法名称过长也可以使用 Example
public List<User> findUsersByNameAndAge(String name, Integer age) {
User user = new User();
user.setName(name);
user.setAge(age);
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("name", match -> match.contains())
.withMatcher("age", match -> match.exact());
Example<User> example = Example.of(user, matcher);
return userRepository.findByExample(example);
}
❗example只能针对字符串进行条件设置
复杂场景(SQL, Specification)
🌟定制SQL
遇到非常复杂的查询场景也可以通过定制SQL的方式实现
@Query(
value = "select t.*,(select group_concat(a.assigner_name) from workflow_task a where a.state='R' and a.proc_inst_id=t.proc_inst_id) deal_person,"
+ " (select a.task_name from workflow_task a where a.state='R' and a.proc_inst_id=t.proc_inst_id limit 1) cur_step "
+ " from workflow_info t where t.state='R' and t.type in (?1) "
+ "and exists(select 1 from workflow_task b where b.assigner=?2 and b.state='R' and b.proc_inst_id=t.proc_inst_id) order by t.create_time desc",
countQuery = "select count(1) from workflow_info t where t.state='R' and t.type in (?1) "
+ "and exists(select 1 from workflow_task b where b.assigner=?2 and b.state='R' and b.proc_inst_id=t.proc_inst_id) ",
nativeQuery = true)
Page<FlowResource> queryResource(List<String> type, String workId, Pageable pageable);
🌟Specification 构建动态查询
1️⃣ 需要定义一个Specification,用来构建查询条件:
Specification<User> spec = (root, query, criteriaBuilder) -> {
Path<Integer> type = root.get("verified");
// verified == "1"
Predicate verifiedPredicate = criteriaBuilder.equal(type, "1");
// email like "%qq%"
Path<String> email = root.get("email");
Predicate emailPredicate = criteriaBuilder.like(email, "%qq%");
// and 条件 verified == "1" and email like "%qq%"
Predicate predicate = criteriaBuilder.and(verifiedPredicate, emailPredicate);
return predicate;
};
lambda表达式中传入的3个参数分别为:
Root:查询哪个表(关联查询) = from CriteriaQuery:查询哪些字段,排序是什么 =组合(order by . where ) CriteriaBuilder:条件之间是什么关系,如何生成一个查询条件,每一个查询条件都是什么类型(> between in...) = where
Specification接口中只定义了如下一个方法:
//构造查询条件
/**
* root :Root接口,代表查询的根对象,可以通过root获取实体中的属性
* query :代表一个顶层查询对象,用来自定义查询
* cb :用来构建查询,此对象里有很多条件方法
* Predicate(Expression): 每一条查询条件的详细描述
**/
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
因此也可以通过匿名内部类的方式:
Specification <User> spec = new Specification<User>() {
public Predicate toPredicate(Root<User> root,
CriteriaQuery<?> query, CriteriaBuilder cb) {
//cb:构建查询,添加查询方式 like:模糊匹配
//root:从实体Customer对象中按照custName属性进行查询
return cb.like(root.get("name").as(String.class), "111%");
}
};
2️⃣ 自定义一个repository接口,继承JpaRepository和JpaSpecificationExecutor
@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}
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);
}
3️⃣ 使用时只需调用repository中的findAll方法,并传入相应参数即可:
userRepository.findAll(specification);
3.5 分页、排序
Pageable
分页,排序使用Pageable对象进行传递,其中包含Page和Sort参数对象。查询时直接传递
List<User> findAllByDepartment(Department dept, Pageable pageable);
** Specification 查询分页也只需要构造Pageable参数并传入
Slice结果对象
**还有一种特殊的分页场景。比如,DB表中有100w条记录,然后现在需要将这些数据全量的加载到ES中。如果逐条查询然后插入ES,显然效率太慢;如果一次性全部查询出来然后直接往ES写,服务端内存可能会爆掉。
🌟 这种场景可以基于Slice结果对象实现
Slice的作用是,只知道是否有下一个Slice可用,不会执行count
private <T extends EsDocument, F> void fullLoadToEs(IESLoadService<T, F> esLoadService) {
try {
final int batchHandleSize = 10000;
Pageable pageable = PageRequest.of(0, batchHandleSize);
do {
// 批量加载数据,返回Slice类型结果
Slice<F> entitySilce = esLoadService.slicePageQueryData(pageable);
// 具体业务处理逻辑
List<T> esDocumentData = esLoadService.buildEsDocumentData(entitySilce);
esUtil.batchSaveOrUpdateAsync(esDocumentData);
// 获取本次实际上加载到的具体数据量
int pageLoadedCount = entitySilce.getNumberOfElements();
if (!entitySilce.hasNext()) {
break;
}
// 自动重置page分页参数,继续拉取下一批数据
pageable = entitySilce.nextPageable();
} while (true);
} catch (Exception e) {
log.error("error occurred when load data into es", e);
}
}