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


SpringBoot 整合 PostGreSQL

一、PostGreSQL簡介

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

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

二、關於 SpringDataJPA

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

JPA 定義了什么,大致有:

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

要體驗 JPA 的魅力,可以從Spring Data JPA 開始。

SpringDataJPA 是 SpringFramework 對 JPA 的一套封裝,主要呢,還是為了簡化數據持久層的開發。
比如:

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

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

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

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

三、整合 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);
    }
}

四、高級操作

前面的部分已經完成了基礎的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篇" ,期待更多精彩內容-


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM