Safely Building SQL with Groovy GString Interpolation and the Sql Class
Groovy’s GString interpolation with the Sql class lets you write concise, readable queries that are automatically bound as parameters, preventing injection. Learn how it works, see a hands‑on example, and understand trade‑offs and best practices.
30 Jul 2026, 10:42 UTC

Concrete Problem: Mixing Concise Code and Security
When writing database access code in Groovy, developers often fall into the temptation of string concatenation:
def query = "SELECT * FROM users WHERE id = " + userId
sql.eachRow(query) { … }
This pattern is terse but opens the door to SQL injection if userId originates from user input. The question becomes: how can we keep the readability of GString interpolation while guaranteeing that values are safely bound?
Thesis: GString + Sql = Concise, Safe, Parameter‑Bound Queries
Groovy’s GString feature lets you embed expressions directly inside string literals. When such a GString is passed to groovy.sql.Sql, the class automatically transforms the string into a PreparedStatement and binds the embedded expressions as parameters. The result is code that looks like plain string interpolation but is executed safely, preventing injection attacks and reducing boilerplate.
Section 1 – How the Sql Class Parses GString
- Interpolation detection: The Sql class scans the GString for
${…}placeholders. Each placeholder becomes a positional parameter (e.g.,?in the underlying JDBC statement). - Parameter extraction: The expressions are evaluated immediately, and their values are collected in order.
- PreparedStatement creation: The resulting SQL string, with placeholders replaced by
?, is used to create a PreparedStatement via the JDBC driver. - Execution: The PreparedStatement is executed with the bound parameters, ensuring that values are treated as data, not executable SQL.
Because the binding happens inside the Sql class, the developer does not need to remember to call preparedStatement.setString() or similar; the class abstracts that detail away.
Section 2 – Hands‑On Example: Querying an H2 Database
Below is a minimal script that demonstrates the full workflow. Replace jdbcUrl, username, and password with your own credentials.
import groovy.sql.Sql
// 1. Connection parameters – adjust for your environment
String jdbcUrl = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"
String username = "sa"
String password = ""
// 2. Create the Sql instance – requires JDBC driver on the classpath
Sql sql = Sql.newInstance(jdbcUrl, username, password, "org.h2.Driver")
// 3. Setup a sample table (id is an integer primary key)
sql.execute("CREATE TABLE users(id INT PRIMARY KEY, name VARCHAR(50))")
sql.execute("INSERT INTO users(id, name) VALUES(1, 'Alice'), (2, 'Bob')")
// 4. Define a user‑supplied value
String userId = "1" // In a real app, this comes from request parameters
// 5. Build a GString query – note the ${userId} placeholder
String gstringQuery = "SELECT id, name FROM users WHERE id = ${userId}"
// 6. Execute with Sql.eachRow – the class binds userId as a parameter
sql.eachRow(gstringQuery) { row ->
println "Found user: ${row.id} – ${row.name}"
}
Running this script will print:
Found user: 1 – Alice
Behind the scenes, the Sql class has generated a PreparedStatement equivalent to:
SELECT id, name FROM users WHERE id = ?
and bound the value 1 to the placeholder. If userId contained malicious content such as "1 OR 1=1", the query would still be safe because the value is treated as a literal string, not as part of the SQL syntax.
Section 3 – Trade‑offs and Limitations
- Performance: For simple queries, the overhead of parsing a GString is negligible. In high‑throughput scenarios, repeatedly parsing large GStrings can add microseconds per call. Pre‑compiled PreparedStatements (e.g.,
sql.withConnection { conn -> conn.prepareStatement(sql) }) avoid this parsing cost. - Dynamic identifiers: GString interpolation works only for values. If you need to insert a table or column name dynamically, you cannot rely on parameter binding. Instead, whitelist allowed identifiers and concatenate them explicitly:
String table = "users"
if (!table.matches(/\w+/)) throw new IllegalArgumentException("Invalid table name")
String query = "SELECT * FROM ${table} WHERE id = ${userId}"
${…} are evaluated to Groovy objects. Numeric values become strings when bound, which is fine for most cases but can affect numeric comparison semantics if the database expects a numeric type. Explicit casting (e.g., userId as Integer) can mitigate surprises.Actionable Closing – Best Practices for Safe GString Queries
- Always use the
groovy.sql.Sqlclass (or its Spring JDBC equivalents) when executing GString queries. - Never concatenate a GString directly into a
Statement; that defeats parameter binding. - When dynamic identifiers are required, whitelist them before interpolation.
- Enable JDBC query logging (e.g.,
org.h2.Driverwith?TRACE_LEVEL_FILE=3) to verify that PreparedStatements are generated. - For performance‑critical paths, benchmark GString parsing versus pre‑compiled statements and refactor accordingly.
- Keep Groovy up to date to avoid known GString parsing bugs.
By following these guidelines, you can enjoy Groovy’s expressive GString syntax while maintaining the security guarantees of parameter‑bound SQL.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.