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. 从数据库读取数据并动态生成easyui tree构结

    一. 数据库表结构 二.从后台读取数据库生成easyui tree结构的树 1.TreeNode树结点类(每个结点都包含easyui tree 的基本属性信息) import java.io.Seri ...

  2. 终于等到你: 图形化开源爬虫Hawk 3发布!

    超级图形化爬虫Hawk已经发布两年半时间了,2015年升级到第二版,收到上千条用户反馈(tucao),100多个红包,总共666块五毛~一直想攒着这笔钱,去北境之王天通苑的龙德商场买最心爱的阿迪王! ...

  3. git-------基础(一)

    更改连接仓库只用操作一次(先删后加) (1)git remote rm origin                                  //若本地已经关联了一个远程库,则先删除已关联的 ...

  4. 如何在自定义组件中使用v-model

    文章属于速记,有错误欢迎指出.风格什么的不喜勿喷. 先来一个组件,不用vue-model,正常父子通信 <!-- parent --> <template> <div c ...

  5. Redis未授权访问

    最近在做校招题目的时候发现有问到未授权访问,特此搭建了诸多未授权访问的环境并且一一复现并做简单总结.再次记录下来 环境介绍 0x00环境搭建 我这里用到的是Microsoft(R) Windows(R ...

  6. selenium webdriver (python)的基本用法一

    阅在线 AIP 文档:http://selenium.googlecode.com/git/docs/api/py/index.html目录一.selenium+python 环境搭建........ ...

  7. RESTful规范

    一. 什么是RESTful REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为“表征状态转移” REST从资源的角 ...

  8. Web前端原生JavaScript浅谈轮播图

    1.一直来说轮播图都是困扰刚进业内小白的一大难点,因为我们不仅需要自己作出一个比较完美的运动框架(虽然网上一抓一大把,但是哪有比自己做出来实现的有成就感,不是吗?^_^),还必须需要非常关键性的把握住 ...

  9. .deb软件包的安装和软件的卸载

    前言: .deb格式的软件包是Debian和Ubuntu等Linux发行版软件安装包的文件扩展名. 使用.deb格式软件安装包安装软件 命令如下: sudo dpkg -i package_file. ...

  10. python3 [爬虫实战] selenium 爬取安居客

    我们爬取的网站:https://www.anjuke.com/sy-city.html 获取的内容:包括地区名,地区链接: 安居客详情 一开始直接用requests库进行网站的爬取,会访问不到数据的, ...