Parameter Binding Strategies in MyBatis: Security and Performance Implications
MyBatis provides two distinct syntaxes for injecting dynamic values into SQL statements. Consider retrieving records from an employee table based on an email address:
SELECT * FROM employee WHERE email = 'alice@example.com';
To make this query dynamic, MyBatis offers two approaches:
SELECT * FROM employee WHERE email = #{email};
Or alternatively:
SELECT * FROM employee WHERE email = ${email};
While both versions produce identical results for simple queries, their underlying mechanisms differ significantly, leading to distinct security and performance characteristics.
Placeholder Resolution Mechanisms
MyBatis performs dynamic SQL resolution before statement execution, converting XML-mapped statements into BoundSql objects. During this phase, #{} and ${} undergo different transformations.
Prepared Statement Parameters
The #{} syntax indicates a JDBC prepared statement parameter marker. When MyBatis encounters:
SELECT * FROM employee WHERE email = #{email};
It trensforms this into a parameterized query:
SELECT * FROM employee WHERE email = ?;
The actual value binding occurs within the database management system (DBMS), not during SQL construction.
Literal String Interpolation
Conversely, ${} triggers direct string substitution during the dynamic SQL parsing phase. Given:
SELECT * FROM employee WHERE email = ${email};
With an input value of 'alice@example.com', the resulting SQL becomes:
SELECT * FROM employee WHERE email = 'alice@example.com';
By the time this reaches the precompilation stage, no variable reference remains—it has been replaced with the literal text.
Security and Performance Considerations
Prefer Parameterized Queries
Always favor #{} when possible. This approach leverages prepared statement caching, allowing the DBMS to reuse execution plans for identical query structures with different parameters.
More critical, ${} enables SQL injection vulnerabilities. Consider this dynamic table selection:
SELECT * FROM ${tableName} WHERE status = #{status};
If tableName receives malicious input such as employee; TRUNCATE employee; --, the resolved SQL becomes:
SELECT * FROM employee; TRUNCATE employee; -- WHERE status = ?;
The double-dash comments out the remaining clause, executing destructive commands.
Exception: Schema Objects
Database identifiers (tables, columns) require ${} because prepared statement placeholders automatically quote string values, causing syntax errors for object names. Using #{} for a table:
SELECT * FROM #{tableName} WHERE id = #{id};
Results in invalid SQL after parameter binding:
SELECT * FROM 'employee' WHERE id = 123;
Object names cannot appear as quoted string literals, necessitating literal substitution for dynamic schema referenecs.
SQL Precompilation Architecture
Precompilation occurs when the database driver transmits SQL to the DBMS before parameter binding. This process offers two primary advantages:
- Execution Plan Optimization: Complex quereis undergo parsing and optimization once, with subsequent executions reusing the compiled structure.
- Resource Efficiency:
PreparedStatementobjects remain in cache, eliminating redundant parsing overhead for recurring query patterns.
MyBatis enables precompilation by default for all mapped statements.
Driver Implementation Analysis
Examining the MySQL Connector/J implementation within ConnectionImpl reveals the precompilation workflow:
public synchronized PreparedStatement prepareStatement(String query,
int resultSetType, int resultSetConcurrency) throws SQLException {
checkClosed();
PreparedStatement statement = null;
boolean serverSideCapable = true;
// Normalize SQL syntax for target database
String normalizedQuery = getProcessEscapeCodesForPrepStmts()
? nativeSQL(query)
: query;
// Evaluate server-side precompilation support
if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) {
serverSideCapable = canHandleAsServerPreparedStatement(normalizedQuery);
}
if (this.useServerPreparedStmts && serverSideCapable) {
if (this.getCachePreparedStatements()) {
synchronized (this.serverSideStatementCache) {
// Attempt retrieval from statement cache
statement = (ServerPreparedStatement) this.serverSideStatementCache.remove(query);
if (statement != null) {
statement.setClosed(false);
statement.clearParameters();
}
if (statement == null) {
try {
// Instantiate new server-prepared statement
statement = ServerPreparedStatement.getInstance(
getLoadBalanceSafeProxy(),
normalizedQuery,
this.database,
resultSetType,
resultSetConcurrency
);
if (query.length() < getPreparedStatementCacheSqlLimit()) {
((ServerPreparedStatement) statement).isCached = true;
}
statement.setResultSetType(resultSetType);
statement.setResultSetConcurrency(resultSetConcurrency);
} catch (SQLException exception) {
if (getEmulateUnsupportedPstmts()) {
statement = (PreparedStatement) clientPrepareStatement(
normalizedQuery, resultSetType, resultSetConcurrency, false
);
if (query.length() < getPreparedStatementCacheSqlLimit()) {
this.serverSideStatementCheckCache.put(query, Boolean.FALSE);
}
} else {
throw exception;
}
}
}
}
} else {
// Direct server-side preparation without caching
try {
statement = ServerPreparedStatement.getInstance(
getLoadBalanceSafeProxy(),
normalizedQuery,
this.database,
resultSetType,
resultSetConcurrency
);
statement.setResultSetType(resultSetType);
statement.setResultSetConcurrency(resultSetConcurrency);
} catch (SQLException exception) {
if (getEmulateUnsupportedPstmts()) {
statement = (PreparedStatement) clientPrepareStatement(
normalizedQuery, resultSetType, resultSetConcurrency, false
);
} else {
throw exception;
}
}
}
} else {
// Fallback to client-side preparation
statement = (PreparedStatement) clientPrepareStatement(
normalizedQuery, resultSetType, resultSetConcurrency, false
);
}
return statement;
}
This implementation demonstrates the dual-path approach: server-side preparation when supported and cached, falling back to client-side emulation when necessary.
Dynamic Parsing Pipeline
Before JDBC precompilation occurs, MyBatis processes SQL through its dynamic language engine. This pipeline handles placeholder substitution, conditional logic, and type validation, transforming mapped XML into executable SQL with bound parameter metadata.