【從零入門系列-3】Spring Boot 之 數據庫操作

【從零入門系列-3】Spring Boot 之 數據庫操作

文章系列


前言

前一章簡述瞭如何設計實現數據庫實體類,本篇文章在此基礎上進行開發,完成對該數據庫表的常用操作,主要包括使用Spring Data JPA進行簡單的增刪改查和複雜查詢操作。

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技術訪問數據庫的關鍵接口。

  • 編寫數據操作接口

在使用時,我們只需要定義一個繼承該接口類型的接口即可實現對錶的基本操作方法,在此我們需要對實體類Book進行操作,因此在Dao目錄上右鍵New->Java Class,然後設置名稱爲BookJpaRepository,kind類型選Interface即可,然後添加註解及繼承自JpaRepository,文件BookJpaRepository.java內容如下所示:

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編寫測試用例,測試增刪改查效果,測試代碼如下:

@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註解,它可以對類成員變量、方法及構造函數進行標註,完成自動裝配的工作。

  • 測試結果

[外鏈圖片轉存失敗(img-sGwiAwwL-1566186700190)(https://raw.githubusercontent.com/arbboter/resource/master/segmentfault/image/SpringBoot/20190514-%E6%95%B0%E6%8D%AE%E5%BA%93%E6%93%8D%E4%BD%9C/1557817269112.png)]
通過測試結果我們可以看到,程序已經能夠對錶數據進行增刪改查,且我們通過刪除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生成器。

通過上述例子,我們可以發現,雖然我們沒有寫任何一條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服務接口,請繼續關注。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章