Spring-Boot-持久化最佳实践-五-

166 阅读42分钟

Spring Boot 持久化最佳实践(五)

原文:Spring Boot Persistence Best Practices

协议:CC BY-NC-SA 4.0

八、计算属性

项目 77:如何映射计算的非持久属性

此项是关于根据持久实体属性映射实体的计算非持久属性。它使用图 8-1 所示的Book实体映射。

img/487471_1_En_8_Fig1_HTML.jpg

图 8-1

图书类图

每本书都有一个价格,通过名为price的持久字段映射。并且,基于price,开发人员必须计算非持久字段discounted的值。这是打了折扣的价格。我们假设每本书都有 25%的折扣。换句话说,在加载了一个Book之后,getDiscounted()属性应该返回应用了折扣的价格,price - price * 0.25

JPA 快速方法

JPA 快速方法包括用 JPA @Transient注释getDiscounted()属性,如下所示:

@Transient
public double getDiscounted() {
    return this.price - this.price * 0.25;
}

这意味着每次调用getDiscounted()方法时都会执行计算。如果计算相当复杂(例如,依赖于其他计算)或者必须多次调用该属性,则此实现不如下面的实现有效。

JPA @PostLoad

更好的方法包括两个步骤:

  • @Transient注释discounted字段

  • 声明一个用@PostLoad注释的private方法,并计算discounted

在代码行中,这两个项目符号如下所示:

@Entity
public class Book implements Serializable {
    ...
    @Transient
    private double discounted;
    ...

    public double getDiscounted() {
        return discounted;
    }

    @PostLoad
    private void postLoad() {
        this.discounted = this.price - this.price * 0.25;
    }
}

有关 JPA 回调的更多细节,请考虑第 104 项。

这一次,计算是在实体加载后执行的。调用getDiscounted()将返回discounted的值,而无需在每次调用时重复计算。

完整的代码可以在 GitHub 1 上找到。

特定于 Hibernate 的@公式

discounted的计算也可以写成一个 SQL 查询表达式。为此,请依赖 Hibernate 特有的@Formula注释。下面的代码片段展示了如何在这种情况下使用@Formula:

@Entity
public class Book implements Serializable {
    ...
    @Formula("price - price * 0.25")
    private double discounted;
    ...

    @Transient
    public double getDiscounted() {
        return discounted;
    }
}

获取一个Book实体将通过下面的 SQL 语句完成(注意给定的公式是查询的一部分):

SELECT
  book0_.id AS id1_0_,
  book0_.isbn AS isbn2_0_,
  book0_.price AS price3_0_,
  book0_.title AS title4_0_,
  book0_.price - book0_.price * 0.25 AS formula0_
FROM book book0_

调用getDiscounted()将在查询时返回计算出的discounted值。

完整的代码可以在 GitHub 2 上找到。

这两种计算discounted的方法可以归入同一类。它们都假设加载了一个Book实体,并且使用它的持久属性来计算非持久字段discounted的值。主要区别在于公式的写法。@PostLoad是用 Java 写的,@Formula是用 SQL 查询表达式写的。

项目 78:如何通过@Generated 映射计算的持久属性

这一项是关于根据其他持久实体属性映射实体的计算持久属性。它使用图 8-2 所示的Book实体映射。

img/487471_1_En_8_Fig2_HTML.jpg

图 8-2

图书类图

每本书都有一个价格,通过名为price的持久字段映射。并且,基于price,开发人员必须计算持久字段discounted的值。这是打了折扣的价格。每本书都有 25%的折扣。换句话说,保持一个给定价格的Book应该保持discountedprice - price * 0.25。更新price也应该更新discounted字段。

此外,让我们看看如何在INSERT和/或UPDATE时间计算discounted

特定于 Hibernate 的@Generated

考虑到discountedBook实体的持久字段,必须基于price持久字段进行计算。因此,discounted字段被物化在book表的一列中,其值将在INSERT或/和UPDATE时间被计算。这是一个生成的列;它对于列就像视图对于表一样。

Hibernate 提供了@Generated注释。通过这个注释,开发人员指示 Hibernate(而不是数据库)何时计算相关的列值。该注释的值可以是GenerationTime.INSERT(仅INSERT时间)或GenerationTime.ALWAYS ( INSERTUPDATE时间)。如果不应该生成该值,GenerationTime.NEVER是合适的选择。

此外,您可以为discounted字段提供自定义的@Column定义。根据经验,生成的列不能通过INSERTUPDATE语句直接编写。在代码中,如下所示:

@Entity
public class Book implements Serializable {

    @Generated(value = GenerationTime.ALWAYS)
    @Column(insertable = false, updatable = false)
    private double discounted;
    ...
    public double getDiscounted() {
        return discounted;
    }
}

对于GenerationTime.INSERT,该栏应标注@Column(insertable = false)

discounted值的计算公式在哪里?有两种方法可以指定公式。

通过 columnDefinition 元素的公式

可以通过@Column注释的columnDefinition元素将公式指定为 SQL 查询表达式,如下所示:

@Generated(value = GenerationTime.ALWAYS)
@Column(insertable = false, updatable = false,
    columnDefinition = "double AS (price - price * 0.25)")
private double discounted;

如果数据库模式是从 JPA 注释(例如,spring.jpa.hibernate.ddl-auto=create)生成的,那么columnDefinition的存在将反映在CREATE TABLE查询中,如下所示(为GenerationTime.INSERT生成了相同的查询):

CREATE TABLE book (
    id BIGINT NOT NULL AUTO_INCREMENT,
    discounted DOUBLE AS (price - price * 0.25),
    isbn VARCHAR(255),
    price DOUBLE PRECISION NOT NULL,
    title VARCHAR(255),
    PRIMARY KEY (id)
)

依赖columnDefinition需要从 JPA 注释生成数据库模式;因此,这不能代表生产解决方案。在生产中,spring.jpa.hibernate.ddl-auto应该被禁用(未指定)或设置为validate,数据库迁移应该通过专用工具(如 Flyway 或 Liquibase)来管理。

显然,这适用于eclipselink.ddl-generation(特定于 EclipseLink 持久性提供者)和任何其他用于为数据库模式生成 DDL 的类似机制。这种机制应该只用于构建数据库模式的原型。

数据库(例如 MySQL、PostgreSQL)通常识别两种生成的列:存储的虚拟的。通过columnDefinition,该列将采用为所用数据库设置的默认值(在 MySQL 和 PostgreSQL 中,默认值为 virtual )。这两个概念将在下一节解释。

通过创建表的公式

在生产中,应该通过CREATE TABLE将公式指定为数据库模式的一部分,而不是在columnDefinition中。在 MySQL 中定义生成列的语法如下(对于其他数据库考虑事项,请阅读文档):

column_name data_type [GENERATED ALWAYS] AS (expression)
   [VIRTUAL | STORED] [UNIQUE [KEY]]

首先,指定列名及其数据类型。

接下来,添加可选的GENERATED ALWAYS子句,以指示该列是生成的列。实际上,AS (expression)表示生成了列,而可选的GENERATED ALWAYS只是以更明确的方式强调了这一点。没有GENERATED INSERT

列的类型可以是VIRTUALSTORED。默认情况下,如果没有明确指定类型,MySQL 会使用VIRTUAL:

  • VIRTUAL:不存储列值,但是在读取行时,在任何BEFORE触发器之后立即计算列值。一个虚拟列不占用任何存储空间(InnoDB 支持虚拟列上的二级索引)。

  • STORED:插入或更新行时,计算并存储列值。一个存储的列确实需要存储空间,并且可以被索引。

之后,指定表达式。表达式可以包含运算符、文字、不带参数的内置函数或对同一表中任何列的引用。函数必须是标量和确定性的。

最后,如果生成的列是存储的,您可以为它定义一个惟一的约束。

CREATE TABLE中指定公式比使用columnDefinition更灵活,并且通过专用工具(如 Flyway 或 Liquibase)保持数据库模式的可维护性。

一个 MySQL CREATE TABLE示例,用于存储生成的列,可以编写如下:

CREATE TABLE book (
  id BIGINT NOT NULL AUTO_INCREMENT,
  discounted DOUBLE GENERATED ALWAYS AS ((`price` - `price` * 0.25)) STORED,
  isbn VARCHAR(255),
  price DOUBLE PRECISION NOT NULL,
  title VARCHAR(255),
  PRIMARY KEY (id)
)

在本书捆绑的应用中,这个 DDL 是在schema-sql.sql中加入的。但是请记住,在生产中,您应该依赖 Flyway 或 Liquibase,它们提供了自动模式迁移。

测试时间

持久化$13.99的书将生成以下 SQL 语句:

INSERT INTO book (isbn, price, title)
  VALUES (?, ?, ?)
Binding:[001-AH, 13.99, Ancient History]

SELECT
  book_.discounted AS discount2_0_
FROM book book_
WHERE book_.id = ?
Binding:[1], Extracted:[10.4925]

在触发INSERT并刷新之后,Hibernate 会自动触发一个SELECT来获取计算出的discounted值。这是将托管实体与基础表行同步所必需的。调用getDiscounted()将返回 10.4925 。这就是@Generated的效果。

此外,让我们触发一个UPDATE,将新价格设置为 9.99 美元*。产生的 SQL 语句是:*

UPDATE book
SET isbn = ?,
    price = ?,
    title = ?
WHERE id = ?
Binding:[001-AH, 9.99, Ancient History, 1]

SELECT
  book_.discounted AS discount2_0_
FROM book book_
WHERE book_.id = ?
Binding:[1], Extracted:[7.4925]

在触发UPDATE并刷新之后,Hibernate 会自动触发一个SELECT来获取计算出的discounted值。这是将托管实体与基础表行同步所必需的。调用getDiscounted()将返回 7.4925 。这就是@Generated的效果。

完整的代码可以在 GitHub 3 上找到。

第 79 项:如何在 JPQL 查询中使用带多个参数的 SQL 函数

SQL 函数(MySQL、PostgreSQL 等)的存在。)可能会导致异常,如果 Hibernate 不能识别它们的话。

选择零件中的功能

例如,MySQL concat_ws()函数(用于通过分隔符/分隔符连接多个字符串)不被 Hibernate 识别。从 Hibernate 5.3(或者更准确地说,5.2.18)开始,这些函数可以通过MetadataBuilderContributor注册,并通过metadata_builder_contributor属性通知 Hibernate。

图 8-3 描绘了concat_ws()的一个使用案例。

img/487471_1_En_8_Fig3_HTML.jpg

图 8-3

MySQL concat_ws()函数

concat_ws()函数用于连接Booktitleprice(来自数据库),用空格分隔$符号和当前日期(来自应用)。

在 Spring 风格中,您可以通过@Query编写如下查询:

@Repository
@Transactional(readOnly = true)
public interface BookRepository extends JpaRepository<Book, Long> {

    @Query(value = "SELECT concat_ws(b.title, ?1, b.price, ?2) "
            + "FROM Book b WHERE b.id = 1")
    String fetchTitleAndPrice(String symbol, Instant instant);
}

在纯 JPA 风格中,您可以通过EntityManager编写如下查询:

@Repository
public class Dao<T, ID extends Serializable> implements GenericDao<T, ID> {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    @Transactional(readOnly = true)
    public String fetchTitleAndPrice(String symbol, Instant instant) {

        return (String) entityManager.createQuery(
            "SELECT concat_ws(b.title, :symbol, b.price, :instant) "
            + "FROM Book b WHERE b.id = 1"
        )
            .setParameter("symbol", symbol)
            .setParameter("instant", instant)
            .getSingleResult();
    }
}

但是,在通过MetadataBuilderContributor注册concat_ws()功能之前,这些尝试都不起作用,如下所示:

public class SqlFunctionsMetadataBuilderContributor
                    implements MetadataBuilderContributor {

    @Override
    public void contribute(MetadataBuilder metadataBuilder) {
        metadataBuilder.applySqlFunction(
            "concat_ws",
            new SQLFunctionTemplate(
                StandardBasicTypes.STRING,
                "concat_ws('  ', ?1, ?2, ?3, ?4)"
            )
        );
    }

}

与前面的示例类似,您可以注册任何其他 SQL 函数。比如你可以注册著名的date_trunc()如下:

@Override
public void contribute(MetadataBuilder metadataBuilder) {
    metadataBuilder.applySqlFunction(
        "date_trunc", new SQLFunctionTemplate(
            StandardBasicTypes.TIMESTAMP, "date_trunc('minute', ?1)"
        )
    );
}

最后,在application.properties中设置spring.jpa.properties.hibernate.metadata_builder_contributor,如下图:

spring.jpa.properties.hibernate.metadata_builder_contributor
    =com.bookstore.config.SqlFunctionsMetadataBuilderContributor

运行该代码将显示类似如下的输出:

A People's History  $  32  2019-07-16 11:17:49.949732

完整的代码可以在 GitHub 4 上找到。

WHERE 部分中的函数

在 JPA 2.1 中,可以在 JPQL 查询的WHERE部分使用函数,而无需注册函数。JPA 2.1 引入了function(),它采用以下参数:

  • 作为第一个参数调用的函数的名称

  • 函数的所有参数

让我们调用同一个concat_ws()函数,但是这次是在WHERE子句中:

@Transactional(readOnly = true)
@Query(value = "SELECT b FROM Book b WHERE b.isbn "
             + "= function('concat_ws', '-', ?1, ?2)")
Book fetchByIsbn(String code, String author);

从服务方法中调用fetchByIsbn()可以如下进行:

public Book fetchBookByIsbn() {
    return bookRepository.fetchByIsbn("001", "JN");
}

触发的 SQL 如下:

SELECT
  book0_.id AS id1_0_,
  book0_.isbn AS isbn2_0_,
  book0_.price AS price3_0_,
  book0_.title AS title4_0_
FROM book book0_
WHERE book0_.isbn = concat_ws('-', ?, ?)
Binding:[001, JN]

您可以按如下方式调用 SQL 函数(标准函数或自定义函数):

  • 在 JPQL 查询中,只要从这里引用标准函数 5

  • WHERE部分和 JPA 2.1 中,可以通过function()调用现成的 SQL 函数

  • SELECT部分,未识别的 SQL 函数必须被注册

搞定了。完整的应用可在 GitHub 6 上获得。

第 80 项:如何通过@JoinFormula 将@ManyToOne 关系映射到 SQL 查询

让我们考虑图 8-4 中的表格和图 8-5 中的数据所反映的单向@ManyToOne关系中涉及的AuthorBook实体。

img/487471_1_En_8_Fig5_HTML.jpg

图 8-5

数据快照

img/487471_1_En_8_Fig4_HTML.jpg

图 8-4

一对多表关系

这个场景要求你找出哪本书比给定的书便宜。换句话说,当通过 ID 获取一本书(姑且称之为书 A )时,您想要获取另一本书,名为书 *B,*的同一作者的书,其价格与书 A 的价格相比是第二便宜的。可以通过以下方式实现这一点:

@Transactional(readOnly = true)
public void fetchBooks() {

    Book book = bookRepository.findById(7L).orElseThrow();
    Book nextBook = bookRepository.fetchNextSmallerPrice(
        book.getPrice(), book.getAuthor().getId());

    System.out.println("Fetched book with id 7: " + book);
    System.out.println("Fetched book with next smallest price: " + nextBook);
}

其中fetchNextSmallerPrice()是以下本机查询:

@Transactional(readOnly = true)
@Query(value="SELECT * FROM book WHERE price < ?1 AND author_id = ?2 "
            + "ORDER BY price DESC LIMIT 1",
       nativeQuery = true)
Book fetchNextSmallerPrice(int price, long authorId);

需要两条SELECT语句来获取booknextBook。或者,通过 Hibernate 特有的@JoinFormula@ManyToOne映射到前面的查询会更简单:

@Entity
public class Book implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String isbn;
    private int price;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private Author author;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinFormula("(SELECT b.id FROM book b "
        + "WHERE b.price < price AND b.author_id = author_id "
        + "ORDER BY b.price DESC LIMIT 1)")
    private Book nextBook;

    public Book getNextBook() {
        return nextBook;
    }

    public void setNextBook(Book nextBook) {
        this.nextBook = nextBook;
    }
    ...
}

基于这个映射,服务方法fetchBooks()变成了:

@Transactional(readOnly = true)
public void fetchBooks() {

    Book book = bookRepository.findById(7L).orElseThrow();
    Book nextBook = book.getNextBook();

    System.out.println("Fetched book with id 7: " + book);
    System.out.println("Fetched book with next smallest price: " + nextBook);
}

为了取出booknextBook,下面的SELECT语句被执行两次:

SELECT
  book0_.id AS id1_1_0_,
  book0_.author_id AS author_i5_1_0_,
  book0_.isbn AS isbn2_1_0_,
  book0_.price AS price3_1_0_,
  book0_.title AS title4_1_0_,
  (SELECT
    b.id
  FROM book b
  WHERE b.price < book0_.price AND b.author_id = book0_.author_id
  ORDER BY b.price DESC LIMIT 1)
  AS formula1_0_
FROM book book0_
WHERE book0_.id = ?
Binding:[7] Extracted:[4, 003-JN, 2, 41, History Today]

第三个提取值是 2 ,对应公式结果。这是nextBook的 ID。因此,再次执行该查询来获取带有以下参数的nextBook:

Binding:[2] Extracted:[4, 002-JN, 1, 34, A People's History]

再次注意,第三个提取的值( 1 )对应于公式结果。这允许您继续调用getNextBook()。没有其他更便宜的书的时候,公式结果会是 null

一般来说,Hibernate 特有的@JoinFormula注释可以用来定义任何SELECT查询,以提供两个实体之间的关系。例如,你可以用它买到某个作者最便宜的书。为此,您也要在Author中添加一个@ManyToOne:

@Entity
public class Author implements Serializable {
    ...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinFormula("(SELECT b.id FROM book b "
        + "WHERE b.author_id = id "
        + "ORDER BY b.price ASC LIMIT 1)")
    private Book cheapestBook;
    ...
}

用法:

Author author = authorRepository.findById(1L).orElseThrow();
Book cheapestBook = author.getCheapestBook();

完整的应用可在 GitHub 7 上获得。

Footnotes 1

hibernate pringb otcalculateprp ertypodload

  2

hibernate pringb otcalculateprp ertyformula

  3

hibernate spring ootCalculateProp erty generated

  4

hibernate pringb oojpqlfunction params

  5

https://en.wikibooks.org/wiki/Java_Persistence/JPQL#JPQL_supported_functions

  6

hibernate pringb oojpqlfunction

  7

hibernate pringb 欧统局公式

 

*

九、监控

第 81 项:为什么以及如何计算和断言 SQL 语句

假设您有映射到表的Author实体,如图 9-1 所示,目标是自动执行下面的简单场景:

img/487471_1_En_9_Fig1_HTML.jpg

图 9-1

作者实体表

  • 从数据库加载一个Author

  • 更新该Authorgenre

一个简单的服务方法可以实现这个场景,如下所示:

@Service
public class BookstoreService {

    private final AuthorRepository authorRepository;
    ...

    public void updateAuthor() {

        Author author = authorRepository.findById(1L).orElseThrow();
        author.setGenre("History");

        authorRepository.save(author);
    }
}

但是,这些操作是原子性的吗?它们不是,因为开发人员不小心忘记在方法级别添加@Transactional,并且没有继承的事务上下文。每个操作将在单独的事务中运行,这将导致性能损失。代码还容易出现意外行为和数据不一致。但是,这个事故对触发的 SQL 的数量和/或类型有负面影响吗?根据预期计算和断言 SQL 语句的数量将回答这个问题。

为计数和断言触发的 SQL 语句的机制提供支持需要两个库。计数是DataSource-Proxy库的责任。在这个库的好处中(检查 Item 83 ,它将代理数据源并提取重要信息,如绑定参数的值和执行的 SQL 语句的数量。

关键是在构建代理之前调用countQuery()方法。这指示DataSource-Proxy创建一个DataSourceQueryCountListener。除了数据源名称之外,该监听器还提供了数据库调用次数、总查询执行时间以及按类型划分的查询次数等指标:

public ProxyDataSourceInterceptor(final DataSource dataSource) {

    super();

    this.dataSource = ProxyDataSourceBuilder.create(dataSource)
        .name("DATA_SOURCE_PROXY")
        .logQueryBySlf4j(SLF4JLogLevel.INFO)
        .multiline()
        .countQuery()
        .build();
}

有了这个监听器,被触发的 SQL 语句可以通过QueryCount API 直接计数。或者,更好的是,您可以使用db-util库。使用这个库的优点是名为SQLStatementCountValidator的开箱即用的自动化验证器。这个验证器公开了下面的static断言:assertSelectCount()assertInsertCount()assertUpdateCount()assertDeleteCount()

使用这个验证器需要三个主要步骤:

  • 通过SQLStatementCountValidator.reset()复位QueryCount

  • 执行 SQL 语句

  • 应用适当的断言

回到updateAuthor()方法,开发人员没有意识到忘记添加@Transactional,因此,判断事务上下文中的代码,SQL 语句的预期数量等于两个,一个SELECT和一个UPDATE。预计不会出现INSERTDELETE。您可以断言预期的查询,如下所示:

private final BookstoreService bookstoreService;
...

SQLStatementCountValidator.reset();

bookstoreService.updateAuthor();

assertSelectCount(1);
assertUpdateCount(1);
assertInsertCount(0);
assertDeleteCount(0);

根据经验,这些断言可以添加到单元测试中。建议断言所有类型的操作,而不仅仅是那些您期望发生的操作。例如,如果触发了一个意外的DELETE,而您跳过了assertDeleteCount(0),那么您将无法捕捉到它。

运行该应用将导致以下异常:

com.vladmihalcea.sql.exception.SQLSelectCountMismatchException: Expected 1 statements but recorded 2 instead!

如果预期的 SQL 语句的数量与已执行的 SQL 语句的数量不同,那么SQLStatementCountValidator将抛出类型为SQL Foo CountMismatchException的异常,其中FooSelectInsertUpdateDelete中的一个,这取决于 SQL 类型。

因此,应用断言了一个SELECT,但是触发了两个。为什么预期的 SQL 语句数量不正确?因为每个语句都在一个单独的事务中运行,所以实际上触发了下面的 SQL 语句(检查右边的注释会发现现实与预期相差很远):

Author author = authorRepository.findById(1L).orElseThrow(); // 1 select
author.setGenre("History");
authorRepository.save(author);   // 1 select, 1 update

列出这些 SQL 语句将揭示以下内容:

-- fetch the author
SELECT
  author0_.id AS id1_0_0_,
  author0_.age AS age2_0_0_,
  author0_.genre AS genre3_0_0_,
  author0_.name AS name4_0_0_
FROM author author0_
WHERE author0_.id = ?

-- the fetched author is not managed,
-- therefore it must be fetched again before updating it
SELECT
  author0_.id AS id1_0_0_,
  author0_.age AS age2_0_0_,
  author0_.genre AS genre3_0_0_,
  author0_.name AS name4_0_0_
FROM author author0_
WHERE author0_.id = ?

-- update the author
UPDATE author
SET age = ?,
    genre = ?,
    name = ?
WHERE id = ?

因此,开发人员期望两个 SQL 语句,但实际上,有三个 SQL 语句。因此,有三次数据库往返,而不是两次。这是不对的,但是,由于计算和断言 SQL 语句,这个错误没有被发现。意识到这个错误后,开发人员修复了updateAuthor()方法,如下所示:

@Service
public class BookstoreService {

    private final AuthorRepository authorRepository;
    ...

    @Transactional
    public void updateAuthor() {

        Author author = authorRepository.findById(1L).orElseThrow();
        author.setGenre("History");

        authorRepository.save(author);
    }
}

再次计数和断言表明,SQL 语句的预期数量和类型符合实际情况。这次只触发一个SELECT和一个UPDATE。没有 ?? 和 ??,这样更好。

但是,等等!现在,既然您提供了事务上下文,那么有必要显式调用save()方法吗?答案是否定的!在项 107 中可以看到,这种情况下调用save()是多余的。通过删除这个显式调用,您不会影响被触发的 SQL 的数量,因为 Hibernate 脏检查机制会代表您触发UPDATE。所以,updateAuthor()方法最好的写法如下(当然现实中你会把作者 ID 作为参数传递给这个方法而不会依赖orElseThrow();这里使用它们只是为了简洁):

@Transactional
public void updateAuthor() {

    Author author = authorRepository.findById(1L).orElseThrow();
    author.setGenre("History");
}

GitHub 1 上有源代码。

项目 82:如何记录预准备语句的绑定和提取参数

考虑从一个INSERT和一个SELECT构建的Author实体和两个准备好的语句。显示相应的 SQL 语句将如下所示:

INSERT INTO author (age, genre, name)
  VALUES (?, ?, ?)

SELECT
  author0_.id AS id1_0_0_,
  author0_.age AS age2_0_0_,
  author0_.genre AS genre3_0_0_,
  author0_.name AS name4_0_0_
FROM author author0_
WHERE author0_.id = ?

注意那些问号(?)。它们是绑定提取参数的占位符。大多数时候,看到这些参数的真实值而不是这些占位符是很有用的。有几种方法可以实现这一点。让我们来看看其中的三个。

微量

解决这个问题的最快方法可能是启用application.properties中的TRACE日志记录级别,如下所示:

logging.level.org.hibernate.type.descriptor.sql=TRACE

这一次,输出如下:

insert into author (age, genre, name) values (?, ?, ?)
binding parameter [1] as [INTEGER] - [34]
binding parameter [2] as [VARCHAR] - [History]
binding parameter [3] as [VARCHAR] - [Joana Nimar]

select author0_.id as id1_0_0_, author0_.age as age2_0_0_, author0_.genre as genre3_0_0_, author0_.name as name4_0_0_ from author author0_ where author0_.id=?
binding parameter [1] as [BIGINT] - [1]
extracted value ([age2_0_0_] : [INTEGER]) - [34]
extracted value ([genre3_0_0_] : [VARCHAR]) - [History]
extracted value ([name4_0_0_] : [VARCHAR]) - [Joana Nimar]

对于每个参数,输出包含其类型(绑定参数提取值)、位置或名称、数据类型和值。

GitHub 2 上有源代码。

当您使用启动器时,默认情况下 Spring Boot 依赖于 Logback。如果您不想在application.properties中设置TRACE日志级别,那么只需添加或创建一个 Logback 配置文件。Spring Boot 会自动识别类路径中的logback-spring.xmllogback.xmllogback-spring.groovylogback.groovy文件并进行相应处理。下面是来自logback-spring.xml的一个样本(完整的文件可以在 GitHub 3 上获得):

...
<logger name="org.hibernate.type.descriptor.sql"
        level="trace" additivity="false">
    <appender-ref ref="Console" />
</logger>
...

Log4j 2

通过Log4j 2 可以获得相同的结果。要启用它,首先要排除 Spring Boot 的默认日志记录,并添加Log4j 2 依赖项,如下所示:

<!-- Exclude Spring Boot's Default Logging -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- Add Log4j2 Dependency -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

此外,配置log4j.xml中的TRACE级别如下(该文件应放在application.properties旁边的/resources文件夹中):

<Loggers>
    <Logger name="org.hibernate.type.descriptor.sql" level="trace"/>
    ...
</Loggers>

当然,日志可以进一步调整,以符合Log4j 2 文档。

GitHub 4 上有源代码。

除了绑定提取的参数之外,其他方法可以提供关于查询的更多细节。执行时间、批处理信息、查询类型等细节可以通过第 83 项中介绍的方法获得。

MySQL 和 profileSQL=true

仅对于 MySQL,绑定参数(不是提取的参数)通过两个步骤可见:

  • 关闭spring.jpa.show-sql(省略或设置为false)

  • 通过将logger=Slf4JLogger&profileSQL=true追加到 JDBC URL 来塑造它

GitHub 5 上有源代码。

项目 83:如何记录查询详细信息

要仅记录预准备语句的绑定参数提取值,请参见第 82 项。

您可以通过几种方式获得 SQL 查询的详细信息。让我们来看看其中的三个。

通过数据源代理

DataSource-Proxy是一个开源项目,它“通过代理为 JDBC 交互和查询执行提供了一个监听器框架”。它不依赖于其他库;一切都是可选的。它是高度可配置的、灵活的、可扩展的,并且是正确的选择。

在 Spring Boot 应用中启用该库需要几个步骤。首先,将datasource-proxy的依赖关系添加到pom.xml:

<dependency>
    <groupId>net.ttddyy</groupId>
    <artifactId>datasource-proxy</artifactId>
    <version>${datasource-proxy.version}</version>
</dependency>

接下来,创建一个 bean post 处理器来拦截DataSource bean,并通过ProxyFactoryMethodInterceptor的实现包装DataSource bean。最终结果如以下代码片段所示:

private static class ProxyDataSourceInterceptor
                        implements MethodInterceptor {

    private final DataSource dataSource;

    public ProxyDataSourceInterceptor(final DataSource dataSource) {
        super();
        this.dataSource = ProxyDataSourceBuilder.create(dataSource)
            .name("DATA_SOURCE_PROXY")
            .logQueryBylf4j(SLF4JLogLevel.INFO)
            .multiline()
            .build();
    }
    ...
}

这是可以定制细节级别的地方。可以使用丰富而流畅的 API 来调整细节(查看文档)。所有设置就绪后,只需调用build()。典型的输出如下所示:

Name:DATA_SOURCE_PROXY, Connection:5, Time:131, Success:True
Type:Prepared, Batch:False, QuerySize:1, BatchSize:0
Query:["insert into author (age, genre, name) values (?, ?, ?)"]
Params:[(34,History,Joana Nimar)]

GitHub 6 上有源代码。

通过 log4jdbc

log4jdbc背后的官员声称“log4jdbc 是一个 Java jdbc 驱动程序,它可以使用 Java (SLF4J)日志记录系统的简单日志门面来记录其他 JDBC 驱动程序的 SQL 和/或 JDBC 调用(以及可选的 SQL 计时信息)”。

Spring Boot 应用可以在将它的依赖项添加到pom.xml后立即利用log4jdbc:

<dependency>
    <groupId>com.integralblue</groupId>
    <artifactId>log4jdbc-spring-boot-starter</artifactId>
    <version>1.0.2</version>
</dependency>

官方文档提供了关于定制输出的详细信息。典型的输出包含 SQL(包括执行时间)、对所涉及的方法的审计以及作为表格的结果集,如图 9-2 所示。

img/487471_1_En_9_Fig2_HTML.jpg

图 9-2

log4jdbc 输出示例

GitHub 7 上有源代码。

Via P6spy

文档中说 P6Spy“…是一个框架,它能够无缝地截取数据库数据并记录日志,而无需对应用进行代码更改”。启用 P6spy 需要将pom.xml添加到相应的依赖关系中:

<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>${p6spy.version}</version>
</dependency>

此外,在application.properties中,您设置了 JDBC URL 和驱动程序类名,如下所示:

spring.datasource.url=jdbc:p6spy:mysql://localhost:3306/bookstoredb
spring.datasource.driverClassName=com.p6spy.engine.spy.P6SpyDriver

最后,向应用根文件夹添加spy.properties文件。该文件包含 P6Spy 配置。在这个应用中,日志将被输出到控制台,但是有一个非常简单的方法来切换到文件。关于 P6Spy 配置的更多细节可以在文档中找到。

输出可能如下所示:

insert into author (age, genre, name) values (?, ?, ?)
insert into author (age, genre, name) values (34, 'History', 'Joana Nimar');
#1562161760396 | took 0ms | commit | connection 0| url jdbc:p6spy:mysql://localhost:3306/bookstoredb?createDatabaseIfNotExist=true

GitHub 8 上有源代码。

项目 84:如何记录带有阈值的慢速查询

您可以通过DataSource-Proxy使用 threshold 记录慢速查询。要熟悉DataSource-Proxy,可以考虑第 83 项

准备好DataSource-Proxy之后,考虑以下步骤来记录缓慢的查询:

  • 在 bean post 处理器中,定义一个常量,以毫秒为单位表示慢速查询的阈值:

  • 此外,定义一个SLF4JQueryLoggingListener监听器并覆盖afterQuery()方法,如下所示:

private static final long THRESHOLD_MILLIS = 30;

  • 最后,使用这个listener来配置数据源代理:
SLF4JQueryLoggingListener listener
    = new SLF4JQueryLoggingListener() {

    @Override
    public void afterQuery(ExecutionInfo execInfo,
                            List<QueryInfo> queryInfoList) {
        // call query logging logic only // when it took more than threshold
        if (THRESHOLD_MILLIS <= execInfo.getElapsedTime()) {
            logger.info("Slow SQL detected ...");
            super.afterQuery(execInfo, queryInfoList);
        }
    }
};

listener.setLogLevel(SLF4JLogLevel.WARN);

this.dataSource = ProxyDataSourceBuilder.create(dataSource)
    .name("DATA_SOURCE_PROXY")
    .multiline()
    .listener(listener)
    .build();

搞定了。现在,记录的 SQL 将只是那些超过阈值的 SQL。GitHub 9 上有源代码。

从 Hibernate 5.4.5 开始,您可以通过一个名为hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS的新属性记录阈值为毫秒的慢速查询。您只需在application.properties中添加该属性,并以毫秒为单位指定阈值,如下例所示:

spring.jpa.properties.hibernate.session
    .events.log.LOG_QUERIES_SLOWER_THAN_MS=25

GitHub 10 上有完整的例子。如果您没有使用 Hibernate 5.4.5+,那么可以使用第三方库来记录慢速查询。

第 85 项:日志事务和查询方法详细信息

有时,为了理解数据访问层中发生的事情,您需要记录关于正在运行的事务(例如,您可能需要理解某个事务传播场景)和查询方法(例如,您可能需要记录某个query-method的执行时间)的更多细节。

记录事务详细信息

默认情况下,logger INFO级别不会透露正在运行的事务的细节。但是您可以通过在下面的行中添加application.properties来轻松地公开事务细节:

logging.level.ROOT=INFO
logging.level.org.springframework.orm.jpa=DEBUG
logging.level.org.springframework.transaction=DEBUG
logging.level.org.hibernate.engine.transaction.internal.TransactionImpl=DEBUG

有时记录连接池的状态也很有用。对于 hikar ICP(Spring Boot 应用中推荐和默认的连接池),您可以通过将application.properties添加到以下设置中来实现:

logging.level.com.zaxxer.hikari.HikariConfig=DEBUG
logging.level.com.zaxxer.hikari=DEBUG

如果你需要更多的细节,用TRACE代替DEBUG

通过事务回调获得控制权

Spring Boot 允许您启用一组回调,这些回调对于在事务提交/完成之前和之后获取控制权非常有用。从全局来看(在应用级别),您可以通过 AOP 组件来实现,如下所示:

@Aspect
@Component
public class TransactionProfiler extends TransactionSynchronizationAdapter {

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Before("@annotation(
        org.springframework.transaction.annotation.Transactional)")
    public void registerTransactionSyncrhonization() {
        TransactionSynchronizationManager.registerSynchronization(this);
    }

    @Override
    public void afterCompletion(int status) {
        logger.info("After completion (global) ...");
    }

    @Override
    public void afterCommit() {
        logger.info("After commit (global) ...");
    }

    @Override
    public void beforeCompletion() {
        logger.info("Before completion (global) ...");
    }

    @Override
    public void beforeCommit(boolean readOnly) {
        logger.info("Before commit (global) ...");
    }
}

例如,您可以调用这个服务方法:

@Transactional
public void updateAuthor() {

    Author author = authorRepository.findById(1L).orElseThrow();

    author.setAge(49);
}

该日志将包含类似如下的内容:

Hibernate: select author0_.id as id1_0_0_, author0_.age as age2_0_0_, author0_.genre as genre3_0_0_, author0_.name as name4_0_0_ from author author0_ where author0_.id=?

c.b.profiler.TransactionProfiler: Before commit (global) ...
c.b.profiler.TransactionProfiler: Before completion (global) ...

Hibernate: update author set age=?, genre=?, name=? where id=?

c.b.profiler.TransactionProfiler: After commit (global) ...
c.b.profiler.TransactionProfiler: After completion (global) ...

您还可以通过TransactionSynchronizationManager#registerSynchronization()在方法级利用这些回调,如下所示:

@Transactional
public void updateAuthor() {

    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronizationAdapter() {

        @Override
        public void afterCompletion(int status) {
            logger.info("After completion (method) ...");
        }

        @Override
        public void afterCommit() {
            logger.info("After commit (method) ...");
        }

        @Override
        public void beforeCompletion() {
            logger.info("Before completion (method) ...");
        }

        @Override
        public void beforeCommit(boolean readOnly) {
            logger.info("Before commit (method) ...");
        }
    });

    Author author = authorRepository.findById(1L).orElseThrow();

    author.setAge(51);
}

这一次,输出如下:

Hibernate: select author0_.id as id1_0_0_, author0_.age as age2_0_0_, author0_.genre as genre3_0_0_, author0_.name as name4_0_0_ from author author0_ where author0_.id=?

c.b.profiler.TransactionProfiler: Before commit (method) ...
c.b.profiler.TransactionProfiler: Before completion (method) ...

Hibernate: update author set age=?, genre=?, name=? where id=?

c.b.profiler.TransactionProfiler: After commit (method) ...
c.b.profiler.TransactionProfiler: After completion (method) ...

TransactionSynchronizationManager类提供了其他有用的方法,比如isActualTransactionActive()getCurrentTransactionName()isCurrentTransactionReadOnly()getCurrentTransactionIsolationLevel()。这些方法中的每一个都有相应的 setter。

完整的应用可在 GitHub 11 上获得。

日志查询-方法执行时间

您可以通过 AOP 轻松记录查询方法的执行时间。下面的组件非常简单:

@Aspect
@Component
public class RepositoryProfiler {

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Pointcut("execution(public *
        org.springframework.data.repository.Repository+.*(..))")
    public void intercept() {
    }
    @Around("intercept()")
    public Object profile(ProceedingJoinPoint joinPoint) {

    long startMs = System.currentTimeMillis();

    Object result = null;
    try {
        result = joinPoint.proceed();
    } catch (Throwable e) {
        logger.error(e.getMessage(), e);
        // do whatever you want with the exception
    }

    long elapsedMs = System.currentTimeMillis() - startMs;

    // you may like to use logger.debug
    logger.info(joinPoint.getTarget()+"."+joinPoint.getSignature()
        + ": Execution time: " + elapsedMs + " ms");

    // pay attention that this line may return null
    return result;
    }
}

例如,您可以调用这个服务方法:

@Transactional
public void updateAuthor() {

    Author author = authorRepository.findById(1L).orElseThrow();

    author.setAge(49);
}

那么日志将包含类似如下的内容:

c.bookstore.profiler.RepositoryProfiler  : org.springframework.data.jpa.repository.support.SimpleJpaRepository@780dbed7.Optional org.springframework.data.repository.CrudRepository.findById(Object):
Execution time: 47 ms

完整的应用可在 GitHub 12 上获得。

Footnotes 1

hibernate pringb oottacount SQLSTATE elements

  2

hibernate pringgb 未记录的 indi 参数

  3

hibernate pringgb 未记录的 indi 参数

  4

hibernate pringb oolog 4j2 检视 Bin 定参数

  5

hibernate pringgb 未连接对 Amer MySQL

  6

hibernate pringb oodatasource pro xy

  7

hibernate spring ootlog 4 jdbcviewb inding parameters

  8

hibernate pringb booth 6 spy

  9

hibernate pringb oologlow querie s

  10

hibernate pringb oologlow querie s 545

  11

hibernate pringb 欧统局回执

  12

hibernate pringb ootorepointercept

 

十、配置数据源和连接池

项目 86:如何自定义 HikariCP 设置

Spring Boot 依赖 HikariCP 作为默认连接池。

在您的项目中添加spring-boot-starter-jdbcspring-boot-starter-data-jpa“starters”将使用默认设置自动添加对 HikariCP 的依赖。

知道如何改变连接池的配置很重要。大多数时候,默认设置不能满足生产要求。为生产调整连接池参数的最佳方式是使用 Vlad Mihalcea 的 FlexyPool 1 。FlexyPool 可以确定维持连接池高性能所需的最佳设置。FlexyPool 只是几个令人惊叹的工具之一。更多详情,请查看附录 J

假设您已经为连接池设置了最佳值,本章将向您展示在 HikariCP 的生产环境中设置这些值的几种方法。

通过 application.properties 调整 HikariCP 参数

您可以在application.properties文件中调整 HikariCP 的参数。每个参数值都可以通过将其名称作为后缀添加到以spring.datasource.hikari.*开头的 Spring 属性来更改。*是参数名的占位符。参数列表及其含义可在 HikariCP 文档中找到。以下代码片段显示了最常见参数的示例设置:

spring.datasource.hikari.connectionTimeout=50000
spring.datasource.hikari.idleTimeout=300000
spring.datasource.hikari.maxLifetime=900000
spring.datasource.hikari.maximumPoolSize=8
spring.datasource.hikari.minimumIdle=8
spring.datasource.hikari.poolName=MyPool
spring.datasource.hikari.connectionTestQuery=select 1 from dual
# disable auto-commit
spring.datasource.hikari.autoCommit=false
# more settings can be added as spring.datasource.hikari.*

或者,像这样:

spring.datasource.hikari.connection-timeout=50000
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.max-lifetime=900000
spring.datasource.hikari.maximum-pool-size=8
spring.datasource.hikari.minimum-idle=8
spring.datasource.hikari.pool-name=MyPool
spring.datasource.hikari.connection-test-query=select 1 from dual

Spring Boot 处理application.properties并根据这些值配置 HikariCP 连接池。完整的代码可以在 GitHub 2 上找到。

通过 application.properties 和 DataSourceBuilder 调整 HikariCP 参数

您可以使用application.properties文件和DataSourceBuilder来调整 HikariCP 的参数。该类为构建具有通用实现和属性的DataSource提供支持。这次在application.properties中,参数名被指定为自定义属性的后缀(如app.datasource.*):

app.datasource.connection-timeout=50000
app.datasource.idle-timeout=300000
app.datasource.max-lifetime=900000
app.datasource.maximum-pool-size=8
app.datasource.minimum-idle=8
app.datasource.pool-name=MyPool
app.datasource.connection-test-query=select 1 from dual
# disable auto-commit
app.datasource.auto-commit=false
# more settings can be added as app.datasource.*

此外,配置DataSource需要两步:

  • 使用@ConfigurationProperties加载app.datasource类型的属性

  • 使用DataSourceBuilder构建HikariDataSource的实例

以下代码不言自明:

@Configuration
public class ConfigureDataSource {

    @Bean
    @Primary
    @ConfigurationProperties("app.datasource")
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @ConfigurationProperties("app.datasource")
    public HikariDataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder()
            .type(HikariDataSource.class)
            .build();
    }
}

完整的代码可以在 GitHub 3 上找到。

通过 DataSourceBuilder 调整 HikariCP 参数

您可以通过DataSourceBuilder以编程方式调整 HikariCP 参数。换句话说,连接池的参数通过DataSourceBuilder API 直接设置。这可以分两步完成:

  • 创建一个HikariDataSource的实例

  • 调用专用方法来形成该数据源

除了setJdbcUrl()setUsername()setPassword()方法之外,DataSourceBuilder API 还公开了 HikariCP 参数的专用方法,如以下代码片段所示:

@Configuration
public class ConfigureDataSource {

    @Bean
    public HikariDataSource dataSource() {

        HikariDataSource hds = new HikariDataSource();
             hds.setJdbcUrl("jdbc:mysql://localhost:3306/numberdb"
                                    + "?createDatabaseIfNotExist=true");
        hds.setUsername("root");
        hds.setPassword("root");

    hds.setConnectionTimeout(50000);
    hds.setIdleTimeout(300000);
    hds.setMaxLifetime(900000);
    hds.setMaximumPoolSize(8);
    hds.setMinimumIdle(8);
    hds.setPoolName("MyPool");
    hds.setConnectionTestQuery("select 1 from dual");
    hds.setAutoCommit(false);

    return hds;
    }
}

完整的代码可以在 GitHub 4 上找到。

调整其他连接池

本论文也可以应用于其他连接池。在大局不变的情况下,开发者需要做一些小的调整,如下面的例子列表(下面的例子使用application.propertiesDataSourceBuilder ): BoneCP 5 ,C3P0 6 ,DBCP2 7 ,Tomcat, 8 和 ViburDBCP 9

这些示例主要遵循三个步骤:

  • pom.xml(对于 Maven)中,添加连接池对应的依赖关系

  • application.properties中,通过自定义前缀配置连接池,例如app.datasource.*

  • 编写一个通过DataSourceBuilder返回DataSource@Bean

第 87 项:如何用两个连接池配置两个数据源

这一项处理带有两个连接池的两个数据库的配置。更准确地说,名为Author的实体被映射到名为authorsdb的数据库中名为author的表,而另一个名为Book的实体被映射到名为booksdb的数据库中名为book的表。这些实体并不相关,而且非常简单:

@Entity
public class Author implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String genre;
    private int age;
    private String books;

    // getters and setters omitted for brevity
}

@Entity
public class Book implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String isbn;
    private String authors;

    // getters and setters omitted for brevity
}

AuthorRepository调用查询方法将导致针对authorsdb数据库触发 SQL 语句,而从BookRepository调用查询方法将导致针对booksdb数据库触发 SQL 语句。

一、重点关注application.properties。这里,让我们添加数据源的配置。更准确地说,让我们添加两个 JDBC URL 和连接池的配置。注意,第一个数据源使用了app.datasource.ds1前缀,而第二个数据源使用了app.datasource.ds2前缀:

app.datasource.ds1.url=jdbc:mysql://localhost:3306/authorsdb
                                    ?createDatabaseIfNotExist=true
app.datasource.ds1.username=root
app.datasource.ds1.password=root
app.datasource.ds1.connection-timeout=50000
app.datasource.ds1.idle-timeout=300000
app.datasource.ds1.max-lifetime=900000
app.datasource.ds1.maximum-pool-size=8
app.datasource.ds1.minimum-idle=8
app.datasource.ds1.pool-name=MyPoolDS1
app.datasource.ds1.connection-test-query=select 1 from dual

app.datasource.ds2.url=jdbc:mysql://localhost:3306/booksdb
                                    ?createDatabaseIfNotExist=true
app.datasource.ds2.username=root
app.datasource.ds2.password=root
app.datasource.ds2.connection-timeout=50000
app.datasource.ds2.idle-timeout=300000
app.datasource.ds2.max-lifetime=900000
app.datasource.ds2.maximum-pool-size=4
app.datasource.ds2.minimum-idle=4
app.datasource.ds2.pool-name=MyPoolDS2
app.datasource.ds2.connection-test-query=select 1 from dual

这些配置也可以在@Configuration类中以编程方式设置。这里有一个例子:

@Bean
public HikariDataSource dataSource() {

    HikariDataSource hds = new HikariDataSource();
    hds.setJdbcUrl("jdbc:mysql://localhost:3306/numberdb
                        ?createDatabaseIfNotExist=true");
    ...
    return hds;
}

此外,这些设置被加载并用于在用@Configuration注释的类中创建HikariDataSource的实例。每个数据库都有一个关联的HikariDataSource:

@Configuration
public class ConfigureDataSources {

    // first database, authorsdb
    @Primary
    @Bean(name = "configAuthorsDb")
    @ConfigurationProperties("app.datasource.ds1")
    public DataSourceProperties firstDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Primary
    @Bean(name = "dataSourceAuthorsDb")
    @ConfigurationProperties("app.datasource.ds1")
    public HikariDataSource firstDataSource(
            @Qualifier("configAuthorsDb") DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder()
            .type(HikariDataSource.class)
            .build();
    }

    // second database, booksdb
    @Bean(name = "configBooksDb")
    @ConfigurationProperties("app.datasource.ds2")
    public DataSourceProperties secondDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean(name = "dataSourceBooksDb")
    @ConfigurationProperties("app.datasource.ds2")
    public HikariDataSource secondDataSource(
            @Qualifier("configBooksDb") DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder()
            .type(HikariDataSource.class)
            .build();
    }
}

接下来,为每个HikariDataSource,配置一个LocalContainerEntityManagerFactoryBean和一个PlatformTransactionManager。告诉 Spring Boot 映射到authorsdb的实体在com.bookstore.ds1包中;

@Configuration
@EnableJpaRepositories(
    entityManagerFactoryRef = "ds1EntityManagerFactory",
    transactionManagerRef = "ds1TransactionManager",
    basePackages = "com.bookstore.ds1"
)
@EnableTransactionManagement
public class FirstEntityManagerFactory {

    @Bean
    @Primary
    public LocalContainerEntityManagerFactoryBean ds1EntityManagerFactory(
                    EntityManagerFactoryBuilder builder,
                    @Qualifier("dataSourceAuthorsDb") DataSource dataSource) {

        return builder
            .dataSource(dataSource)
            .packages(packagesToScan())
            .persistenceUnit("ds1-pu")
            .properties(hibernateProperties())
            .build();
    }

    @Bean
    @Primary
    public PlatformTransactionManager ds1TransactionManager(
                    @Qualifier("ds1EntityManagerFactory") EntityManagerFactory ds1EntityManagerFactory) {
        return new JpaTransactionManager(ds1EntityManagerFactory);
    }

    protected String[] packagesToScan() {
        return new String[]{
            "com.bookstore.ds1"
        };
    }

    protected Map<String, String> hibernateProperties() {
        return new HashMap<String, String>() {
            {
                put("hibernate.dialect",
                    "org.hibernate.dialect.MySQL5Dialect");
                put("hibernate.hbm2ddl.auto", "create");
            }
        };

    }
}

接下来,为第二个数据源配置一个LocalContainerEntityManagerFactoryBean和一个PlatformTransactionManager。这次,告诉 Spring Boot 映射到booksdb的实体在com.bookstore.ds2包中:

@Configuration
@EnableJpaRepositories(
    entityManagerFactoryRef = "ds2EntityManagerFactory",
    transactionManagerRef = "ds2TransactionManager",
    basePackages = "com.bookstore.ds2"
)
@EnableTransactionManagement
public class SecondEntityManagerFactory {

    @Bean
    public LocalContainerEntityManagerFactoryBean ds2EntityManagerFactory(
                    EntityManagerFactoryBuilder builder,
                    @Qualifier("dataSourceBooksDb") DataSource dataSource) {

        return builder
            .dataSource(dataSource)
            .packages(packagesToScan())
            .persistenceUnit("ds2-pu")
            .properties(hibernateProperties())
            .build();
    }

    @Bean
    public PlatformTransactionManager ds2TransactionManager(
                    @Qualifier("ds2EntityManagerFactory") EntityManager
                    Factory secondEntityManagerFactory) {
        return new JpaTransactionManager(secondEntityManagerFactory);
    }

    protected String[] packagesToScan() {
        return new String[]{
            "com.bookstore.ds2"
        };
    }

    protected Map<String, String> hibernateProperties() {
        return new HashMap<String, String>() {
            {
                put("hibernate.dialect",
                    "org.hibernate.dialect.MySQL5Dialect");
                put("hibernate.hbm2ddl.auto", "create");
            }
        };
    }
}

测试时间

AuthorRepository添加到com.bookstore.ds1包,BookRepository添加到com.bookstore.ds2包:

package com.bookstore.ds1;

@Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {
}

package com.bookstore.ds2;

@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
}

可以在服务方法中持久化作者,如下所示:

public Author persistAuthor() {

    Author author = new Author();

    author.setName("Joana Nimar");
    author.setGenre("History");
    author.setAge(34);
    author.setBooks("A History of Ancient Prague, A People's History");

    return authorRepository.save(author);
}

调用persistAuthor()会将作者保存在authorsdb数据库中。

持久化一本书可以在如下的服务方法中完成:

public Book persistBook() {

    Book book = new Book();

    book.setIsbn("001-JN");
    book.setTitle("A History of Ancient Prague");
    book.setAuthors("Joana Nimar");

    return bookRepository.save(book);
}

调用persistBook()会将图书保存在booksdb数据库中。

GitHub 10 上有完整的应用。

Footnotes 1

https://github.com/vladmihalcea/flexy-pool

  2

hibernate pringb oodharicpprope rtieskiff

  3

hibernate pringb oodatasource bui ldrhikiricpkick 关

  4

hibernate pringb oodatasource bui ldroghkarikcp 开球

  5

hibernate pringb oodatasource bui ldrbonecpkikof

  6

hibernate pringb oodatasource bui ldrc 3pp 0 启动

  7

hibernate pringb oodatasource bui ldrdbcp 2 开球

  8

hibernate pringb oodatasource bui 经缓解的 omcatkikof

  9

hibernate pringb oodatasource bui lderburdbcpkic koff

  10

hibernate spring oottowatasource BuilderKickoff

 

十一、审计

第 88 项:如何跟踪创建和修改时间以及实体用户

此项说明如何添加自动生成的持久字段来跟踪创建和修改时间以及用户。审核对于维护记录的历史很有用。这可以帮助您跟踪用户活动。

让我们考虑这些持久字段的以下名称(可以随意修改这些名称):

  • created:将行插入数据库时的时间戳

  • createdBy:触发此行插入的当前登录用户

  • lastModified:该行上次更新的时间戳

  • lastModifiedBy:触发上次更新的当前登录用户

    默认情况下,时间戳将保存在本地时区,但让我们改为保存在 UTC(或 GMT)中。在 MySQL 中,用 UTC(或 GMT)存储时间戳可以分两步完成(参见 Item 111 ):

  • useLegacyDatetimeCode=false添加到 JDBC 网址

  • spring.jpa.properties.hibernate.jdbc.time_zone=UTC添加到application.properties

通过 Spring Data JPA 审计或 Hibernate 支持,可以将这些自动生成的持久字段添加到实体中。在这两种情况下,这些字段都被添加到一个用@MappedSuperclass标注的abstract非实体类中(?? 指定一个类,它的映射信息被应用到从它继承的实体)。

我们把这个类命名为BaseEntity,如图 11-1 所示。

img/487471_1_En_11_Fig1_HTML.jpg

图 11-1

基本实体类图

实体可以通过扩展BaseEntity来继承这些字段。例如,让我们跟踪AuthorBook实体的用户活动。

图 11-2 不言自明。

img/487471_1_En_11_Fig2_HTML.jpg

图 11-2

领域模型

现在,让我们通过 Spring Data JPA auditing 把它放到代码中。

依靠 Spring 数据 JPA 审计

Spring Data 提供了四个注释来实现这个目标。这些注释是@CreatedBy(对于createdBy字段)、@CreatedDate(对于created字段)、@LastModifiedBy(对于lastModifiedBy字段)和@LastModifiedDate(对于lastModified字段)。对字段进行相应的注释只是解决方案的一半。

另外,在@MappedSuperclass注释的旁边,BaseEntity要用@EntityListeners({AuditingEntityListener.class})注释。指定为监听器的类(AuditingEntityListener)是一个 Spring Data JPA 实体监听器类。它使用回调方法(用@PrePersist@PreUpdate注释注释)来保存和更新createdcreatedBylastModifiedlastModifiedBy字段。每当实体被持久化或更新时,都会发生这种情况。话虽如此,代码如下:

@MappedSuperclass
@EntityListeners({AuditingEntityListener.class})
public abstract class BaseEntity<U> {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Long id;

    @CreatedDate
    protected LocalDateTime created;

    @CreatedBy
    protected U createdBy;

    @LastModifiedDate
    protected LocalDateTime lastModified;

    @LastModifiedBy
    protected U lastModifiedBy;
}

AuthorBook扩展BaseEntity如下:

@Entity
public class Author extends BaseEntity<String> implements Serializable {
    ...
}

@Entity
public class Book extends BaseEntity<String> implements Serializable {
    ...
}

但这还不是全部!此时,JPA 可以使用当前系统时间填充createdlastModified字段,但是它不能填充createdBylastModifiedBy。对于这个任务,JPA 需要知道当前登录的用户。换句话说,开发人员需要提供一个AuditorAware的实现,并覆盖getCurrentAuditor()方法。

当前登录的用户通过 Spring Security 在getCurrentAuditor()中获取。在这个例子中,有一个带有硬编码用户的虚拟实现,但是只要您已经有了 Spring 安全性,就应该很容易钩住真正的用户:

public class AuditorAwareImpl implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {

        // use Spring Security to retrieve the currently logged-in user(s)
        return Optional.of(Arrays.asList("mark1990", "adrianm", "dan555")
            .get(new Random().nextInt(3)));
    }
}

最后一步是通过在配置类上指定@EnableJpaAuditing来启用 JPA 审计。@EnableJpaAuditing接受一个元素,auditorAwareRef。这个元素的值是AuditorAware bean 的名称:

@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
public class MainApplication {
    ...
}

搞定了。查看“测试时间”部分,了解应用的快速运行和输出。完整的代码可以在 GitHub 1 上找到。

依靠 Hibernate 支持

如果由于某种原因,这种方法不合适,您可以依赖 Hibernate 支持。

创建的和最后修改的字段

对于createdlastModified字段,Hibernate 提供了两个内置注释(@CreationTimestamp@UpdateTimestamp),可以开箱即用。

@CreationTimestamp@UpdateTimestamp都执行时间戳的内存生成(使用 VM 时间)。

createdBylastModifiedBy字段需要必须实现的注释,您很快就会看到。现在,让我们考虑一下createdBy的注释是@CreatedBy,而lastModifiedBy的注释是@ModifiedBy。将所有这些放在BaseEntity中会产生以下代码:

import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
...
@MappedSuperclass
public abstract class BaseEntity<U> {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Long id;

    @CreationTimestamp
    protected LocalDateTime created;

    @UpdateTimestamp
    protected LocalDateTime lastModified;

    @CreatedBy
    protected U createdBy;

    @ModifiedBy
    protected U lastModifiedBy;
}

“创建者”和“最后修改者”字段

对于createdBylastModifiedBy字段,没有 Hibernate 特有的内置注释。但是您可以通过 Hibernate 特有的AnnotationValueGeneration接口构建@CreatedBy@ModifiedBy注释。该接口表示基于定制 Java 生成器注释类型的ValueGeneration,其中ValueGeneration描述属性值的生成。首先,让我们使用@ValueGenerationType定义@CreatedBy注释,如下所示:

@ValueGenerationType(generatedBy = CreatedByValueGeneration.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface CreatedBy {
}

然后是@ModifiedBy标注:

@ValueGenerationType(generatedBy = ModifiedByValueGeneration.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface ModifiedBy {
}

从 Hibernate 4.3 开始,通过@ValueGenerationType元注释,可以使用一种新的方法来声明生成的属性和定制生成器。@Generated注释已经被改进以使用@ValueGenerationType

CreatedByValueGeneration类实现AnnotationValueGeneration并提供用户名的生成器(创建实体的用户)。这里列出了相关的代码(只有在实体第一次被持久化时,才应该生成这个时间戳;因此,将生成时间设置为GenerationTiming.INSERT):

public class CreatedByValueGeneration
                implements AnnotationValueGeneration<CreatedBy> {

    private final ByValueGenerator generator
        = new ByValueGenerator(new UserService());
    ...
    @Override
    public GenerationTiming getGenerationTiming() {
        return GenerationTiming.INSERT;
    }

    @Override
    public ValueGenerator<?> getValueGenerator() {
        return generator;
    }
    ...
}

ModifiedByValueGeneration类实现AnnotationValueGeneration并提供用户名(修改实体的用户)的生成器。这里列出了相关的代码(这个时间戳应该在实体的每次更新时生成;因此,将生成时间设置为GenerationTiming.ALWAYS):

public class ModifiedByValueGeneration
                    implements AnnotationValueGeneration<ModifiedBy> {

    private final ModifiedByValueGenerator generator
        = new ModifiedByValueGenerator(new UserService());
    ...
    @Override
    public GenerationTiming getGenerationTiming() {
        return GenerationTiming.ALWAYS;
    }

    @Override
    public ValueGenerator<?> getValueGenerator() {
        return generator;
    }
    ...
}

CreatedByValueGenerationModifiedByValueGeneration返回的generatorByValueGenerator。这代表了ValueGenerator接口的简单实现。这个类的结果是generateValue()方法:

public class ByValueGenerator implements ValueGenerator<String> {

    public final UserService userService;

    public ByValueGenerator(UserService userService) {
        this.userService = userService;
    }

    @Override
    public String generateValue(Session session, Object entity) {
        // Hook into a service to get the current user, etc.
        return userService.getCurrentUserName();
    }
}

UserService应该使用 Spring Security 通过getCurrentUserName()返回当前登录的用户。现在,让我们简单地使用一个虚拟实现:

@Service
public class UserService {

    public String getCurrentUserName() {
        // use Spring Security to retrieve the currently logged-in user(s)
        return Arrays.asList("mark1990", "adrianm", "dan555")
            .get(new Random().nextInt(3));
    }
}

显然,您可以快速挂钩自己的处理登录用户的服务。

完整的代码可以在 GitHub 2 上找到。

测试时间

这两种方法产生相同的 SQL 语句和结果;因此,下面的讨论涵盖了这两者。

保留作者会触发以下 SQL 语句:

INSERT INTO author (created, created_by, last_modified,
                    last_modified_by, age, genre, name)
  VALUES (?, ?, ?, ?, ?, ?, ?)

保存一本书会触发以下 SQL 语句:

INSERT INTO book (created, created_by, last_modified,
                  last_modified_by, author_id, isbn, title)
  VALUES (?, ?, ?, ?, ?, ?, ?)

更新作者会触发以下 SQL 语句:

UPDATE author
SET created = ?,
    created_by = ?,
    last_modified = ?,
    last_modified_by = ?,
    age = ?,
    genre = ?,
    name = ?
WHERE id = ?

更新图书会触发以下 SQL 语句:

UPDATE book
SET created = ?,
    created_by = ?,
    last_modified = ?,
    last_modified_by = ?,
    author_id = ?,
    isbn = ?,
    title = ?
WHERE id = ?

图 11-3 显示了authorbook表的快照。注意createdcreated_bylast_modifiedlast_modified_by列。

img/487471_1_En_11_Fig3_HTML.jpg

图 11-3

来自作者和图书表的数据快照

项目 89:如何启用 Hibernate 特有的环境审计

Item 88 讲述了如何通过 Spring Data JPA 审计和 Hibernate 值生成器来跟踪实体的创建和修改时间以及用户。Hibernate ORM 有一个名为 Hibernate Envers 的模块,专门用于审计/版本控制实体类。在它的特性中,Hibernate Envers 为每个修订版提供审计、记录数据,以及查询实体及其关联的历史快照。

这一项重复了启用 Hibernate Envers 的最佳实践。但不是在将 Hibernate Envers 依赖项添加到pom.xml之前(对于 Maven):

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-envers</artifactId>
</dependency>

Hibernate Envers 将 Java 架构用于 XML 绑定(JAXB)API;因此,如果您遇到这种类型的异常:

 Caused by: javax.xml.bind.JAXBException: Implementation of JAXB-API has not been found on module path or classpath

这意味着还需要以下依赖关系:

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
</dependency>

审计实体

准备应该通过 Hibernate Envers 审计的实体是一项简单的任务,需要您在实体类级别添加@Audited注释。每个实体都在单独的数据库表中进行审计。要显式指定每个实体的审计表的名称,依赖于@AuditTable注释(默认情况下,该名称的类型是entity _AUD)。对于AuthorBook实体,可以这样做:

@Entity
@Audited
@AuditTable("author_audit")
public class Author implements Serializable {
    ...
}

@Entity
@Audited
@AuditTable("book_audit")
public class Book implements Serializable {
    ...
}

数据库模式显示了图 11-4 中的表格,包括author_audit表格。

img/487471_1_En_11_Fig4_HTML.jpg

图 11-4

作者 _ 审计表

Revision

是 Hibernate Envers 特有的术语。修改了被审计实体(INSERTUPDATEDELETE)的数据库事务。revinfo表(图 11-4 中的最后一个表)存储了版本号及其纪元时间戳。

author_audit(和book_audit)表存储某个版本的实体快照。rev列包含修订号。

revtype列值取自RevisionType枚举,其定义如下:

  • 0(或ADD):插入了一个数据库表格行。

  • 1(或MOD):更新了一个数据库表行。

  • 2(或DEL):删除了一个数据库表行。

revend列保存审计实体中的最终修订号。仅当使用有效性审核策略时,此列才会显示。ValidityAuditStrategy稍后再讨论。

模式生成

除了实际的实体表之外,使用 Hibernate Envers 还需要一套表。只要将spring.jpa.hibernate.ddl-auto设置为将模式 DDL 导出到数据库中,就可以从 JPA 注释(例如@Audited@AuditedTable)中生成这些表。这样的应用可以在 GitHub 3 上找到。

但是,在生产中,依赖这种实践是一个坏主意。如果不需要自动模式迁移,那么schema-*.sql可以为您完成这项工作。否则,最好依靠 Flyway 或 Liquibase 之类的工具。

在这两种情况下,开发人员都需要 Envers 表的CREATE TABLE语句。在这种情况下,这些语句如下(注意,表的名称对应于通过@AuditedTable指定的名称):

CREATE TABLE author_audit (
  id BIGINT(20) NOT NULL,
  rev INT(11) NOT NULL,
  revtype TINYINT(4) DEFAULT NULL,
  revend INT(11) DEFAULT NULL,
  age INT(11) DEFAULT NULL,
  genre VARCHAR(255) DEFAULT NULL,
  name VARCHAR(255) DEFAULT NULL,
  PRIMARY KEY (id,rev),
  KEY FKp4vbplw134mimnk3nlxfvmch0 (rev),
  KEY FKdtg6l7ccqhpsdnkltcoisi9l9 (revend));

CREATE TABLE book_audit (
  id BIGINT(20) NOT NULL,
  rev INT(11) NOT NULL,
  revtype TINYINT(4) DEFAULT NULL,
  revend INT(11) DEFAULT NULL,
  isbn VARCHAR(255) DEFAULT NULL,
  title VARCHAR(255) DEFAULT NULL,
  author_id BIGINT(20) DEFAULT NULL,
  PRIMARY KEY (id,rev),
  KEY FKjx5fxkthrd6kxbxb3ukwb04mf (rev),
  KEY FKr9ed64q1nek7vjfbcxm04v8ic (revend));

CREATE TABLE revinfo (
  rev INT(11) NOT NULL AUTO_INCREMENT,
  revtstmp BIGINT(20) DEFAULT NULL,
  PRIMARY KEY (rev));

如果 Envers 不能自动识别所使用的模式,那么模式名应该如下传递:

  • 对于 MySQL: spring.jpa.properties.org.hibernate.envers.default_catalog

  • 其他:spring.jpa.properties.org.hibernate.envers.default_schema

完整的代码可以在 GitHub 4 上找到。

查询实体快照

Hibernate Envers 为查询实体快照提供了支持。起点由AuditReaderFactory表示,它是AuditReader对象的工厂。

您可以通过 JPA EntityManager或 Hibernate Session构建一个AuditReader,如下所示:

EntityManager em;
...
// via EntityManager
AuditReader reader = AuditReaderFactory.get(em);

// via Session
AuditReader reader = AuditReaderFactory.get(em.unwrap(Session.class));

AuditReader是审计日志的一系列特性的入口点。在它的特性中,AuditReader允许您通过createQuery()方法查询审计日志。这里有两个例子:

  • 获取在修订版 #3 中修改的所有Book实例:

  • 获取所有被审计状态中的所有Book实例:

List<Book> books = reader.createQuery()
    .forEntitiesAtRevision(Book.class, 3).getResultList();

List<Book> books = reader.createQuery()
    .forRevisionsOfEntity(Book.class, true, true).getResultList();

我强烈建议您花些时间了解一下这个 API,因为它有太多的特性。尤其是当您需要更高级的查询时。

有效性审计策略审计日志记录策略

默认情况下,Hibernate Envers 使用名为DefaultAuditStrategy的审计日志策略。让我们使用下面的查询(获取在修订版 #3 中修改的所有Book实例):

List<Book> books = reader.createQuery()
    .forEntitiesAtRevision(Book.class, 3).getResultList();

幕后触发的SELECT如下:

SELECT
  book_aud0_.id AS id1_3_,
  book_aud0_.rev AS rev2_3_,
  book_aud0_.revtype AS revtype3_3_,
  book_aud0_.isbn AS isbn4_3_,
  book_aud0_.title AS title5_3_,
  book_aud0_.author_id AS author_i6_3_
FROM book_audit book_aud0_
WHERE book_aud0_.rev =
  (
    SELECT MAX(book_aud1_.rev)
    FROM book_audit book_aud1_
    WHERE book_aud1_.rev <= ?
    AND book_aud0_.id = book_aud1_.id
  )
AND book_aud0_.revtype <> ?

很明显,这个查询的性能不是很好,尤其是当审计日志很大时(查看SELECT子查询)。

但是DefaultAuditStrategy只是AuditStrategy的实现之一。另一个是ValidityAuditStrategy。您可以使用application.properties在 Spring Boot 应用中启用该策略,如下所示:

spring.jpa.properties.org.hibernate.envers.audit_strategy
    =org.hibernate.envers.strategy.ValidityAuditStrategy

在 Hibernate 版之前,正确的值是org.hibernate.envers.strategy.internal.ValidityAuditStrategy

一旦ValidityAuditStrategy被启用,您可以再次尝试相同的查询。这一次,SQL 语句更加高效:

SELECT
  book_aud0_.id AS id1_3_,
  book_aud0_.rev AS rev2_3_,
  book_aud0_.revtype AS revtype3_3_,
  book_aud0_.revend AS revend4_3_,
  book_aud0_.isbn AS isbn5_3_,
  book_aud0_.title AS title6_3_,
  book_aud0_.author_id AS author_i7_3_
FROM book_audit book_aud0_
WHERE book_aud0_.rev <= ?
AND book_aud0_.revtype <> ?
AND (book_aud0_.revend > ?
OR book_aud0_.revend IS NULL)

这次,没有SELECT子查询!很好!此外,这可以通过为revendrev列添加一个索引来改进。通过这种方式,避免了顺序扫描,Envers 变得更加高效。然而,revend列仅在您使用ValidityAuditStrategy时出现,并且它引用了revinfo表。其目的是标记该实体快照仍然有效的最后一次修订。

请记住,ValidityAuditStrategy非常擅长快速实体快照抓取,但是在保存数据库中的实体状态时,它的性能比DefaultAuditStrategy差。通常在写作过程中花费额外的时间和更快的阅读速度是值得的,但这不是一个普遍的规则。如果你需要的话,选择DefaultAuditStrategy并没有错。

项目 90:如何检查持久性上下文

你有没有想过持久性上下文中有什么?或者某个实体或集合是否在当前的持久化上下文中?您可以通过org.hibernate.engine.spi.PersistenceContext检查 Hibernate 持久性上下文。首先,一个帮助器方法利用SharedSessionContractImplementor来获取PersistenceContext,如下所示:

@PersistenceContext
private final EntityManager entityManager;
...
private org.hibernate.engine.spi.PersistenceContext getPersistenceContext() {

    SharedSessionContractImplementor sharedSession = entityManager.unwrap(
        SharedSessionContractImplementor.class
    );

    return sharedSession.getPersistenceContext();
}

此外,PersistenceContext提供了大量添加、删除和检查其内容的方法。例如,以下方法显示了受管实体的总数及其相关信息,包括它们的状态和水合状态:

private void briefOverviewOfPersistentContextContent() {

    org.hibernate.engine.spi.PersistenceContext persistenceContext
        = getPersistenceContext();

    int managedEntities
        = persistenceContext.getNumberOfManagedEntities();
    int collectionEntriesSize
        = persistenceContext.getCollectionEntriesSize();

    System.out.println("Total number of managed entities: "
        + managedEntities);
    System.out.println("Total number of collection entries: "
        + collectionEntriesSize);

    // getEntitiesByKey() will be removed and probably replaced
    // with #iterateEntities()
    Map<EntityKey, Object> entitiesByKey
        = persistenceContext.getEntitiesByKey();

    if (!entitiesByKey.isEmpty()) {
        System.out.println("\nEntities by key:");
        entitiesByKey.forEach((key, value) -> System.out.println(key
            + ": " + value));

        System.out.println("\nStatus and hydrated state:");
        for (Object entry : entitiesByKey.values()) {
            EntityEntry ee = persistenceContext.getEntry(entry);
            System.out.println(
                "Entity name: " + ee.getEntityName()
                    + " | Status: " + ee.getStatus()
                    + " | State: " + Arrays.toString(ee.getLoadedState()));
        }
    }

    if (collectionEntriesSize > 0) {
        System.out.println("\nCollection entries:");
        persistenceContext.forEachCollectionEntry(
            (k, v) -> System.out.println("Key:" + k
                + ", Value:" + (v.getRole() == null ? "" : v)), false);
    }
}

让我们看看双向懒惰@OneToMany关联中的AuthorBook实体。以下服务方法:

  • 找到一个作者

  • 获取相关书籍

  • 删除作者和相关书籍

  • 用一本书创建一个新作者

在每个操作之后,执行briefOverviewOfPersistentContextContent()方法调用:

@Transactional
public void sqlOperations() {

    briefOverviewOfPersistentContextContent();

    Author author = authorRepository.findByName("Joana Nimar");
    briefOverviewOfPersistentContextContent();

    author.getBooks().get(0).setIsbn("not available");
    briefOverviewOfPersistentContextContent();

    authorRepository.delete(author);
    authorRepository.flush();
    briefOverviewOfPersistentContextContent();

    Author newAuthor = new Author();
    newAuthor.setName("Alicia Tom");
    newAuthor.setAge(38);
    newAuthor.setGenre("Anthology");

    Book book = new Book();
    book.setIsbn("001-AT");
    book.setTitle("The book of swords");

    newAuthor.addBook(book); // use addBook() helper

    authorRepository.saveAndFlush(newAuthor);
    briefOverviewOfPersistentContextContent();
}

调用sqlOperations()输出:

最初,持久性上下文为空:

Total number of managed entities: 0
Total number of collection entities: 0

SELECT被触发后,乔安娜·尼玛尔:

Total number of managed entities: 1
Total number of collection entries: 1

Entities by key:
EntityKey[com.bookstore.entity.Author#4]:
    Author{id=4, name=Joana Nimar, genre=History, age=34}

Status and hydrated state (because we required the hydrated state, Hibernate will trigger a SELECT to fetch the books of this author):
Entity name: com.bookstore.entity.Author
    | Status: MANAGED
    | State: [34, [Book{id=1, title=A History of Ancient Prague,
                isbn=001-JN}, Book{id=2, title=A People's History,
                isbn=002-JN}], History, Joana Nimar]

Collection entries:
Key:[Book{id=1, title=A History of Ancient Prague, isbn=001-JN}, Book{id=2, title=A People's History, isbn=002-JN}], Value:CollectionEntry[com.bookstore.entity.Author.books#4]

在针对乔安娜·尼玛尔的图书的SELECT语句被触发后(有两本书):

Total number of managed entities: 3
Total number of collection entries: 1

Entities by key:
EntityKey[com.bookstore.entity.Book#2]:
    Book{id=2, title=A People's History, isbn=002-JN}
EntityKey[com.bookstore.entity.Author#4]:
    Author{id=4, name=Joana Nimar, genre=History, age=34}
EntityKey[com.bookstore.entity.Book#1]:
    Book{id=1, title=A History of Ancient Prague, isbn=not available}

Status and hydrated state:
Entity name: com.bookstore.entity.Book
    | Status: MANAGED
    | State: [Author{id=4, name=Joana Nimar, genre=History, age=34},
                002-JN, A People's History]

Entity name: com.bookstore.entity.Author
    | Status: MANAGED
    | State: [34, [Book{id=1, title=A History of Ancient Prague,
                isbn=not available}, Book{id=2, title=A People's History,
                isbn=002-JN}], History, Joana Nimar]

Entity name: com.bookstore.entity.Book
    | Status: MANAGED
    | State: [Author{id=4, name=Joana Nimar, genre=History, age=34},
                001-JN, A History of Ancient Prague]

Collection entries:
Key:[Book{id=1, title=A History of Ancient Prague, isbn=not available}, Book{id=2, title=A People's History, isbn=002-JN}], Value:CollectionEntry[com.bookstore.entity.Author.books#4]

在作者和相关书籍的DELETE语句被触发后:

Total number of managed entities: 0
Total number of collection entities: 0

在持续新作者和他们的书的INSERT语句被触发后:

Total number of managed entities: 2
Total number of collection entries: 1

Entities by key:
EntityKey[com.bookstore.entity.Book#5]:
    Book{id=5, title=The book of swords, isbn=001-AT}
EntityKey[com.bookstore.entity.Author#5]:
    Author{id=5, name=Alicia Tom, genre=Anthology, age=38}

Status and hydrated state:
Entity name: com.bookstore.entity.Book
    | Status: MANAGED
    | State: [Author{id=5, name=Alicia Tom, genre=Anthology, age=38},
                001-AT, The book of swords]

Entity name: com.bookstore.entity.Author
    | Status: MANAGED
    | State: [38, [Book{id=5, title=The book of swords,
                isbn=001-AT}], Anthology, Alicia Tom]

Collection entries:
Key:[Book{id=5, title=The book of swords, isbn=001-AT}], Value:CollectionEntry[com.bookstore.entity.Author.books#5]
    ->[com.bookstore.entity.Author.books#5]

这只是一个让你熟悉PersistenceContext API 的例子。仔细阅读文档以发现更多有用的方法。

完整的应用可在 GitHub 5 上获得。

项目 91:如何提取表元数据

您可以通过 Hibernate SPI,org.hibernate.integrator.spi.Integrator提取表元数据(或者一般来说,数据库元数据)。实现Integrator包括覆盖integrate()方法并返回metadata.getDatabase(),如下所示:

public class DatabaseTableMetadataExtractor
            implements org.hibernate.integrator.spi.Integrator {

    public static final DatabaseTableMetadataExtractor EXTRACTOR
        = new DatabaseTableMetadataExtractor();

    private Database database;

    // this method will be deprecated starting with Hibernate 6.0
    @Override
    public void integrate(
        Metadata metadata,
        SessionFactoryImplementor sessionImplementor,
        SessionFactoryServiceRegistry serviceRegistry) {

        database = metadata.getDatabase();
    }

    @Override
    public void disintegrate(
        SessionFactoryImplementor sessionImplementor,
        SessionFactoryServiceRegistry serviceRegistry) {
    }

    public Database getDatabase() {
        return database;
    }
}

接下来,通过LocalContainerEntityManagerFactoryBean注册该Integrator,如下所示:

@Configuration
@EnableJpaRepositories(
    entityManagerFactoryRef = "entityManagerFactory",
    transactionManagerRef = "transactionManager",
    basePackages = "com.bookstore.*"
)
@EnableTransactionManagement
public class EntityManagerFactoryConfig {

    @Bean
    @Primary
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder builder, DataSource dataSource) {

        return builder
            .dataSource(dataSource)
            .packages(packagesToScan())
            .persistenceUnit("ds-pu")
            .properties(hibernateProperties())
            .build();
    }

    @Bean
    @Primary
    public PlatformTransactionManager transactionManager(
            @Qualifier("entityManagerFactory") EntityManagerFactory                             entityManagerFactory) {

        return new JpaTransactionManager(entityManagerFactory);
    }

    protected String[] packagesToScan() {
        return new String[]{
            "com.bookstore.*"
        };
    }

    protected Map<String, Object> hibernateProperties() {
        return new HashMap<String, Object>() {
            {
                put("hibernate.dialect",
                    "org.hibernate.dialect.MySQL5Dialect");
                put("hibernate.hbm2ddl.auto", "create");
                put("hibernate.integrator_provider",
                    (IntegratorProvider) () -> Collections.singletonList(
                        DatabaseTableMetadataExtractor.EXTRACTOR
                    ));
            }
        };
    }
}

搞定了。现在,让我们使用图 11-5 所示的领域模型。

img/487471_1_En_11_Fig5_HTML.jpg

图 11-5

领域模型

您可以提取并显示映射表的元数据,如下所示(每个实体类有一个映射表):

public void extractTablesMetadata() {
    for (Namespace namespace :
                DatabaseTableMetadataExtractor.EXTRACTOR
                    .getDatabase()
                    .getNamespaces()) {

        namespace.getTables().forEach(this::displayTablesMetdata);
    }
}

private void displayTablesMetdata(Table table) {

    System.out.println("\nTable: " + table);
    Iterator it = table.getColumnIterator();
    while (it.hasNext()) {
        System.out.println(it.next());
    }
}

调用extractTablesMetadata()将产生以下输出:

Table: org.hibernate.mapping.Table(Author)
org.hibernate.mapping.Column(id)
org.hibernate.mapping.Column(age)
org.hibernate.mapping.Column(genre)
org.hibernate.mapping.Column(name)

Table: org.hibernate.mapping.Table(Book)
org.hibernate.mapping.Column(id)
org.hibernate.mapping.Column(isbn)
org.hibernate.mapping.Column(title)
org.hibernate.mapping.Column(author_id)

Table: org.hibernate.mapping.Table(Ebook)
org.hibernate.mapping.Column(format)
org.hibernate.mapping.Column(ebook_book_id)

Table: org.hibernate.mapping.Table(Paperback)
org.hibernate.mapping.Column(sizeIn)
org.hibernate.mapping.Column(weightLbs)
org.hibernate.mapping.Column(paperback_book_id)

完整的应用可在 GitHub 6 上获得。

Footnotes 1

hibernate pringb booth audit

  2

hibernate pringb ottime tampon 配给

  3

hibernate pringb ooten vs

  4

hibernate pringb 欧顿方案 ql

  5

hibernate pringb oostinspection persistent tencontext

  6

hibernate pringb boottablestop a