2

I am developing a Spring Boot web (REST) application where I need to serve many requests. Therefore I wanted my application to be able to handle requests concurrently. Since Spring Boot REST-Services are out-of-the-box concurrently usable, I only need to make the (PostgreSQL) database access concurrently accessible. For that I am using the HikariCP data source.

Since a lot of my statements are prepared statements, I collected them in one method where I call pstmt = connection.prepareStatement("SQLCODE"); once for every statemment. Those prepared statements are then used in various methods when user interaction from the REST service is processed.

Now, when I use the HikariCP I can't do that anymore, can I? When I prepare a statement, this statement is bound to one connection. If I then try to access it concurrently, I can't because the connection is not shared.

Am I missing something? How can I solve this? Do I need to retrieve a connection from the pool, prepare the statement locally, execute my query, and close the connection? If so, what's the point of using a prepared statement then (other than preventing SQL injection)?

I know that the statements are cached by on the PostreSQL side. So would it be a good idea to keep the method where all prepared statements are prepared? To sent them to the database cache. And then just creating locally the same statements again. That way, one might still leverage the caching possibilities of the database. But on the other hand it would be really ugly code.

Im am using Spring: 5.3.10, Java: 11, PostgreSQL: 14.0

@RestController
public class RESTController {
    
    /** The database controller. */
    private DBController dbc;
    
    /** The data source object serving as a connection pool. */
    private HikariDataSource ds;
    
    /** The logger object for this class. */
    private static Logger logger = LoggerFactory.getLogger(RESTController.class);
    
    public RESTController(DBController dbc, Config config) {
        this.dbc = dbc;

        // Create the database
        if (!this.dbc.createDB(config)) {
            logger.error("Couldn't create the database. The service will now exit.");
            Runtime.getRuntime().halt(1);
        }
        
        // Create a connection pool
        ds = new HikariDataSource();
        ds.setJdbcUrl(config.getUrl());
        ds.setUsername(config.getUser());
        ds.setPassword(config.getPassword());
        ds.addDataSourceProperty("cachePrepStmts", "true");
        ds.addDataSourceProperty("prepStmtCacheSize", "250");
        ds.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        
        // Create the necessary tables
        if (!this.dbc.createTables(ds)) {
            logger.error("Couldn't create the tables. The service will now exit.");
            ds.close();
            Runtime.getRuntime().halt(1);
        }
        
        // Prepare SQL statements
        if (!this.dbc.prepareStatements(ds)) {
            logger.error("Couldn't prepare the SQL statements. The service will now exit.");
            ds.close();
            Runtime.getRuntime().halt(1);
        }
    }
    
    @PostMapping("/ID")
    public ResponseEntity<String> createNewDomain(@RequestParam(name = "name", required = true) String name) {
        // Do stuff ...
    }

    // [...]
}
@Component
public class DBController {

    /** The logger object for this class. */
    private static Logger logger = LoggerFactory.getLogger(DBController.class);
    
    // Prepared Statements
    private PreparedStatement stmt1, stmt2, stmt3;

    public boolean prepareStatements(HikariDataSource ds) {
        try {
            // Get connection from the pool
            Connection c = ds.getConnection();
            
            // Prepare all the statements
            stmt1 = c.prepareStatement("SQLCODE");
            stmt2 = c.prepareStatement("SQLCODE1");
            stmt2 = c.prepareStatement("SQLCODE1");
            // [...]
            
        } catch (SQLException e) {
            logger.debug("Could not prepare the SQL statements: " + e.getMessage());
            return false;
        }
        
        logger.debug("Successfully prepared the SQL statements.");
        return true;
    }

    public boolean m1(int i) {
        stmt1.setInt(i);
        ResultSet rs = stmt1.executeQuery();
    }

    public boolean m2(int j) {
        stmt1.setInt(j);
        ResultSet rs = stmt1.executeQuery();
    }

    public boolean m3(String a) {
        stmt2.setString(a);
        ResultSet rs = stmt2.executeQuery();
    }

    // [...]

}

Thanks in advance.

A.M.
  • 69
  • 1
  • 7
  • 1
    "Since a lot of my statements are prepared statements". All of them should be `PreparedStatements` really, unless you have `CallableStatements`. Multiple threads can't share the `PreparedStatements` like that, so your approach isn't very useful unless you're in a single-threaded environment. I'd advise trying to avoid the "I think I'm optimizing for performance" route, and do it the standard way everyone else is doing (i.e. prepare, set parameters, execute). – Kayaman Jan 11 '22 at 13:00
  • @Kayaman Some of my statements are not `PreparedStatements` but "normal" `Statements`, since they're only executed once and don't contain variables (e.g. creating the database). Is there really no way to use one of the initial properties of PreparedStatements then? – A.M. Jan 11 '22 at 13:08
  • 1
    Well, you might want to consider not having your application create the database. That kind of infrastructure setup is nicer to have done by other tools. The most important thing of a `PreparedStatement` is to avoid SQL injection and make things easier to use, but it does have some [performance benefits](https://stackoverflow.com/a/32300222/2541560). – Kayaman Jan 11 '22 at 13:19
  • Your design is incredibly awkward. Not only do you have a complicated `DBController` class that does unreadable things, but you're not using the tools that Spring is giving you. If you need low level access to the database, you could use `JdbcTemplate`, if you don't need low level access, you could use e.g. `spring-data-jdbc`. Your DB class is a design flaw, not a performance improvement. – Kayaman Jan 11 '22 at 13:27
  • `Runtime.getRuntime().halt(1);` where did you get the idea to write this kind of code? Spring has great tutorials, I suggest you follow them. – Kayaman Jan 11 '22 at 13:30
  • I have some constraints in my design choices. That's why the application is constructed this way. The `Runtime.getRuntime().halt(1);` part was suggested on SO. I can't use `System.exit(1);` since it simply does not work. – A.M. Jan 11 '22 at 13:53
  • 1
    You shouldn't use `System.exit(1);` either. I hope those constraints are real and not imaginary, otherwise you're forcing a bad design on yourself and suffering the consequences. – Kayaman Jan 11 '22 at 15:07
  • Good question. @Kayaman. I don't understand your point why prepared statement can't be used by multiple threads. I would imagine this is an immutable object in server and a driver. The bound should be related to the fact that server should clean up all objects related to a connection after it closes. – feech Nov 30 '22 at 02:51

1 Answers1

1

pleae read the part Statement Cache at https://github.com/brettwooldridge/HikariCP

Many connection pools, including Apache DBCP, Vibur, c3p0 and others offer PreparedStatement caching. HikariCP does not. Why?

So it does not cache. and if you read explanaition maybe you decide you don't need it too.

feech
  • 404
  • 4
  • 15