Statement caching with Spring Boot and Oracle database

Karol Dowbecki · December 8, 2019

This post is a continuation of this Stack Overflow question where the author attempted to incorrectly setup statement caching. The overall idea is well described in chapter 20. Statement and Result Set Caching documentation however the example uses OracleDataSource.setImplicitCachingEnabled(true) which is a proprietary API.

Luckily, the same behaviour can be achieved just by using Spring Boot properties. We will need the following to prove this:

Database server setup

We need an Oracle database server. Since we don’t need any special configuration we can use Oracle Database 12c Enterprise Edition docker image. This can be done with:

docker run -d -it --name oracle12c -p 1521:1521 store/oracle/database-enterprise:12.2.0.1

Once the server is up we can create new user for our Spring Boot application:

ALTER SESSION SET "_ORACLE_SCRIPT" = true;
CREATE USER test1 IDENTIFIED BY test1;
GRANT ALL PRIVILEGES TO test1;
GRANT UNLIMITED TABLESPACE TO test1;
ALTER USER test1 quota unlimited ON users;

Now having created test1 user we can create a fruit table with two columns and few example rows:

CREATE TABLE fruit (
	id INTEGER PRIMARY KEY,
	name VARCHAR2(200)
);
INSERT INTO fruit (id, name) VALUES (1, 'Apple');
INSERT INTO fruit (id, name) VALUES (2, 'Orange');
INSERT INTO fruit (id, name) VALUES (3, 'Strawberry');
INSERT INTO fruit (id, name) VALUES (4, 'Banana');
INSERT INTO fruit (id, name) VALUES (5, 'Blackberry');
INSERT INTO fruit (id, name) VALUES (6, 'Papaya');
INSERT INTO fruit (id, name) VALUES (7, 'Cherry');
INSERT INTO fruit (id, name) VALUES (8, 'Tomato');
INSERT INTO fruit (id, name) VALUES (9, 'Cucumber');
INSERT INTO fruit (id, name) VALUES (10, 'Avocado');

Application setup

To quickly generate a new Spring boot application we can use Spring Initializer. It’s handy to select additional spring-boot-starter-data-jdbc dependency to get basic JDBC boilerplate.

Next, ojdbc8.jar driver must be downloaded to match the database server version. This can be done from Oracle Database 12.2.0.1 JDBC Driver & UCP Downloads website. Once we have the JAR we can to add it as a runtime dependency.

To execute new SQL query we can create a bean with @Scheduled method. Every 100ms a connection will be obtained from the pool, and the same SQL query will be executed using PreparedStatement. The actual SQL query used makes no difference to us, we just need something running on the connection to see that it’s cached:

@Service
public class QueryService {
  private static final Logger LOG = LoggerFactory.getLogger(QueryService.class);
  private static final String SQL = "SELECT id, name FROM fruit WHERE lower(name) LIKE ? ORDER BY name";

  @Autowired
  private DataSource dataSource;

  @Scheduled(fixedRate = 100)
  public void runQuery() throws Exception {
    try (Connection conn = dataSource.getConnection()) {
      try (PreparedStatement stmt = conn.prepareStatement(SQL)) {
        stmt.setString(1, "%a%");
        try (ResultSet rs = stmt.executeQuery()) {
          StringJoiner joiner = new StringJoiner(" ");
          while (rs.next()) {
            joiner.add(rs.getInt("id") + "=" + rs.getString("name"));
          }
          LOG.info(joiner.toString());
        }
      }
    }
  }
}

If we configure the task scheduler with Executors.newScheduledThreadPool(10) we see that QueryService.runQuery() method is executed using multiple threads, so far so good:

2019-12-08 21:52:48.323  INFO 28312 --- [pool-1-thread-1] io.github.kdowbecki.QueryService         : 1=Apple 10=Avocado 4=Banana 5=Blackberry 2=Orange 6=Papaya 3=Strawberry 8=Tomato
2019-12-08 21:52:48.354  INFO 28312 --- [pool-1-thread-3] io.github.kdowbecki.QueryService         : 1=Apple 10=Avocado 4=Banana 5=Blackberry 2=Orange 6=Papaya 3=Strawberry 8=Tomato
2019-12-08 21:52:48.454  INFO 28312 --- [pool-1-thread-2] io.github.kdowbecki.QueryService         : 1=Apple 10=Avocado 4=Banana 5=Blackberry 2=Orange 6=Papaya 3=Strawberry 8=Tomato
2019-12-08 21:52:48.554  INFO 28312 --- [pool-1-thread-4] io.github.kdowbecki.QueryService         : 1=Apple 10=Avocado 4=Banana 5=Blackberry 2=Orange 6=Papaya 3=Strawberry 8=Tomato

There is no statement caching

To observe this we should run the application and query the V$SQLAREA view which provides “statistics on SQL statements that are in memory, parsed, and ready for execution”. We can focus on just two columns: PARSE_CALLS and EXECUTIONS:

SELECT parse_calls, executions
FROM v$sqlarea 
WHERE sql_text LIKE 'SELECT id, name FROM fruit%'

Which will return:

89	89

These two numbers are constantly increasing with time. They will always have the same value which indicates that each time PreparedStatement.executeQuery() is run the SQL query is parsed.

Enabling statement caching in the driver

By design HikariCP doesn’t implement a statement cache as explained by the author. This functionality must be provided by the JDBC driver if we want to use it in our application. In our case ojdbc8.jar provides a configuration oracle.jdbc.implicitStatementCacheSize property which defines the size of the internal LRU statement cache. By default, the value is 0, we need to overwrite it to enable caching.

This can be done with application.yml property, if we want to cache 100 statements:

spring:
  datasource:
    hikari:
      data-source-properties:
        oracle.jdbc.implicitStatementCacheSize: 100

Let’s restart the database server and compare PARSE_CALLS with EXECUTIONS values for our SQL query again. This time we see:

1	59

Only the EXECUTIONS value is increasing with time while PARSE_CALLS doesn’t change. We have a working statement cache!

Resources

Twitter, Facebook