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

补习系列(19)-springboot JPA + PostGreSQL的更多相关文章

  1. 补习系列(15)-springboot 分布式会话原理

    目录 一.背景 二.SpringBoot 分布式会话 三.样例程序 四.原理进阶 A. 序列化 B. 会话代理 C. 数据老化 小结 一.背景 在 补习系列(3)-springboot 几种scope ...

  2. 补习系列(14)-springboot redis 整合-数据读写

    目录 一.简介 二.SpringBoot Redis 读写 A. 引入 spring-data-redis B. 序列化 C. 读写样例 三.方法级缓存 四.连接池 小结 一.简介 在 补习系列(A3 ...

  3. 补习系列(18)-springboot H2 迷你数据库

    目录 关于 H2 一.H2 用作本地数据库 1. 引入依赖: 2. 配置文件 3. 样例数据 二.H2 用于单元测试 1. 依赖包 2. 测试配置 3. 测试代码 小结 关于 H2 H2 数据库是一个 ...

  4. 补习系列(16)-springboot mongodb 数据库应用技巧

    目录 一.关于 MongoDB 二.Spring-Data-Mongo 三.整合 MongoDB CRUD A. 引入框架 B. 数据库配置 C. 数据模型 D. 数据操作 E. 自定义操作 四.高级 ...

  5. 补习系列(9)-springboot 定时器,你用对了吗

    目录 简介 一.应用启动任务 二.JDK 自带调度线程池 三.@Scheduled 定制 @Scheduled 线程池 四.@Async 定制 @Async 线程池 小结 简介 大多数的应用程序都离不 ...

  6. 补习系列(2)-springboot mime类型处理

    目标 了解http常见的mime类型定义: 如何使用springboot 处理json请求及响应: 如何使用springboot 处理 xml请求及响应: http参数的获取及文件上传下载: 如何获得 ...

  7. 补习系列(1)-springboot项目基础搭建课

    目录 前言 一.基础结构 二.添加代码 三.应用配置 四.日志配置 五.打包部署 小结 前言 springboot 最近火的不行,目前几乎已经是 spring 家族最耀眼的项目了.抛开微服务.技术社区 ...

  8. 补习系列(17)-springboot mongodb 内嵌数据库

    目录 简介 一.使用 flapdoodle.embed.mongo A. 引入依赖 B. 准备测试类 C. 完善配置 D. 启动测试 细节 二.使用Fongo A. 引入框架 B. 准备测试类 C.业 ...

  9. 补习系列(13)-springboot redis 与发布订阅

    目录 一.订阅发布 常见应用 二.Redis 与订阅发布 三.SpringBoot 与订阅发布 A. 消息模型 B. 序列化 C. 发布消息 D. 接收消息 小结 一.订阅发布 订阅发布是一种常见的设 ...

随机推荐

  1. 如何更好的编写async函数

    2018年已经到了5月份,node的4.x版本也已经停止了维护 我司的某个服务也已经切到了8.x,目前正在做koa2.x的迁移 将之前的generator全部替换为async 但是,在替换的过程中,发 ...

  2. 《T-SQL查询》读书笔记Part 2.执行计划

    一.关于执行计划 执行计划是优化器生成的用于确定如何处理一个给定查询的“工作计划”.一个计划包含一组运算符,通常按照特定的顺序来应用这些运算符.此外,一些运算符可以在它们之前的运算符还在处理时被应用( ...

  3. 洛谷 P1490 解题报告

    P1490 买蛋糕 题目描述 野猫过生日,大家当然会送礼物了(咳咳,没送礼物的同志注意了哈!!),由于不知道送什么好,又考虑到实用性等其他问题,大家决定合伙给野猫买一个生日蛋糕.大家不知道最后要买的蛋 ...

  4. GPU渲染流水线的简单概括

    GPU流水线 主要分为两个阶段:几何阶段和光栅化阶段   几何阶段      顶点着色器 --> 曲面细分着色器(可选)----->几何着色器(可选)----->裁剪-->屏幕 ...

  5. ActiveMQ的消息持久化机制

    为了避免意外宕机以后丢失信息,需要做到重启后可以恢复消息队列,消息系统一般都会采用持久化机制. ActiveMQ的消息持久化机制有JDBC,AMQ,KahaDB和LevelDB,无论使用哪种持久化方式 ...

  6. Spring Security 源码分析(四):Spring Social实现微信社交登录

    社交登录又称作社会化登录(Social Login),是指网站的用户可以使用腾讯QQ.人人网.开心网.新浪微博.搜狐微博.腾讯微博.淘宝.豆瓣.MSN.Google等社会化媒体账号登录该网站. 前言 ...

  7. 映射内网ftp服务器到公网报错问题解决

    这两天公司测试环境有个需求要让合作方通过ftp推送数据,一般内网环境是不会对公网开放ftp服务的,但是因为是临时需求就帮着搭了ftp服务,并且做了公网映射.ftp服务搭好之后在内网访问正常,但是在公网 ...

  8. Elasticsearch笔记五之java操作es

    Java操作es集群步骤1:配置集群对象信息:2:创建客户端:3:查看集群信息 1:集群名称 默认集群名为elasticsearch,如果集群名称和指定的不一致则在使用节点资源时会报错. 2:嗅探功能 ...

  9. MySQL 开发实践

    最近研发的项目对DB依赖比较重,梳理了这段时间使用MySQL遇到的8个比较具有代表性的问题,答案也比较偏自己的开发实践,没有DBA专业和深入,有出入的请使劲拍砖!- MySQL读写性能是多少,有哪些性 ...

  10. BZOJ_1433_[ZJOI2009]假期的宿舍_二分图匹配

    BZOJ_1433_[ZJOI2009]假期的宿舍_二分图匹配 题意: 学校放假了······有些同学回家了,而有些同学则有以前的好朋友来探访,那么住宿就是一个问题.比如A 和B都是学校的学生,A要回 ...