介绍
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