目錄html
PostGreSQL是一個功能強大的開源對象關係數據庫管理系統(ORDBMS),號稱世界上最早進的開源關係型數據庫
通過長達15年以上的積極開發和不斷改進,PostGreSQL已在可靠性、穩定性、數據一致性等得到了很大的提高。
對比時下最流行的 MySQL 來講,PostGreSQL 擁有更靈活,更高度兼容標準的一些特性。
此外,PostGreSQL基於MIT開源協議,其開放性極高,這也是其成爲各個雲計算大T 主要的RDS數據庫的根本緣由。git
從DBEngine的排名上看,PostGreSQL排名第四,且保持着高速的增加趨勢,很是值得關注。
這篇文章,以整合SpringBoot 爲例,講解如何在常規的 Web項目中使用 PostGreSQL。spring
JPA 是指 Java Persistence API,即 Java 的持久化規範,一開始是做爲 JSR-220 的一部分。
JPA 的提出,主要是爲了簡化 Java EE 和 Java SE 應用開發工做,統一當時的一些不一樣的 ORM 技術。
通常來講,規範只是定義了一套運做的規則,也就是接口,而像咱們所熟知的Hibernate 則是 JPA 的一個實現(Provider)。sql
JPA 定義了什麼,大體有:數據庫
要體驗 JPA 的魅力,能夠從Spring Data JPA 開始。apache
SpringDataJPA 是 SpringFramework 對 JPA 的一套封裝,主要呢,仍是爲了簡化數據持久層的開發。
好比:api
基本上,SpringDataJPA 幾乎已經成爲 Java Web 持久層的必選組件。更多一些細節能夠參考官方文檔:緩存
https://docs.spring.io/spring-data/jpa/docs/1.11.0.RELEASE/reference/htmltomcat
接下來的篇幅,將演示 JPA 與 PostGreSQL 的整合實例。springboot
這裏假定你已經安裝好數據庫,並已經建立好一個 SpringBoot 項目,接下來需添加依賴:
<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 驅動包。
編輯 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,這樣框架會自動幫咱們建立或更新表結構。
咱們以書籍信息來做爲實例,一本書會有標題、類型、做者等屬性,對應於表的各個字段。
這裏爲了演示多對一的關聯,咱們還會定義一個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 { ...
持久層基本是繼承於 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這樣莫名其妙的錯誤。
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操做,但在正式的項目中每每會須要一些定製作法,下面作幾點介紹。
使用 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; } }
聚合功能能夠用 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;
···
視圖的操做與表基本是相同的,只是視圖通常是隻讀的(沒有更新操做)。
執行下面的語句能夠建立一個視圖:
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> { }
這樣就能夠進行讀寫了。
在生產環境中通常須要配置合適的鏈接池大小,以及超時參數等等。
這些須要經過對數據源(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
從這裏能夠找到一些詳盡的參數
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) { ...
爲了演示事務的使用,上面的代碼指定了幾個關鍵屬性,包括:
選項 | 描述 |
---|---|
REQUIRED | 使用已存在的事務,若是沒有則建立一個。 |
MANDATORY | 若是存在事務則加入,若是沒有事務則報錯。 |
REQUIRES_NEW | 建立一個事務,若是已存在事務會將其掛起。 |
NOT_SUPPORTED | 以非事務方式運行,若是當前存在事務,則將其掛起。 |
NEVER | 以非事務方式運行,若是當前存在事務,則拋出異常。 |
NESTED | 建立一個事務,若是已存在事務,新事務將嵌套執行。 |
級別 | 描述 |
---|---|
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篇" ,期待更多精彩內容^-^