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篇" ,期待更多精彩內容-