介绍

Hibernate简化了CRUD操作,尤其是在处理实体图时。 但是任何抽象都有其代价,而Hibernate也不例外。 我已经讨论了获取策略和了解Criteria SQL查询的重要性,但是您可以做更多的事情来统治JPA。 这篇文章是关于控制Hibernate代表您调用SQL语句计数的。

在ORM工具如此流行之前,所有数据库交互都是通过显式SQL语句完成的,而优化主要针对慢速查询。

Hibernate可能会给人一种错误的印象,即您不必担心SQL语句。 这是一个错误和危险的假设。 Hibernate应该减轻域模型的持久性,而不是使您摆脱任何SQL交互。

使用Hibernate,您可以管理实体状态转换,然后转换为SQL语句。 生成SQL语句的数量受当前的获取策略,条件查询或集合映射影响,您可能并不总是能获得所需的结果。 忽略SQL语句是有风险的,最终可能会给整个应用程序性能带来沉重的负担。

我是同行评审的坚定倡导者,但这并不是发现不良的Hibernate使用情况的“必要条件”。 细微的更改可能会影响SQL语句的计数,并且在检查过程中不会引起注意。 至少,当“猜测” JPA SQL语句时,我觉得我可以使用任何其他帮助。 我要尽可能地实现自动化,这就是为什么我想出一种用于执行SQL语句计数期望的机制的原因。

首先,我们需要一种方法来拦截所有已执行SQL语句。 我对此主题进行了研究,很幸运能找到这个出色的数据源代理库。

添加自动验证器

此保护措施旨在仅在测试阶段运行,因此我将其专门添加到“集成测试”弹簧上下文中。 我已经讨论过Spring bean别名 ,现在正是使用它的合适时机。

<bean id="testDataSource" class="bitronix.tm.resource.jdbc.PoolingDataSource" init-method="init"
	  destroy-method="close">
	<property name="className" value="bitronix.tm.resource.jdbc.lrc.LrcXADataSource"/>
	<property name="uniqueName" value="testDataSource"/>
	<property name="minPoolSize" value="0"/>
	<property name="maxPoolSize" value="5"/>
	<property name="allowLocalTransactions" value="false" />
	<property name="driverProperties">
		<props>
			<prop key="user">${jdbc.username}</prop>
			<prop key="password">${jdbc.password}</prop>
			<prop key="url">${jdbc.url}</prop>
			<prop key="driverClassName">${jdbc.driverClassName}</prop>
		</props>
	</property>
</bean>

<bean id="proxyDataSource" class="net.ttddyy.dsproxy.support.ProxyDataSource">
	<property name="dataSource" ref="testDataSource"/>
	<property name="listener">
		<bean class="net.ttddyy.dsproxy.listener.ChainListener">
			<property name="listeners">
				<list>
					<bean class="net.ttddyy.dsproxy.listener.CommonsQueryLoggingListener">
						<property name="logLevel" value="INFO"/>
					</bean>
					<bean class="net.ttddyy.dsproxy.listener.DataSourceQueryCountListener"/>
				</list>
			</property>
		</bean>
	</property>
</bean>

<alias name="proxyDataSource" alias="dataSource"/>

新的代理数据源将装饰现有数据源,从而拦截所有已执行SQL语句。 该库可以记录所有SQL语句以及实际参数值,这与默认的Hibernate记录不同,该记录只显示一个占位符。

验证器的外观如下:

public class SQLStatementCountValidator {

    private SQLStatementCountValidator() {
    }

    /**
     * Reset the statement recorder
     */
    public static void reset() {
        QueryCountHolder.clear();
    }

    /**
     * Assert select statement count
     * @param expectedSelectCount expected select statement count
     */
    public static void assertSelectCount(int expectedSelectCount) {
        QueryCount queryCount = QueryCountHolder.getGrandTotal();
        int recordedSelectCount = queryCount.getSelect();
        if(expectedSelectCount != recordedSelectCount) {
            throw new SQLSelectCountMismatchException(expectedSelectCount, recordedSelectCount);
        }
    }

    /**
     * Assert insert statement count
     * @param expectedInsertCount expected insert statement count
     */
    public static void assertInsertCount(int expectedInsertCount) {
        QueryCount queryCount = QueryCountHolder.getGrandTotal();
        int recordedInsertCount = queryCount.getInsert();
        if(expectedInsertCount != recordedInsertCount) {
            throw new SQLInsertCountMismatchException(expectedInsertCount, recordedInsertCount);
        }
    }

    /**
     * Assert update statement count
     * @param expectedUpdateCount expected update statement count
     */
    public static void assertUpdateCount(int expectedUpdateCount) {
        QueryCount queryCount = QueryCountHolder.getGrandTotal();
        int recordedUpdateCount = queryCount.getUpdate();
        if(expectedUpdateCount != recordedUpdateCount) {
            throw new SQLUpdateCountMismatchException(expectedUpdateCount, recordedUpdateCount);
        }
    }

    /**
     * Assert delete statement count
     * @param expectedDeleteCount expected delete statement count
     */
    public static void assertDeleteCount(int expectedDeleteCount) {
        QueryCount queryCount = QueryCountHolder.getGrandTotal();
        int recordedDeleteCount = queryCount.getDelete();
        if(expectedDeleteCount != recordedDeleteCount) {
            throw new SQLDeleteCountMismatchException(expectedDeleteCount, recordedDeleteCount);
        }
    }
}

该实用程序与JPA和MongoDB乐观并发控制重试机制一起,是我的db-util项目的一部分。

由于它已经在Maven Central Repository中提供,因此只需将以下依赖项添加到pom.xml中就可以轻松使用它:

<dependency>
	<groupId>com.vladmihalcea</groupId>
	<artifactId>db-util</artifactId>
	<version>0.0.1</version>
</dependency>

让我们写一个测试来检测臭名昭著的N + 1选择查询问题 。

为此,我们将编写两种服务方法,其中一种受到上述问题的影响:

@Override
@Transactional
public List<WarehouseProductInfo> findAllWithNPlusOne() {
	List<WarehouseProductInfo> warehouseProductInfos = entityManager.createQuery(
			"from WarehouseProductInfo", WarehouseProductInfo.class).getResultList();
	navigateWarehouseProductInfos(warehouseProductInfos);
	return warehouseProductInfos;
}

@Override
@Transactional
public List<WarehouseProductInfo> findAllWithFetch() {
	List<WarehouseProductInfo> warehouseProductInfos = entityManager.createQuery(
			"from WarehouseProductInfo wpi " +
			"join fetch wpi.product p " +
			"join fetch p.company", WarehouseProductInfo.class).getResultList();
	navigateWarehouseProductInfos(warehouseProductInfos);
	return warehouseProductInfos;
}

private void navigateWarehouseProductInfos(List<WarehouseProductInfo> warehouseProductInfos) {
	for(WarehouseProductInfo warehouseProductInfo : warehouseProductInfos) {
		warehouseProductInfo.getProduct();
	}
}

单元测试非常简单,因为它遵循与任何其他JUnit断言机制相同的编码风格。

try {
	SQLStatementCountValidator.reset();
	warehouseProductInfoService.findAllWithNPlusOne();
	assertSelectCount(1);
} catch (SQLSelectCountMismatchException e) {
	assertEquals(3, e.getRecorded());
}

SQLStatementCountValidator.reset();
warehouseProductInfoService.findAllWithFetch();
assertSelectCount(1);

我们的验证器适用于所有SQL语句类型,因此让我们检查以下服务方法正在执行多少个SQL INSERT:

@Override
@Transactional
public WarehouseProductInfo newWarehouseProductInfo() {

	LOGGER.info("newWarehouseProductInfo");

	Company company = entityManager.createQuery("from Company", Company.class).getResultList().get(0);

	Product product3 = new Product("phoneCode");
	product3.setName("Phone");
	product3.setCompany(company);

	WarehouseProductInfo warehouseProductInfo3 = new WarehouseProductInfo();
	warehouseProductInfo3.setQuantity(19);
	product3.addWarehouse(warehouseProductInfo3);

	entityManager.persist(product3);
	return warehouseProductInfo3;
}

验证器看起来像:

SQLStatementCountValidator.reset();
warehouseProductInfoService.newWarehouseProductInfo();
assertSelectCount(1);
assertInsertCount(2);

让我们检查一下测试日志,以使自己确信其有效性:

INFO  [main]: o.v.s.i.WarehouseProductInfoServiceImpl - newWarehouseProductInfo
Hibernate: select company0_.id as id1_6_, company0_.name as name2_6_ from Company company0_
INFO  [main]: n.t.d.l.CommonsQueryLoggingListener - Name:, Time:1, Num:1, Query:{[select company0_.id as id1_6_, company0_.name as name2_6_ from Company company0_][]}
Hibernate: insert into WarehouseProductInfo (id, quantity) values (default, ?)
INFO  [main]: n.t.d.l.CommonsQueryLoggingListener - Name:, Time:0, Num:1, Query:{[insert into WarehouseProductInfo (id, quantity) values (default, ?)][19]}
Hibernate: insert into Product (id, code, company_id, importer_id, name, version) values (default, ?, ?, ?, ?, ?)
INFO  [main]: n.t.d.l.CommonsQueryLoggingListener - Name:, Time:0, Num:1, Query:{[insert into Product (id, code, company_id, importer_id, name, version) values (default, ?, ?, ?, ?, ?)][phoneCode,1,-5,Phone,0]}

结论

代码审查是一种很好的技术,但是在大规模开发项目中还远远不够。 这就是为什么自动检查至关重要。 一旦编写了测试,您可以放心,将来的任何更改都不会破坏您的假设。

  • 代码可在GitHub上获得 。

参考: Hibernate Fact:如何通过Vlad Mihalcea的Blog博客从我们的JCG合作伙伴 Vlad Mihalcea “断言” SQL语句计数 。

翻译自: https://www.javacodegeeks.com/2014/02/hibernate-facts-how-to-assert-the-sql-statement-count.html