補習系列(19)-springboot JPA + PostGreSQL

SpringBoot 整合 PostGreSQL

1、PostGreSQL簡介

PostGreSQL是一個功能強大的開源對象關係數據庫管理系統(ORDBMS),號稱世界上最早進的開源關係型數據庫
通過長達15年以上的積極開發和不斷改進,PostGreSQL已在可靠性、穩定性、數據一致性等得到了很大的提高。
對比時下最流行的 MySQL 來講,PostGreSQL 擁有更靈活,更高度兼容標準的一些特性。
此外,PostGreSQL基於MIT開源協議,其開放性極高,這也是其成爲各個雲計算大T 主要的RDS數據庫的根本緣由。git

從DBEngine的排名上看,PostGreSQL排名第四,且保持着高速的增加趨勢,很是值得關注。
這篇文章,以整合SpringBoot 爲例,講解如何在常規的 Web項目中使用 PostGreSQL。spring

2、關於 SpringDataJPA

JPA 是指 Java Persistence API,即 Java 的持久化規範,一開始是做爲 JSR-220 的一部分。
JPA 的提出,主要是爲了簡化 Java EE 和 Java SE 應用開發工做,統一當時的一些不一樣的 ORM 技術。
通常來講,規範只是定義了一套運做的規則,也就是接口,而像咱們所熟知的Hibernate 則是 JPA 的一個實現(Provider)。sql

JPA 定義了什麼,大體有:數據庫

  • ORM 映射元數據,用來將對象與表、字段關聯起來
  • 操做API,即完成增刪改查的一套接口
  • JPQL 查詢語言,實現一套可移植的面向對象查詢表達式

要體驗 JPA 的魅力,能夠從Spring Data JPA 開始。apache

SpringDataJPA 是 SpringFramework 對 JPA 的一套封裝,主要呢,仍是爲了簡化數據持久層的開發。
好比:api

  • 提供基礎的 CrudRepository 來快速實現增刪改查
  • 提供一些更靈活的註解,如@Query、@Transaction

基本上,SpringDataJPA 幾乎已經成爲 Java Web 持久層的必選組件。更多一些細節能夠參考官方文檔:緩存

https://docs.spring.io/spring-data/jpa/docs/1.11.0.RELEASE/reference/htmltomcat

接下來的篇幅,將演示 JPA 與 PostGreSQL 的整合實例。springboot

3、整合 PostGreSQL

這裏假定你已經安裝好數據庫,並已經建立好一個 SpringBoot 項目,接下來需添加依賴:

A. 依賴包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>${spring-boot.version}</version>
</dependency>

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

經過spring-boot-stater-data-jpa,能夠間接引入 spring-data-jpa的配套版本;
爲了使用 PostGreSQL,則須要引入 org.postgresql.postgresql 驅動包。

B. 配置文件

編輯 application.properties,以下:

## 數據源配置 (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url=jdbc:postgresql://localhost:5432/appdb
spring.datasource.username=appuser
spring.datasource.password=appuser

# Hibernate 原語
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect

# DDL 級別 (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update

其中,spring.jpa.hibernate.ddl-auto 指定爲 update,這樣框架會自動幫咱們建立或更新表結構。

C. 模型定義

咱們以書籍信息來做爲實例,一本書會有標題、類型、做者等屬性,對應於表的各個字段。
這裏爲了演示多對一的關聯,咱們還會定義一個Author(做者信息)實體,書籍和實體經過一個外鍵(author_id)關聯

Book 類

@Entity
@Table(name = "book")
public class Book extends AuditModel{

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(min = 1, max = 50)
    private String type;

    @NotBlank
    @Size(min = 3, max = 100)
    private String title;

    @Column(columnDefinition = "text")
    private String description;

    @Column(name = "fav_count")
    private int favCount;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "author_id", nullable = false)
    private Author author;

    //省略 get/set

這裏,咱們用了一系列的註解,好比@Table、@Column分別對應了數據庫的表、列。
@GeneratedValue 用於指定ID主鍵的生成方式,GenerationType.IDENTITY 指採用數據庫原生的自增方式,
對應到 PostGreSQL則會自動採用 BigSerial 作自增類型(匹配Long 類型)

@ManyToOne 描述了一個多對一的關係,這裏聲明瞭其關聯的"做者「實體,LAZY 方式指的是當執行屬性訪問時才真正去數據庫查詢數據;
@JoinColumn 在這裏配合使用,用於指定其關聯的一個外鍵。

Book 實體的屬性:

屬性 描述
id 書籍編號
type 書籍分類
title 書籍標題
description 書籍描述
favCount 收藏數
author 做者

Author信息

@Entity
@Table(name = "author")
public class Author extends AuditModel{

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(min = 1, max = 100)
    private String name;

    @Size(max = 400)
    private String hometown;

審計模型

注意到兩個實體都繼承了AuditModel這個類,這個基礎類實現了"審計"的功能。

審計,是指對數據的建立、變動等生命週期進行審閱的一種機制,
一般審計的屬性包括 建立時間、修改時間、建立人、修改人等信息

AuditModel的定義以下所示:

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditModel implements Serializable {
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_at", nullable = false, updatable = false)
    @CreatedDate
    private Date createdAt;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "updated_at", nullable = false)
    @LastModifiedDate
    private Date updatedAt;

上面的審計實體包含了 createAt、updateAt 兩個日期類型字段,@CreatedDate、@LastModifiedDate分別對應了各自的語義,仍是比較容易理解的。
@Temporal 則用於聲明日期類型對應的格式,如TIMESTAMP會對應 yyyy-MM-dd HH:mm:ss的格式,而這個也會被體現到DDL中。
@MappedSuperClass 是必須的,目的是爲了讓子類定義的表能擁有繼承的字段(列)

審計功能的「魔力」在於,添加了這些繼承字段以後,對象在建立、更新時會自動刷新這幾個字段,這些是由框架完成的,應用並不須要關心。
爲了讓審計功能生效,須要爲AuditModel 添加 @EntityListeners(AuditingEntityListener.class)聲明,同時還應該爲SpringBoot 應用聲明啓用審計:

@EnableJpaAuditing
@SpringBootApplication
public class BootJpa {
    ...

D. 持久層

持久層基本是繼承於 JpaRepository或CrudRepository的接口。
以下面的代碼:

***AuthorRepository

@Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {
}

*** BookRepository ***

@Repository
public interface BookRepository extends JpaRepository<Book, Long>{

    List<Book> findByType(String type, Pageable request);

    @Transactional
    @Modifying
    @Query("update Book b set b.favCount = b.favCount + ?2 where b.id = ?1")
    int incrFavCount(Long id, int fav);
}

findByType 實現的是按照 類型(type) 進行查詢,這個方法將會被自動轉換爲一個JPQL查詢語句。
並且,SpringDataJPA 已經能夠支持大部分經常使用場景,能夠參考這裏
incrFavCount 實現了收藏數的變動,除了使用 @Query 聲明瞭一個update 語句以外,@Modify用於標記這是一個「產生變動的查詢」,用於通知EntityManager及時清除緩存。
@Transactional 在這裏是必須的,不然會提示 TransactionRequiredException這樣莫名其妙的錯誤。

E. Service 層

Service 的實現相對簡單,僅僅是調用持久層實現數據操做。

@Service
public class BookService {

    @Autowired
    private BookRepository bookRepository;

    @Autowired
    private AuthorRepository authorRepository;


    /**
     * 建立做者信息
     *
     * @param name
     * @param hometown
     * @return
     */
    public Author createAuthor(String name, String hometown) {

        if (StringUtils.isEmpty(name)) {
            return null;
        }

        Author author = new Author();
        author.setName(name);
        author.setHometown(hometown);

        return authorRepository.save(author);
    }

    /**
     * 建立書籍信息
     *
     * @param author
     * @param type
     * @param title
     * @param description
     * @return
     */
    public Book createBook(Author author, String type, String title, String description) {

        if (StringUtils.isEmpty(type) || StringUtils.isEmpty(title) || author == null) {
            return null;
        }

        Book book = new Book();
        book.setType(type);
        book.setTitle(title);
        book.setDescription(description);

        book.setAuthor(author);
        return bookRepository.save(book);
    }


    /**
     * 更新書籍信息
     *
     * @param bookId
     * @param type
     * @param title
     * @param description
     * @return
     */
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.SERIALIZABLE, readOnly = false)
    public boolean updateBook(Long bookId, String type, String title, String description) {
        if (bookId == null || StringUtils.isEmpty(title)) {
            return false;
        }

        Book book = bookRepository.findOne(bookId);
        if (book == null) {
            return false;
        }

        book.setType(type);
        book.setTitle(title);
        book.setDescription(description);
        return bookRepository.save(book) != null;
    }

    /**
     * 刪除書籍信息
     *
     * @param bookId
     * @return
     */
    public boolean deleteBook(Long bookId) {
        if (bookId == null) {
            return false;
        }


        Book book = bookRepository.findOne(bookId);
        if (book == null) {
            return false;
        }
        bookRepository.delete(book);
        return true;
    }

    /**
     * 根據編號查詢
     *
     * @param bookId
     * @return
     */
    public Book getBook(Long bookId) {
        if (bookId == null) {
            return null;
        }
        return bookRepository.findOne(bookId);
    }

    /**
     * 增長收藏數
     *
     * @return
     */
    public boolean incrFav(Long bookId, int fav) {

        if (bookId == null || fav <= 0) {
            return false;
        }
        return bookRepository.incrFavCount(bookId, fav) > 0;
    }

    /**
     * 獲取分類下書籍,按收藏數排序
     *
     * @param type
     * @return
     */
    public List<Book> listTopFav(String type, int max) {

        if (StringUtils.isEmpty(type) || max <= 0) {
            return Collections.emptyList();
        }

        // 按投票數倒序排序
        Sort sort = new Sort(Sort.Direction.DESC, "favCount");
        PageRequest request = new PageRequest(0, max, sort);

        return bookRepository.findByType(type, request);
    }
}

4、高級操做

前面的部分已經完成了基礎的CRUD操做,但在正式的項目中每每會須要一些定製作法,下面作幾點介紹。

1. 自定義查詢

使用 findByxxx 這樣的方法映射已經能夠知足大多數的場景,但若是是一些"不肯定"的查詢條件呢?
咱們知道,JPA 定義了一套的API來幫助咱們實現靈活的查詢,經過EntityManager 能夠實現各類靈活的組合查詢。
那麼在 Spring Data JPA 框架中該如何實現呢?

首先建立一個自定義查詢的接口:

public interface BookRepositoryCustom {
    public PageResult<Book> search(String type, String title, boolean hasFav, Pageable pageable);
}

接下來讓 BookRepository 繼承於該接口:

@Repository
public interface BookRepository extends JpaRepository<Book, Long>, BookRepositoryCustom {
    ...

最終是 實現這個自定義接口,經過 AOP 的"魔法",框架會將咱們的實現自動嫁接到接口實例上。
具體的實現以下:

public class BookRepositoryImpl implements BookRepositoryCustom {

    private final EntityManager em;

    @Autowired
    public BookRepositoryImpl(JpaContext context) {
        this.em = context.getEntityManagerByManagedType(Book.class);
    }

    @Override
    public PageResult<Book> search(String type, String title, boolean hasFav, Pageable pageable) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery cq = cb.createQuery();

        Root<Book> root = cq.from(Book.class);

        List<Predicate> conds = new ArrayList<>();

        //按類型檢索
        if (!StringUtils.isEmpty(type)) {
            conds.add(cb.equal(root.get("type").as(String.class
            ), type));
        }

        //標題模糊搜索
        if (!StringUtils.isEmpty(title)) {
            conds.add(cb.like(root.get("title").as(String.class
            ), "%" + title + "%"));
        }

        //必須被收藏過
        if (hasFav) {
            conds.add(cb.gt(root.get("favCount").as(Integer.class
            ), 0));
        }

        //count 數量
        cq.select(cb.count(root)).where(conds.toArray(new Predicate[0]));
        Long count = (Long) em.createQuery(cq).getSingleResult();

        if (count <= 0) {
            return PageResult.empty();
        }

        //list 列表
        cq.select(root).where(conds.toArray(new Predicate[0]));

        //獲取排序
        List<Order> orders = toOrders(pageable, cb, root);

        if (!CollectionUtils.isEmpty(orders)) {
            cq.orderBy(orders);
        }


        TypedQuery<Book> typedQuery = em.createQuery(cq);

        //設置分頁
        typedQuery.setFirstResult(pageable.getOffset());
        typedQuery.setMaxResults(pageable.getPageSize());

        List<Book> list = typedQuery.getResultList();

        return PageResult.of(count, list);

    }

    private List<Order> toOrders(Pageable pageable, CriteriaBuilder cb, Root<?> root) {

        List<Order> orders = new ArrayList<>();
        if (pageable.getSort() != null) {
            for (Sort.Order o : pageable.getSort()) {
                if (o.isAscending()) {
                    orders.add(cb.asc(root.get(o.getProperty())));
                } else {
                    orders.add(cb.desc(root.get(o.getProperty())));
                }
            }
        }

        return orders;
    }

}

2. 聚合

聚合功能能夠用 SQL 實現,但經過JPA 的 Criteria API 會更加簡單。
與實現自定義查詢的方法同樣,也是經過EntityManager來完成操做:

public List<Tuple> groupCount(){
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery cq = cb.createQuery();

    Root<Book> root = cq.from(Book.class);

    Path<String> typePath = root.get("type");

    //查詢type/count(*)/sum(favCount)
    cq.select(cb.tuple(typePath,cb.count(root).alias("count"), cb.sum(root.get("favCount"))));
    //按type分組
    cq.groupBy(typePath);
    //按數量排序
    cq.orderBy(cb.desc(cb.literal("count")));

    //查詢出元祖
    TypedQuery<Tuple> typedQuery = em.createQuery(cq);
    return typedQuery.getResultList();
}

上面的代碼中,會按書籍的分組統計數量,且按數量降序返回。
等價於下面的SQL:

···
select type, count(*) as count , sum(fav_count) from book
group by type order by count;
···

3. 視圖

視圖的操做與表基本是相同的,只是視圖通常是隻讀的(沒有更新操做)。
執行下面的語句能夠建立一個視圖:

create view v_author_book as
 select b.id, b.title, a.name as author_name, 
        a.hometown as author_hometown, b.created_at
   from author a, book b
   where a.id = b.author_id;

在代碼中使用@Table來進行映射:

@Entity
@Table(name = "v_author_book")
public class AuthorBookView {

    @Id
    private Long id;
    private String title;

    @Column(name = "author_name")
    private String authorName;
    @Column(name = "author_hometown")
    private String authorHometown;

    @Column(name = "created_at")
    private Date createdAt;

建立一個相應的Repository:

@Repository
public interface AuthorBookViewRepository extends JpaRepository<AuthorBookView, Long> {

}

這樣就能夠進行讀寫了。

4. 鏈接池

在生產環境中通常須要配置合適的鏈接池大小,以及超時參數等等。
這些須要經過對數據源(DataSource)進行配置來實現,DataSource也是一個抽象定義,默認狀況下SpringBoot 1.x會使用Tomcat的鏈接池。

以Tomcat的鏈接池爲例,配置以下:

spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource

# 初始鏈接數
spring.datasource.tomcat.initial-size=15
# 獲取鏈接最大等待時長(ms)
spring.datasource.tomcat.max-wait=20000
# 最大鏈接數
spring.datasource.tomcat.max-active=50
# 最大空閒鏈接
spring.datasource.tomcat.max-idle=20
# 最小空閒鏈接
spring.datasource.tomcat.min-idle=15
# 是否自動提交事務
spring.datasource.tomcat.default-auto-commit=true

這裏能夠找到一些詳盡的參數

5. 事務

SpringBoot 默認狀況下會爲咱們開啓事務的支持,引入 spring-starter-data-jpa 的組件將會默認使用 JpaTransactionManager 用於事務管理。
在業務代碼中使用@Transactional 能夠聲明一個事務,以下:

@Transactional(propagation = Propagation.REQUIRED, 
        isolation = Isolation.DEFAULT, 
        readOnly = false, 
        rollbackFor = Exception.class)
public boolean updateBook(Long bookId, String type, String title, String description) {
...

爲了演示事務的使用,上面的代碼指定了幾個關鍵屬性,包括:

  • propagation 傳遞行爲,指事務的建立或嵌套處理,默認爲 REQUIRED
選項 描述
REQUIRED 使用已存在的事務,若是沒有則建立一個。
MANDATORY 若是存在事務則加入,若是沒有事務則報錯。
REQUIRES_NEW 建立一個事務,若是已存在事務會將其掛起。
NOT_SUPPORTED 以非事務方式運行,若是當前存在事務,則將其掛起。
NEVER 以非事務方式運行,若是當前存在事務,則拋出異常。
NESTED 建立一個事務,若是已存在事務,新事務將嵌套執行。
  • isolation 隔離級別,默認值爲DEFAULT
級別 描述
DEFAULT 默認值,使用底層數據庫的默認隔離級別。大部分等於READ_COMMITTED
READ_UNCOMMITTED 未提交讀,一個事務能夠讀取另外一個事務修改但尚未提交的數據。不能防止髒讀和不可重複讀。
READ_COMMITTED 已提交讀,一個事務只能讀取另外一個事務已經提交的數據。能夠防止髒讀,大多數狀況下的推薦值。
REPEATABLE_READ 可重複讀,一個事務在整個過程當中能夠屢次重複執行某個查詢,而且每次返回的記錄都相同。能夠防止髒讀和不可重複讀。
SERIALIZABLE 串行讀,全部的事務依次逐個執行,這樣事務之間就徹底不可能產生干擾,能夠防止髒讀、不可重複讀以及幻讀。性能低。
  • readOnly
    指示當前事務是否爲只讀事務,默認爲false

  • rollbackFor
    指示當捕獲什麼類型的異常時會進行回滾,默認狀況下產生 RuntimeException 和 Error 都會進行回滾(受檢異常除外)

碼雲同步代碼

參考文檔
https://www.baeldung.com/spring-boot-tomcat-connection-pool
https://www.baeldung.com/transaction-configuration-with-jpa-and-spring
https://www.callicoder.com/spring-boot-jpa-hibernate-postgresql-restful-crud-api-example/
https://docs.spring.io/spring-data/jpa/docs/1.11.0.RELEASE/reference/html/#projections
https://www.cnblogs.com/yueshutong/p/9409295.html

小結

本篇文章描述了一個完整的 SpringBoot + JPA + PostGreSQL 開發案例,一些作法可供你們借鑑使用。
因爲 JPA 幫咱們簡化許多了數據庫的開發工做,使得咱們在使用數據庫時並不須要瞭解過多的數據庫的特性。
所以,本文也適用於整合其餘的關係型數據庫。
前面也已經提到過,PostGreSQL因爲其開源許可的開放性受到了雲計算大T的青睞,相信將來前景可期。在接下來將會更多的關注該數據庫的發展。

歡迎繼續關注"美碼師的補習系列-springboot篇" ,期待更多精彩內容^-^

相關文章
相關標籤/搜索