【从零入门系列-3】Spring Boot 之 数据库操做

文章系列


前言

前一章简述了如何设计实现数据库实体类,本篇文章在此基础上进行开发,完成对该数据库表的经常使用操做,主要包括使用Spring Data JPA进行简单的增删改查和复杂查询操做。java

Spring Data JPASpring提供的一套简化JPA开发的框架,按照约定好的【方法命名规则】写dao层接口,就能够在不写接口实现的状况下,实现对数据库的访问和操做,同时提供了不少除了CRUD以外的功能,如分页、排序、复杂查询等等,Spring Data JPA 能够理解为 JPA 规范的再次封装抽象,底层仍是使用了 Hibernate 的 JPA 技术实现。经过引入Spring Data JPA后,咱们能够基本不用写代码就能实现对数据库的增删改查操做。程序员

此外,因为Spring Data JPA自带实现了不少内置的后台操做方法,所以在调用方法时必须根据其规范使用,深入理解规范约定算法


表的基本操做实现(CRUD)

在这里,先介绍一下JpaRepository,这是类型为interface的一组接口规范,是基于JPA的Repository接口,可以极大地减小访问数据库的代码编写,是实现Spring Data JPA技术访问数据库的关键接口。spring

  • 编写数据操做接口

在使用时,咱们只须要定义一个继承该接口类型的接口便可实现对表的基本操做方法,在此咱们须要对实体类Book进行操做,所以在Dao目录上右键New->Java Class,而后设置名称为BookJpaRepository,kind类型选Interface便可,而后添加注解及继承自JpaRepository,文件BookJpaRepository.java内容以下所示:sql

package com.arbboter.demolibrary.Dao;

import com.arbboter.demolibrary.Domain.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface BookJpaRepository extends JpaRepository<Book, Integer> {
}

@Repository持久层组件,用于标注数据访问组件,即DAO组件,此时配合上上一篇文章中的JPA配置,咱们就能够进行增删改查啦,不用添加任何其余代码,由于JpaRepository已经帮咱们实现好了。数据库

  • 编写测试用例代码

打开框架自动生成的测试代码文件DemoLibraryApplicationTests.java编写测试用例,测试增删改查效果,测试代码以下:segmentfault

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoLibraryApplicationTests {
    /**
     * @Autowired 注释,它能够对类成员变量、方法及构造函数进行标注,完成自动装配的工做。 
     * 经过 @Autowired的使用来消除 set ,get方法,简化程序代码
     * 此处自动装配咱们实现的BookJpaRepository接口,而后能够直接使用bookJpaRepository操做数据库
     * 若是不加@Autowired,直接使用bookJpaRepository,程序运行会抛出异常
     */
    @Autowired
    private BookJpaRepository bookJpaRepository;

    @Test
    public void contextLoads() {
        Book book = new Book();

        // 增
        book.setName("Spring Boot 入门学习实践");
        book.setAuthor("arbboter");
        book.setImage("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=2656353677,2997395625&fm=26&gp=0.jpg");
        bookJpaRepository.save(book);
        System.out.println("保存数据成功:" + book);

        // 查
        book = bookJpaRepository.findById(book.getId()).get();
        System.out.println("新增后根据ID查询结果:" + book);

        // 修改
        book.setName("Spring Boot 入门学习实践(修改版)");
        bookJpaRepository.save(book);
        System.out.println("修改后根据ID查询结果:" + book);

        // 删除
        bookJpaRepository.deleteById(book.getId());
    }
}

注意在测试代码用须要对属性bookJpaRepository使用@Autowired 自动注入实现初始化,@Autowired 注解,它能够对类成员变量、方法及构造函数进行标注,完成自动装配的工做。数组

  • 测试结果

1557817269112
经过测试结果咱们能够看到,程序已经可以对表数据进行增删改查,且咱们经过删除SQL能够观察到,获取生成记录ID的SQL语句为:框架

Hibernate: select next_val as id_val from hibernate_sequence with (updlock, holdlock, rowlock)
Hibernate: update hibernate_sequence set next_val= ? where next_val=?

所以可推断出,JpaRepository对默认的自增ID均使用表hibernate_sequence做为ID生成器,全部默认的ID表公用此ID生成器。ide

经过上述例子,咱们能够发现,虽然咱们没有写任何一条SQL语句,可是程序已经能够正常操做数据库了,这对苦逼的C++程序员手写SQL来讲真是不要说太幸福哈。不过上述示例也存在一些问题,数据查询均是经过ID操做的,可是实际使用中,数据查询还须要根据其余条件,好比书名做者,是否是须要手写SQL实现?答案是否认的,JpaRepository支持接口规范方法名查询,意思是若是在接口中定义的查询方法符合它的命名规则,就能够不用写实现,框架自动提供实现的方法,只须要声明无需本身实现便可使用。


JpaRepository的规范方法名查询

在咱们实现的接口中,能够只定义查询方法,若是是符合规范的,能够不用写实现,就能够直接使用。

JpaRepository会对方法名进行校验,不符合规范会报错,除非添加@Query注解。

在本示例中,咱们但愿经过书名和做者的经常使用场景提供查询方案,可按下述实现:

@Repository
public interface BookJpaRepository extends JpaRepository<Book, Integer> {
    /**
     * 根据书名精准查询书籍列表
     * @param name 查询的书名
     * @return 名字为name的书籍列表
     */
    List<Book> findByName(String name);

    /**
     *
     * 根据书名模糊查询书籍列表
     * @param name 查询的书名
     * @return 查询结果
     */
    List<Book> findByNameLike(String name);

    /**
     * 根据书名和做者查询,注意参数列表顺序和名字顺序保持一致(约定!)
     * @param name 查询的书名
     * @param author 查询的做者名
     * @return 查询结果
     */
    List<Book> findByNameAndAuthor(String name, String author);

    /**
     * 根据书名或做者查询,注意参数列表顺序和名字顺序保持一致(约定!)
     * @param name 查询的书名
     * @param author 查询的做者名
     * @return 查询结果
     */
    List<Book> findByNameOrAuthor(String name, String author);

    /**
     * 根据做者集合查询
     * @param authors 书列表名
     * @return
     */
    List<Book> findByAuthorIn(Collection authors);
}

上述代码通,咱们实现了模糊、精准、And和Or以及In的查询定义,都是根据JPA的命名规范定义方法,此时咱们不用本身去实现方法,直接能够调用。

测试代码以下:

// 模拟数据
for (int i=0; i<20; i++){
    Book b = new Book();
    b.setName("书名_" + i);
    b.setAuthor("做者_" + i%5);
    b.setImage("img" + i);
    bookJpaRepository.save(b);
}

List<Book> bookList;
// 根据书名精准查询
bookList = bookJpaRepository.findByName("书名_2");
System.out.println("根据书名精准查询:" + bookList);

// 根据书名模糊查询
bookList = bookJpaRepository.findByNameLike("书名_2");
System.out.println("根据书名模糊查询:" + bookList);

// 根据书名和做者名查询
bookList = bookJpaRepository.findByNameAndAuthor("书名_2", "做者_2");
System.out.println("根据书名和做者名查询:" + bookList);

// 根据书名或做者名查询
bookList = bookJpaRepository.findByNameOrAuthor("书名_2", "做者_2");
System.out.println("根据书名或做者名查询:" + bookList);

// 根据做者名集合查询
Collection c = new ArrayList();
c.add("做者_1");
c.add("做者_3");
bookList = bookJpaRepository.findByAuthorIn(c);
System.out.println("根据做者名集合查询:" + bookList);

运行结果为:

Hibernate: select book0_.id as id1_0_, book0_.author as author2_0_, book0_.image as image3_0_, book0_.name as name4_0_ from library_book book0_ where book0_.name=?
根据书名精准查询:[Book{id=9, name='书名_2', author='做者_2', image='img2'}]
Hibernate: select book0_.id as id1_0_, book0_.author as author2_0_, book0_.image as image3_0_, book0_.name as name4_0_ from library_book book0_ where book0_.name like ? escape ?
根据书名模糊查询:[Book{id=9, name='书名_2', author='做者_2', image='img2'}]
Hibernate: select book0_.id as id1_0_, book0_.author as author2_0_, book0_.image as image3_0_, book0_.name as name4_0_ from library_book book0_ where book0_.name=? and book0_.author=?
根据书名和做者名查询:[Book{id=9, name='书名_2', author='做者_2', image='img2'}]
Hibernate: select book0_.id as id1_0_, book0_.author as author2_0_, book0_.image as image3_0_, book0_.name as name4_0_ from library_book book0_ where book0_.name=? or book0_.author=?
根据书名或做者名查询:[Book{id=9, name='书名_2', author='做者_2', image='img2'}, Book{id=14, name='书名_7', author='做者_2', image='img7'}, Book{id=19, name='书名_12', author='做者_2', image='img12'}, Book{id=24, name='书名_17', author='做者_2', image='img17'}]
Hibernate: select book0_.id as id1_0_, book0_.author as author2_0_, book0_.image as image3_0_, book0_.name as name4_0_ from library_book book0_ where book0_.author in (? , ?)
根据做者名集合查询:[Book{id=8, name='书名_1', author='做者_1', image='img1'}, Book{id=10, name='书名_3', author='做者_3', image='img3'}, Book{id=13, name='书名_6', author='做者_1', image='img6'}, Book{id=15, name='书名_8', author='做者_3', image='img8'}, Book{id=18, name='书名_11', author='做者_1', image='img11'}, Book{id=20, name='书名_13', author='做者_3', image='img13'}, Book{id=23, name='书名_16', author='做者_1', image='img16'}, Book{id=25, name='书名_18', author='做者_3', image='img18'}]

JpaRepository规范方法名查询规约说明:JpaRepository框架在进行方法名解析时,会先把方法名多余的前缀截取掉,好比 find、findBy、read、readBy、get、getBy,而后对剩下部分进行解析。

  • 方法关键字必须遵循彻底的驼峰形式,由于JPA的方法名称解析引擎算法是经过驼峰来解析的
  • 下划线能够被用来中断解析算法的语义,可是它是一个保留字,不建议使用
  • In和NotIn也能够将Collection的任何子类做为参数以及数组或可变参数。

JpaRepository的复杂查询

在咱们的图书管理系统中要提供查询功能,能够根据书籍ID、书名或者做者中的三个任意组合查询,且支持查询结果自定义分页和排序,这样的话,使用JpaRepository规范方法名查询可能就变得很复杂了,因为组合后方案不少,不可能每种方案区分对待,此时应该提供一种通用可自适应的方法来实现,具有动态构建相应的查询语句的能力。

Sppring Boot JPA经过JpaSpecificationExecutor提供复杂查询的能力,继承该接口后,重写接口Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);来实现自定义的接口查询条件。

1.继承JpaSpecificationExecutor

@Repository
public interface BookJpaRepository extends JpaRepository<Book, Integer>, JpaSpecificationExecutor {
    /*内容省略*/
}

在原有的BookJpaRepository补充继承JpaSpecificationExecutor便可。

2.建立Service接口

因为须要自实现toPredicate方法,因此这里把搜索查询功能实现放到Service层,在Service目录上右键New->Java Class建立BookService.java文件:

@Service
   public class BookService {
       @Autowired
       BookJpaRepository bookJpaRepository;
       /**
        * 搜索查询接口
        * @param para: 键值对包含name,id,author,pageSize,pageNumber,ordName,ordDir
        * @return
        */
       Page<Book> search(Map<String, String> para){
           return null;
       }
   }

上述代码中@Service注解该类为服务类。

3.重写toPredicate方法

// 构造查询条件
Specification<Book> specification = new Specification<Book>() {
    @Override
    public Predicate toPredicate(Root<Book> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
        List<Predicate> predicate = new ArrayList<>();

        // 根据支持参数列表获取查询参数
        String matchMode = para.getOrDefault("matchMode", "AND");
        List<String> bookFields =  Arrays.asList("id", "name", "author", "image");
        for (String p : bookFields){
            String buf = para.get(p);
            if(buf != null){
                if(matchMode == "LIKE") {
                    predicate.add(cb.like(root.get(p).as(String.class), "%" + buf + "%"));
                } else {
                    predicate.add(cb.equal(root.get(p).as(String.class), buf));
                }
            }
        }
        Predicate[] pre = new Predicate[predicate.size()];
        return query.where(predicate.toArray(pre)).getRestriction();
    }
};

该方法返回的Predicate即为查询条件。

4.分页排序及查询

// 分页排序
Integer pageNumber = para.get("pageNumber") == null ? 0:Integer.valueOf(para.get("pageNumber"));
Integer pageSize = para.get("pageSize") == null ? 10:Integer.valueOf(para.get("pageSize"));
Sort.Direction sortDir = para.getOrDefault("sortDir", "DESC") == "DESC" ? Sort.Direction.DESC : Sort.Direction.ASC;
String ordName = para.getOrDefault("ordName", "id");
Pageable pageable = PageRequest.of(pageNumber, pageSize, sortDir, ordName);

return bookJpaRepository.findAll(specification, pageable);

最后bookJpaRepository.findAll(specification, pageable)返回的结果即为查询结果

5.完整的搜索方法

@Service
public class BookService {
    @Autowired
    private BookJpaRepository bookJpaRepository;
    /**
     * 搜索查询接口
     * 默认值:pageSize-10 pageNumber-0 ordName-id sortDir-ASC matchMode-EQUAL
     * @param para: 键值对包含name,id,author,pageSize,pageNumber,ordName,sortDir,matchMode
     * @return
     */
    public Page<Book> search(Map<String, String> para){
        // 构造查询条件
        Specification<Book> specification = new Specification<Book>() {
            @Override
            public Predicate toPredicate(Root<Book> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                List<Predicate> predicate = new ArrayList<>();

                // 根据支持参数列表获取查询参数
                String matchMode = para.getOrDefault("matchMode", "AND");
                List<String> bookFields =  Arrays.asList("id", "name", "author", "image");
                for (String p : bookFields){
                    String buf = para.get(p);
                    if(buf != null){
                        if(matchMode == "LIKE") {
                            predicate.add(cb.like(root.get(p).as(String.class), "%" + buf + "%"));
                        } else {
                            predicate.add(cb.equal(root.get(p).as(String.class), buf));
                        }
                    }
                }
                Predicate[] pre = new Predicate[predicate.size()];
                return query.where(predicate.toArray(pre)).getRestriction();
            }
        };

        // 分页排序
        Integer pageNumber = para.get("pageNumber") == null ? 0:Integer.valueOf(para.get("pageNumber"));
        Integer pageSize = para.get("pageSize") == null ? 10:Integer.valueOf(para.get("pageSize"));
        Sort.Direction sortDir = para.getOrDefault("sortDir", "DESC") == "DESC" ? Sort.Direction.DESC : Sort.Direction.ASC;
        String ordName = para.getOrDefault("ordName", "id");
        Pageable pageable = PageRequest.of(pageNumber, pageSize, sortDir, ordName);

        return bookJpaRepository.findAll(specification, pageable);
    }
}

6.测试代码

@Autowired
private BookService bookService;

@Test
public void search(){
    Map<String, String> para = new HashMap<>();
    Page<Book> books = bookService.search(para);
    System.out.println("分页3-1降序查询:" + books.getTotalElements() + ",页元素数目:" + books.getNumberOfElements());

    para.put("sortDir", "DESC");
    para.put("pageSize", "3");
    para.put("pageNumber", "1");
    books = bookService.search(para);
    System.out.println("分页3-1降序查询:" + books.getTotalElements() + ",页元素数目:" + books.getNumberOfElements());

    para.put("author", "做者_2");
    para.put("pageNumber", "0");
    books = bookService.search(para);
    System.out.println("做者名为[做者_2]查询结果:" + books.getTotalElements() + ",页元素数目:" + books.getNumberOfElements());

    para.put("id", "9");
    books = bookService.search(para);
    System.out.println("做者为[做者_2] id为9的查询结果:" + books.getTotalElements() + ",页元素数目:" + books.getNumberOfElements());

    para.put("matchMode", "LIKE");
    books = bookService.search(para);
    System.out.println("做者为[做者_2] id为9的模糊查询结果:" + books.getTotalElements() + ",页元素数目:" + books.getNumberOfElements());
}

此处BookService对象采用@Autowired注解自动装配初始化,而后再测试代码中针对分页、查询模式都分别测试。

7.测试执行结果

2019-05-14 18:44:43.422  INFO 131236 --- [           main] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory
Hibernate: select TOP(?) book0_.id as id1_0_, book0_.author as author2_0_, book0_.image as image3_0_, book0_.name as name4_0_ from library_book book0_ where 1=1 order by book0_.id desc
Hibernate: select count(book0_.id) as col_0_0_ from library_book book0_ where 1=1
分页3-1降序查询:23,页元素数目:10
Hibernate: WITH query AS (SELECT inner_query.*, ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ FROM ( select TOP(?) book0_.id as id1_0_, book0_.author as author2_0_, book0_.image as image3_0_, book0_.name as name4_0_ from library_book book0_ where 1=1 order by book0_.id desc ) inner_query ) SELECT id1_0_, author2_0_, image3_0_, name4_0_ FROM query WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?
Hibernate: select count(book0_.id) as col_0_0_ from library_book book0_ where 1=1
分页3-1降序查询:23,页元素数目:3
Hibernate: select TOP(?) book0_.id as id1_0_, book0_.author as author2_0_, book0_.image as image3_0_, book0_.name as name4_0_ from library_book book0_ where book0_.author=? order by book0_.id desc
Hibernate: select count(book0_.id) as col_0_0_ from library_book book0_ where book0_.author=?
做者名为[做者_2]查询结果:4,页元素数目:3
Hibernate: select TOP(?) book0_.id as id1_0_, book0_.author as author2_0_, book0_.image as image3_0_, book0_.name as name4_0_ from library_book book0_ where cast(book0_.id as varchar(255))=? and book0_.author=? order by book0_.id desc
做者为[做者_2] id为9的查询结果:1,页元素数目:1
Hibernate: select TOP(?) book0_.id as id1_0_, book0_.author as author2_0_, book0_.image as image3_0_, book0_.name as name4_0_ from library_book book0_ where (cast(book0_.id as varchar(255)) like ?) and (book0_.author like ?) order by book0_.id desc
做者为[做者_2] id为9的模糊查询结果:2,页元素数目:2

结束语

本章节篇幅较长,简单介绍了下JPA的基本增删改查功能,并进一步介绍定义了JPA的规范方法名查询,最后引入JpaSpecificationExecutor经过搜索查询接口,阐述了复杂场景下的查询搜索。

下一篇内容将整合当前方法服务,编写控制层的接口,提供WEB服务接口,请继续关注。

相关文章
相关标签/搜索