About Vlad Mihalcea

Vlad Mihalcea is a software architect passionate about software integration, high scalability and concurrency challenges.

Hibernate Facts: How to “assert” the SQL statement count

Introduction

Hibernate simplifies CRUD operations, especially when dealing with entity graphs. But any abstraction has its price and Hibernate is no different. I’ve already talked about the importance of fetching strategy and knowing your Criteria SQL queries, but there is more you can do to rule over JPA. This post is about controlling the SQL statement count that Hibernate calls on your behalf.

Before ORM tools got so popular, all database interactions were done through explicit SQL statements, and optimizations were mostly targeted towards slow queries.

Hibernate may give the false impression that you don’t need to worry about SQL statements. This is a wrong and dangerous assumption. Hibernate is supposed to ease domain model persistence, not to free you from any SQL interaction.

With Hibernate, you manage entity state transitions which are then translated to SQL statements. The number of generated SQL statements is affected by the current fetching strategy, Criteria queries or Collection mappings and you might not always get what you expected. Ignoring SQL statements is risky and it may eventually put a heavy toll on the overall application performance.

I’m a strong advocate of peer reviewing, but that’s not “sine qua non” for detecting bad Hibernate usage. Subtle changes may affect the SQL statement count and pass unnoticed through the reviewing process. Not in the least, when it comes to “guessing” the JPA SQL statements, I feel like I can use any extra help. I’m for as much automation as possible, and that’s why I came up with a mechanism for enforcing the SQL statement count expectations.

First, we need a way to intercept all executed SQL statements. I researched on this topic and I was lucky to find this great datasource-proxy library.

Adding an automated validator

This safeguard is meant to run only during the testing phase, so I’ll add it to the Integration Testing spring context exclusively. I’ve already talked about Spring bean aliasing and this is the right time to make use of it.

<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"/>

The new proxy data source decorates the already existing data source, intercepting all executed SQL statements. This library can log all SQL statements along with the actual parameter values, unlike the default Hibernate logging which only prints a placeholder instead.

This is how the validator looks like:

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);
        }
    }
}

This utility is part of my db-util project along with the JPA and MongoDB optimistic concurrency control retry mechanism.

Since it’s already available in Maven Central Repository, you can easily use it by just adding this dependency to your pom.xml:

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

Let’s write a test for detecting the infamous N+1 select query problem.

For this we will write two service methods, one of them being affected by the aforementioned issue:

@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();
	}
}

The unit test is rather simple to write since it follows the same coding style of any other JUnit assert mechanism.

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

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

Our validator works for all SQL statement types, so let’s check how many SQL INSERTs are being executed by the following service method:

@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;
}

And the validator looks like:

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

Let’s check the test logs to convince ourselves of its effectiveness:

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]}

Conclusion

Code reviewing is a fine technique, but it’s not enough on large scale development projects. That’s why automatic checking is of paramount importance. Once the test written, you are assured that no future change can break your assumptions.

 

Do you want to know how to develop your skillset to become a Java Rockstar?

Subscribe to our newsletter to start Rocking right now!

To get you started we give you two of our best selling eBooks for FREE!

JPA Mini Book

Learn how to leverage the power of JPA in order to create robust and flexible Java applications. With this Mini Book, you will get introduced to JPA and smoothly transition to more advanced concepts.

JVM Troubleshooting Guide

The Java virtual machine is really the foundation of any Java EE platform. Learn how to master it with this advanced guide!

Given email address is already subscribed, thank you!
Oops. Something went wrong. Please try again later.
Please provide a valid email address.
Thank you, your sign-up request was successful! Please check your e-mail inbox.
Please complete the CAPTCHA.
Please fill in the required fields.

Leave a Reply


seven − 7 =



Java Code Geeks and all content copyright © 2010-2014, Exelixis Media Ltd | Terms of Use | Privacy Policy | Contact
All trademarks and registered trademarks appearing on Java Code Geeks are the property of their respective owners.
Java is a trademark or registered trademark of Oracle Corporation in the United States and other countries.
Java Code Geeks is not connected to Oracle Corporation and is not sponsored by Oracle Corporation.
Do you want to know how to develop your skillset and become a ...
Java Rockstar?

Subscribe to our newsletter to start Rocking right now!

To get you started we give you two of our best selling eBooks for FREE!

Get ready to Rock!
You can download the complementary eBooks using the links below:
Close