一、说明
JPA提供了操作数据库的接口,在开发过程中继承和使用这些接口,可简化现有的持久化开发的工作。
二、JPA提供的接口
1、JpaRepository
JpaRepository继承自PagingAndSortingRepository
2、PagingAndSortingRepository
PagingAndSortingRepository提供了分页和排序方法。
3、CrudRepository
提供了增加、删除、修改、查询方法。
以上三种Repository的关系如图:
4、分页类Pageable和Page
构造Pageable调用Repository相关接口,返回Page
创建方式:PageRequest类of等static方法创建Pageable对象。
PageRequest与Pageable关系:PageRequest implements Pageable
@RequestMapping("")
public ModelAndView articlelist(@RequestParam(value = "start", defaultValue = "0") Integer start,
@RequestParam(value = "limit", defaultValue = "5") Integer limit) {
start = start < 0 ? 0 : start;
Sort sort = Sort.by(Sort.Direction.DESC, "id");
Pageable pageable = PageRequest.of(start, limit, sort);
Page<Article> page = articleRepository.findAll(pageable);
ModelAndView mav = new ModelAndView("article/list");
mav.addObject("page", page);
return mav;
}
上面示例,通过接收客户端页号、每页记录数,创建Pageable对象,然后执行分页查询
-start,第几页,从0开始,默认为第0页
-limit,每一页的大小,默认为20
5、排序类Sort
Sort类专门用来处理排序。最简单的排序就是通过一个字段属性,按照此属性值进行排序,默认升序。
当然可以提供多个字段属性进行排序。
创建方式:
以下代码分别对应图中四种方式
Sort sort = new Sort(Sort.Direction.DESC, "id");
Sort sort = Sort.by(Sort.Order.desc("uid"));
Sort sort = Sort.by("id","name","code");
List<Sort.Order> orderList = null;
Sort sort = Sort.by(orderList);
其他特性(这里只是列出,等工作中如果用到,再调查):
三、使用
当上面三个接口中JPA提供的方法不够用时,可以扩展上面接口(具体方式是自定义接口并extends上面接口)的方式在自定义接口中增加新的JPA方法,但是新增JPA方法需要具备一定的命名规则。
1、JPA方法命名规则
1、JPA讲究的就是约定优于配置,因此自定义Jpa方法名时要按照命名规范来写,之后,springdata会根据方法命名的【前缀】、【中间连接词】如And、Or、Like、NotNull等类似SQL中的关键词、【字段属性名】,生成方法的实现。
2、约定方法名还可以支持以下几种语法:
2、@Query注解1
通过@Query注解,可以实现在通过Jpa方法无法方便的命名时,@Query注解参数默认是JPQL语言,该语言是一种和SQL非常类似的中间性和对象和语言,最终会将JPQL编译成对应数据库的的SQL语言,从而屏蔽不通数据库的差异。
使用示例如下:
可以通过JPQL完成UPDATE、DELETE、SELECT操作,但是不支持INSERT。同时在应用UPDATE、DELETE时,必须使用@Modifying注解。
使用示例如:
3、@Query注解2
@Query除了可以使用JPQL语言作为注解参数外,还可以使用原生SQL作为参数,但是要设置nativeQuery参数为true
使用示例如:
4、事务
自定义的update、delete操作需要使用事务。此时需要先定义Service层,然后在Service层的方法上添加事务。
Spring data在JPA实现层面也提供了默认事务:
如果需要改变Spring Data提供的事务默认方式,可以在方法上使用注解:org.springframework.transaction.annotation.Transactional
在进行多个Repository操作时,也应该使这些操作在同一个事务中,按照分层架构思想,这些操作属于业务逻辑层(Service),因此需要在Service层实现对多个Repository的调用,并在方法上声明@Transactional5
5、用Specification实现动态(复杂)查询
使用JPA进行一些单表简单的查询非常轻松,后来在遇到多条件动态查询的业务场景时,发现现有的【JpaRepository提供的方法】和【自己写@Query】已经满足了不了需求,难不成要对所有的条件和字段进行判断,再写很多个dao方法?后面查到JPA提供了围绕Specification这个类的一系列类,来用于实现动态查询。
使用方法:
1)自定义Repository接口
这个接口除了继承JpaRepository之外,还需要继承JpaSpecificationExecutor。
public interface CardRepository extends JpaRepository<Card,Long> , JpaSpecificationExecutor<Card> {
Card findById(long id);
}
这是我用于查询文件信息的dao,我们可以看到JpaSpecificationExecutor有以下这些方法:
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);
- findOne(spec):根据条件查询获取一条数据;
- findAll(spec) :根据条件查询获取全部数据;
- findAll(specification, pageable) : 根据条件分页查询数据,pageable设定页码、一页数据量,同时返回的是Page类对象,可以通过getContent()方法拿到List集合数据;
- findAll(specification, sort) : 根据条件查询并返回排序后的数据;
- count(spec) : 获取满足当前条件查询的数据总数;
现在光定义了Repository没用,调用JpaSpecificationExecutor接口中方法需要往方法里传Specification的对象,Specification是一个接口,肯定没法直接获取对象。
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}
所以我自定义了一个类实现类这个接口(通常创建Specification的匿名内部类)。
package com.example.demo.entity;
import com.example.demo.repository.CardRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.test.context.junit4.SpringRunner;
import javax.persistence.criteria.*;
/**
* @Auther: mazhongjia
* @Date: 2020/8/10 19:57
* @Version: 1.0
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestJpaSpecificationExecutor {
@Autowired
private CardRepository cardRepository;
@Test
public void testJpaSpecificationExecutor(){
int pageNo = 0;
int pageSize = 5;
PageRequest pageRequest = PageRequest.of(pageNo,pageSize);
Specification<Card> specification = new Specification<Card>() {
@Override
public Predicate toPredicate(Root<Card> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Path path = root.get("id");
//gt是大于的意思。这里代表id大于2
Predicate predicate1 = cb.gt(path,2);
//equal是等于的意思,代表查询num值为422803的数据记录
Predicate predicate2 = cb.equal(root.get("num"),422803);
//构建组合的Predicate
Predicate predicate = cb.and(predicate1,predicate2);
return predicate;
}
};
//执行动态条件查询
Page<Card> page = cardRepository.findAll(specification,pageRequest);
System.out.println("总记录数:"+page.getTotalElements());
System.out.println("当前第:"+(page.getNumber() + 1+"页"));
System.out.println("总页数:"+(page.getTotalPages()));
System.out.println("当前页面的List:"+(page.getContent()));
System.out.println("当前页面的记录数:"+(page.getNumberOfElements()));
}
}
代码解释如下:
其中root个人理解为代表是哪个表,通过root可以获取表模
运行上面代码,hibernate生成的sql语句:
6、用ExampleMatcher进行查询
说明
以对象的方式作为查询条件调用JPA带Example参数的接口
ExampleMatcher实例查询三要素
- 实体对象:在ORM框架中与Table对应的域对象,一个对象代表数据库表中的一条记录,如上例中User对象,对应user表。在构建查询条件时,一个实体对象代表的是查询条件中的“数值”部分。如:要查询姓“X”的客户,实体对象只需要存储条件值“X”。
- 匹配器:ExampleMatcher对象,它是匹配“实体对象”的,表示了如何使用“实体对象”中的“值”进行查询,它代表的是“查询方式”,解释了如何去查的问题。如:要查询姓“X”的客户,即姓名以“X”开头的客户,该对象就表示了“以某某开头的”这个查询方式,如上例中:withMatcher(“userName”, GenericPropertyMatchers.startsWith())
- 实例:即Example对象,代表的是完整的查询条件。由实体对象(查询条件值)和匹配器(查询方式)共同创建。最终根据实例来findAll即可。
示例
Controller
package com.mzj.springboot.jpa.exampleMatcher.controller;
import com.mzj.springboot.jpa.exampleMatcher.entity.User;
import com.mzj.springboot.jpa.exampleMatcher.repository.UserRespository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Timestamp;
import java.util.List;
@RestController
public class TestMatcherController {
@Autowired
private UserRespository userRespository;
@GetMapping("/lost/list")
public String findAllLostProperty () {
User user = new User();
user.setName("ma");
user.setPhone("130");
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
.withMatcher("name", ExampleMatcher.GenericPropertyMatchers.contains()) //姓名采用“开始匹配”的方式查询
.withMatcher("phone", ExampleMatcher.GenericPropertyMatchers.contains()) //姓名采用“开始匹配”的方式查询
.withIgnorePaths("id"); //忽略属性:是否关注。因为是基本类型,需要忽略掉
//创建实例
Example<User> ex = Example.of(user, matcher);
//查询
List<User> ls = userRespository.findAll(ex);
System.out.println(ls);
return "sucess";
}
}
Entity
package com.mzj.springboot.jpa.exampleMatcher.entity;
import lombok.Data;
import javax.persistence.*;
import javax.validation.constraints.NotEmpty;
@Entity
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(nullable = false, unique = true)
@NotEmpty(message = "姓名不能为空")
private String name;
@Column(nullable = false)
private String phone;
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("User{");
sb.append("id=").append(id);
sb.append(", name='").append(name).append('\'');
sb.append(", phone='").append(phone).append('\'');
sb.append('}');
return sb.toString();
}
}
Respository
package com.mzj.springboot.jpa.exampleMatcher.repository;
import com.mzj.springboot.jpa.exampleMatcher.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface UserRespository extends JpaRepository<User,Long>, JpaSpecificationExecutor<User> {
}
application.properties
spring.datasource.url=jdbc:mysql://127.0.0.1/book?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
#此项过时,请替换为spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql= true
spring.thymeleaf.cache=false
server.port=8080
application
package com.mzj.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
数据库模拟假数据
postman访问URL:http://127.0.0.1:8080/lost/list
控制台输出:
其他说明
Matching生成的语句说明
- DEFAULT (case-sensitive) firstname = ?0 默认(大小写敏感)
- DEFAULT (case-insensitive) LOWER(firstname) = LOWER(?0) 默认(忽略大小写)
- EXACT (case-sensitive) firstname = ?0 精确匹配(大小写敏感)
- EXACT (case-insensitive) LOWER(firstname) = LOWER(?0) 精确匹配(忽略大小写)
- STARTING (case-sensitive) firstname like ?0 + ‘%’ 前缀匹配(大小写敏感)
- STARTING (case-insensitive) LOWER(firstname) like LOWER(?0) + ‘%’ 前缀匹配(忽略大小写)
- ENDING (case-sensitive) firstname like ‘%’ + ?0 后缀匹配(大小写敏感)
- ENDING (case-insensitive) LOWER(firstname) like ‘%’ + LOWER(?0) 后缀匹配(忽略大小写)
- CONTAINING (case-sensitive) firstname like ‘%’ + ?0 + ‘%’ 模糊查询(大小写敏感)
- CONTAINING (case-insensitive) LOWER(firstname) like ‘%’ + LOWER(?0) + ‘%’ 模糊查询(忽略大小写)
后话
使用一段时间之后,发现ExampleMatcher对日期的查询很不友好,不支持动态查询的,所以有了接下来研究的Specification复杂查询,可以了解一下。