mug icon indicating copy to clipboard operation
mug copied to clipboard

`mug-safesql` - `PreparedStatement.addBatch()` support?

Open facboy opened this issue 1 month ago • 12 comments

is there support (or a suggestion for how to implement) JDBC batching with SafeSql?

facboy avatar Dec 04 '25 11:12 facboy

That's an intruiguing question!

For the moment I can't think of a good API to directly integrate batching. But for now, at least you can still use the SQL template to create dynamic SQL for batching, and then use the JDBC API directly to send batches:

// Create SafeSql, with the right identifier and subqueries, but with dummy jdbc parameters
SafeSql  sql = SafeSql.of("select * from `{table}` where id = {id}", table, 0);
try (PreparedStatement stmt = sql.prepareStatement(conn)) {
  for (id : ids) {
    stmt.setInt(1, id);
    stmt.addBatch();
  }
  stmt.executeBatch();
}

It's not as compile-time safe as single statements though.

fluentfuture avatar Dec 05 '25 05:12 fluentfuture

you already have:

  public static Template<Integer> prepareToUpdate(
      Connection connection, @CompileTimeConstant String template) {
    return prepare(connection, template, PreparedStatement::executeUpdate);
  }

It could use PreparedStatement::addBatch instead and then perhaps return a sub-interface of Template that would allow 'executing' the batch at some point?

facboy avatar Dec 05 '25 13:12 facboy

Good call!

I've added prepareToBatch(). It returns a Template<PreparedStatement> upon each call. Usage pattern will be like:

Template<PreparedStatement> insertUser = sql.prepareToBatch(connection, ...);
Set<PreparedStatement> batches = new HashSet<>();
for (...) {
  batches.add(insertUser.with(...));
}
for (var batch : batches) {
  batch.executeBatch();
}

You need a Set in case there are unequal identifier or subquery parameters passed to with(). If you only have JDBC parameters, the same PreparedStatement instance will be returned from with(). But using a Set is probably safest and easy.

fluentfuture avatar Dec 05 '25 16:12 fluentfuture

You can use prepareToBatch() in release 9.5

fluentfuture avatar Dec 09 '25 03:12 fluentfuture

cheers. btw poking around in the source, should the Template be closing the cached PreparedStatements?

facboy avatar Dec 09 '25 21:12 facboy

The Template interface isn't an AutoCloseable.

The hope is that the caller just calls Connection.close() in a try-with-resources and all the statements will be closed.

Will that pattern work for your use case?

fluentfuture avatar Dec 09 '25 22:12 fluentfuture

yeah, it would. just seems a bit...untidy.

facboy avatar Dec 10 '25 00:12 facboy

For batching, you could still perform a tidy batch with:

void insertUsers(Connection connection, List<User> users) throws SQLException {
  checkArgument(users.size() > 0);
  var insertUser = SafeSql.prepareToBatch(connection, ...);
  try (PreparedStatement batch =
      users.stream().map(u -> insertUser.with(...)).distinct().collect(toOnlyElement())) {
    batch.executeBatch();
  }
}

toOnlyElement() is from Guava if you know it's only a single batch, as is usually the case.

This way SafeSql remains least intrusive, dealing only with the SQL templating stuff; and you still interact with the JDBC API for executing batch, and for closing the JDBC resources. No need to learn yet another third-party API that merely delegates to JDBC. Every API should pull its weight.

fluentfuture avatar Dec 10 '25 05:12 fluentfuture

yeah, that's what i'm doing at the moment. i guess the only other thing i'd point out is that with:

  PreparedStatement stmt = cached.computeIfAbsent(sql.toString(), s -> {
    <...>
  });
  try {
    return action.apply(sql.setParameters(stmt));
  } catch (SQLException e) {
    throw new UncheckedSqlException(e);
  }

if something gets thrown from the try block then the PreparedStatement ends up being 'orphaned'. though i guess that's a problem for the existing prepareToUupdate etc too.

facboy avatar Dec 10 '25 07:12 facboy

This isn't using SafeSql, right?

Imho, if you will be closing the connection soon-ish (like for the current request), there is no need to worry about unclosed temporary statements upon exceptions. They don't happen often and statements are closed along with the connection anyways.

Speculatively, if closing a statement could involve interacting with the DB server, it might even be more efficient to close them in connection.close() because it'd be a batch close operation.

fluentfuture avatar Dec 10 '25 15:12 fluentfuture

no, that's code from within prepare in SafeSql: https://github.com/google/mug/blob/79f2f9d22bcde5f27e513348a7128d05ce22e328/mug-safesql/src/main/java/com/google/mu/safesql/SafeSql.java#L1875

facboy avatar Dec 11 '25 09:12 facboy

Oh I see.

Yeah, as suggested in the javadoc, it's expected that the caller manages these jdbc resources through the connection.

This is in compliance with JDBC spec:

Releases this Connection object's database and JDBC resources immediately... All Statement objects created by this Connection object are also closed.

It's for use cases where you open a connection -> do a bunch of things -> close it.

Note that the pattern works even with connection pools, as all spec-compliant connection pools must close the open statements and result sets when Connection.close() is called before returning the connection to the pool.

fluentfuture avatar Dec 11 '25 17:12 fluentfuture