Introduction

When you’re using JDBC or if you are generating SQL statements by hand, you always know what statements are sent to the database server. Although there are situations when a native query is the most obvious solution to a given business use case, most statements are simple enough to be generated automatically That’s exactly what JPA and Hibernate do, and the application developer can focus on entity state transitions instead.

Nevertheless, the application developer must always assert that Hibernate generates the expected statements, as well as the number of statements being generated (to avoid N+1 query issues).

Proxying the underlying JDBC Driver or DataSource

In production, it’s very common to Proxy the underlying Driver Connection providing mechanism so that the application benefits from connection pooling, or for monitoring connection pool usage. For this purpose, the underlying JDBC Driver or DataSource can be proxied using tools such as P6spy or datasource-proxy. In fact, this is also a very convenient way of logging JDBC statements along with their bind parameters.

While for many application, it’s not an issue to add yet another dependency when you are developing an open source framework you strive to minimize the number of dependencies your project needs to depend on. Luckily, for Hibernate, we don’t even need to use an external dependency for intercepting JDBC statements, and this post is going to show you how easily you can tackle this requirement.

StatementInspector

For many use cases, the StatementInspector is the only thing you need to capture all SQL statements that are executed by Hibernate. The StatementInspector must be provided during SessionFactory bootstrapping as follows:

public class SQLStatementInterceptor {

    private final LinkedList<String> sqlQueries = new LinkedList<>();

    public SQLStatementInterceptor(SessionFactoryBuilder sessionFactoryBuilder) {
        sessionFactoryBuilder.applyStatementInspector(
        (StatementInspector) sql -> {
            sqlQueries.add( sql );
            return sql;
        } );
    }

    public LinkedList<String> getSqlQueries() {
        return sqlQueries;
    }
}

With this utility we can easily verify the Oracle follow-on-locking mechanism which is caused by the FOR UPDATE clause restrictions imposed by the database engine:

sqlStatementInterceptor.getSqlQueries().clear();

List<Product> products = session.createQuery(
    "select p from Product p", Product.class )
.setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) )
.setFirstResult( 40 )
.setMaxResults( 10 )
.getResultList();

assertEquals( 10, products.size() );
assertEquals( 11, sqlStatementInterceptor.getSqlQueries().size() );

So far, so good. But as simple as the StatementInspector may be, it does not mix well with JDBC batching. StatementInspector intercepts the prepare phase, whereas for batching we need to intercept the addBatch and executeBatch method calls.

Even without native support for such a feature, we can easily design a custom ConnectionProvider that can intercept all PreparedStatement method calls.

First, we start with the ConnectionProviderDelegate which is capable of substituting any other ConnectionProvider that would otherwise be picked by Hibernate (e.g. DatasourceConnectionProviderImpl, DriverManagerConnectionProviderImpl, HikariCPConnectionProvider) for the current configuration properties.

public class ConnectionProviderDelegate implements
        ConnectionProvider,
        Configurable,
        ServiceRegistryAwareService {

    private ServiceRegistryImplementor serviceRegistry;

    private ConnectionProvider connectionProvider;

    @Override
    public void injectServices(ServiceRegistryImplementor serviceRegistry) {
        this.serviceRegistry = serviceRegistry;
    }

    @Override
    public void configure(Map configurationValues) {
        @SuppressWarnings("unchecked")
        Map<String, Object> settings = new HashMap<>( configurationValues );
        settings.remove( AvailableSettings.CONNECTION_PROVIDER );
        connectionProvider = ConnectionProviderInitiator.INSTANCE.initiateService(
                settings,
                serviceRegistry
        );
        if ( connectionProvider instanceof Configurable ) {
            Configurable configurableConnectionProvider = (Configurable) connectionProvider;
            configurableConnectionProvider.configure( settings );
        }
    }

    @Override
    public Connection getConnection() throws SQLException {
        return connectionProvider.getConnection();
    }

    @Override
    public void closeConnection(Connection conn) throws SQLException {
        connectionProvider.closeConnection( conn );
    }

    @Override
    public boolean supportsAggressiveRelease() {
        return connectionProvider.supportsAggressiveRelease();
    }

    @Override
    public boolean isUnwrappableAs(Class unwrapType) {
        return connectionProvider.isUnwrappableAs( unwrapType );
    }

    @Override
    public <T> T unwrap(Class<T> unwrapType) {
        return connectionProvider.unwrap( unwrapType );
    }
}

With the ConnectionProviderDelegate in place, we can now implement the PreparedStatementSpyConnectionProvider which, using Mockito, it returns a Connection spy instead of an actual JDBC Driver Connection object:

public class PreparedStatementSpyConnectionProvider
        extends ConnectionProviderDelegate {

    private final Map<PreparedStatement, String> preparedStatementMap = new LinkedHashMap<>();

    @Override
    public Connection getConnection() throws SQLException {
        Connection connection = super.getConnection();
        return spy( connection );
    }

    private Connection spy(Connection connection) {
        if ( new MockUtil().isMock( connection ) ) {
            return connection;
        }
        Connection connectionSpy = Mockito.spy( connection );
        try {
            doAnswer( invocation -> {
                PreparedStatement statement = (PreparedStatement) invocation.callRealMethod();
                PreparedStatement statementSpy = Mockito.spy( statement );
                String sql = (String) invocation.getArguments()[0];
                preparedStatementMap.put( statementSpy, sql );
                return statementSpy;
            } ).when( connectionSpy ).prepareStatement( anyString() );
        }
        catch ( SQLException e ) {
            throw new IllegalArgumentException( e );
        }
        return connectionSpy;
    }

    /**
     * Clears the recorded PreparedStatements and reset the associated Mocks.
     */
    public void clear() {
        preparedStatementMap.keySet().forEach( Mockito::reset );
        preparedStatementMap.clear();
    }

    /**
     * Get one and only one PreparedStatement associated to the given SQL statement.
     *
     * @param sql SQL statement.
     *
     * @return matching PreparedStatement.
     *
     * @throws IllegalArgumentException If there is no matching PreparedStatement or multiple instances, an exception is being thrown.
     */
    public PreparedStatement getPreparedStatement(String sql) {
        List<PreparedStatement> preparedStatements = getPreparedStatements( sql );
        if ( preparedStatements.isEmpty() ) {
            throw new IllegalArgumentException(
                    "There is no PreparedStatement for this SQL statement " + sql );
        }
        else if ( preparedStatements.size() > 1 ) {
            throw new IllegalArgumentException( "There are " + preparedStatements
                    .size() + " PreparedStatements for this SQL statement " + sql );
        }
        return preparedStatements.get( 0 );
    }

    /**
     * Get the PreparedStatements that are associated to the following SQL statement.
     *
     * @param sql SQL statement.
     *
     * @return list of recorded PreparedStatements matching the SQL statement.
     */
    public List<PreparedStatement> getPreparedStatements(String sql) {
        return preparedStatementMap.entrySet()
                .stream()
                .filter( entry -> entry.getValue().equals( sql ) )
                .map( Map.Entry::getKey )
                .collect( Collectors.toList() );
    }

    /**
     * Get the PreparedStatements that were executed since the last clear operation.
     *
     * @return list of recorded PreparedStatements.
     */
    public List<PreparedStatement> getPreparedStatements() {
        return new ArrayList<>( preparedStatementMap.keySet() );
    }
}

To use this custom provider, we just need to provide an instance via the hibernate.connection.provider_class configuration property:

private PreparedStatementSpyConnectionProvider connectionProvider =
    new PreparedStatementSpyConnectionProvider();

@Override
protected void addSettings(Map settings) {
    settings.put(
            AvailableSettings.CONNECTION_PROVIDER,
            connectionProvider
    );
}

Now, we can assert that the underlying PreparedStatement is batching statements according to our expectations:

Session session = sessionFactory().openSession();
session.setJdbcBatchSize( 3 );

session.beginTransaction();
try {
    for ( long i = 0; i < 5; i++ ) {
        Event event = new Event();
        event.id = id++;
        event.name = "Event " + i;
        session.persist( event );
    }
}
finally {
    connectionProvider.clear();
    session.getTransaction().commit();
    session.close();
}

PreparedStatement preparedStatement = connectionProvider.getPreparedStatement(
    "insert into Event (name, id) values (?, ?)" );

verify(preparedStatement, times( 5 )).addBatch();
verify(preparedStatement, times( 2 )).executeBatch();

The PreparedStatement is not a mock but a real object spy, which can intercept method call while also propagating the call to the underlying actual JDBC Driver PreparedStatement object.

Although getting the PreparedStatement by its associated SQL String is useful for the aforementioned test case, we can also get all executed PreparedStatements like this:

List<PreparedStatement> preparedStatements = connectionProvider.getPreparedStatements();
assertEquals(1, preparedStatements.size());
preparedStatement = preparedStatements.get( 0 );

verify(preparedStatement, times( 5 )).addBatch();
verify(preparedStatement, times( 2 )).executeBatch();

Back to top