springboot 之 JPA

一、说明

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复杂查询,可以了解一下。

7、其他知识点

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值