The #{} parameter in mybatis is the most commonly used feature. The #{} parameter in Mybatis is eventually treated as a compilation parameter, that is, it is replaced with ‘? ‘, and then set the parameter values using the setXXX method in PreparedStatement, so using the #{} parameter has no RISK of SQL injection.
Let’s briefly review the use of JDBC precompiled statements:
// Load the driver class.forname (driver); / / get the db Connection Connection Connection = DriverManager. GetConnection (url, user, password); / / create precompiled statements PreparedStatement ps = connection. The prepareStatement (" select the name from the user where id =?" ); // Set the precompiled statement parameter value ps.setint (1, 1); // Execute ResultSet ResultSet = ps.executeQuery(); While (resultSet.next()) {system.out.println ("name = "+ resultSet.getString("name")); }Copy the code
When using JDBC precompilation, we first specify the argument in the statement as’? ‘, using the setXXX method to specify a value for the parameter before execution. Mybatis does the same with #{} instead of? Then #{} specifies the type information to select the corresponding set method for parameter setting.
To recap the use of #{} in mybatis, the following example queries all information about a user based on the user id from a simple user table. Mapper configuration:
<mapper namespace="cn.**.UserMapper">
<select id="queryUserById" resultType="Map">
select * from user
<where>
<if test="id ! = null">
id = #{value, javaType=int, jdbcType=NUMERIC}
</if>
</where>
limit 1
</select>
</mapper
Copy the code
Execute query:
List<? > datas = sqlSession.selectList("queryUserById".1);
Copy the code
When we execute queryUserById, Mybatis will replace #{value} with? And pass 1 to the ID parameter through setInt.
Parse #{} parameters
The previous articles described how Mybatis generated executable SQL and did ${} string substitution, and after completing these two steps, you end up with a SQL that only has #{} parameters to parse. #{} parameters are parsed by SqlSourceBuilder, which replaces #{} with ‘? ‘and parse the contents of #{} into a wrapper for ParameterMapping, which contains the attributes of the parameter. In addition, SqlSourceBuilder generates a StaticSqlSource for the statement, which represents dynamic internal SQL with no content, that is, executable SQL.
// src/main/java/cn/ly/ibatis/builder/SqlSourceBuilder.java
public class SqlSourceBuilder extends BaseBuilder {
public SqlSource parse(String originalSql, Class
parameterType, Map
additionalParameters)
,> {
/ / ParameterMappingTokenHandler is responsible for parsing the contents of the each # {}
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("# {"."}", handler);
String sql = parser.parse(originalSql);
// Generate the StaticSqlSource for the statement
return newStaticSqlSource(configuration, sql, handler.getParameterMappings()); }}Copy the code
1.1 Parameter Description
SqlSourceBuilder takes three parameters when parsing:
- originalSql
SQL to parse that has already parsed a ${} string reference
- parameterType
ParameterType Specifies the type of the configuration item if it is a static statement (excluding ${} and dynamic tags).
<select id="queryUserById" resultType="Map" parameterType="cn.***.Test">.</select>
Copy the code
SqlSourceBuilder parameterType is set to cn.***.Test. If parameterType is not specified, Object.
ParameterType specifies the type of the parameter specified when executing SQL with sqlSession.
sqlSession.selectList("queryUserById".1);
Copy the code
For this query, parameterType in SqlSourceBuilder is integer.class.
- additionalParameters
(1) If the statement is a static statement (without ${} and dynamic tags), the value of additionalParameters is an empty HashMap. (2) if the statement is a non-static statement, then the value of this parameter is the OGNL context of the parameter specified when SQL is executed using sqlSession. This context map contains the two fixed attributes _parameter and _databaseId, as well as the attributes added using BIND.
public class Test {
private int id = 1;
private String name = "zhangsan";
}
<select id="queryUserById" resultType="Map" parameterType="cn.***.test.mybatis.Test">
<bind name="userName" value="name" />
select * from user where id = #{id} and name =#{userName} limit 1
</select>
Copy the code
AdditionalParameters value for this configuration is:
1.2 Analysis Process
ParameterMapping contains the configuration items supported by #{}. ParameterMapping is a ParameterMapping object for each parameter.
// src/main/java/org/apache/ibatis/mapping/ParameterMapping.java
public class ParameterMapping {
/ / property
private String property;
// OUT or INOUT will modify the attribute value of the parameter object
private ParameterMode mode;
// The type of the attribute
privateClass<? > javaType = Object.class;// The jdbcType of the property
private JdbcType jdbcType;
// Keep the number of decimal places
private Integer numericScale;
// typeHandler for the parameter type, if not specified, obtained from typeHandlerRegistry via jdbcType and javaType
privateTypeHandler<? > typeHandler;// Indicates the ID of the corresponding resultMap
private String resultMapId;
/ / jdbcType name
private String jdbcTypeName;
}
Copy the code
Parsing means that all attributes except typeHandler and javaType are fetched directly from the configuration and set, or null if not specified.
// src/main/java/org/apache/ibatis/mapping/ParameterMapping.java
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
private ParameterMapping buildParameterMapping(String content) {
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
} else if ("property".equals(name)) {
// Do Nothing
} else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are "+ PARAMETER_PROPERTIES); }}}}Copy the code
TypeHandler and javaType in ParameterMapping are required and important because typeHandler determines which set method to use in PreparedStatement when setting parameter values. TypeHandler is obtained via javaType. Parse javaType as follows:
- AdditionalParameters has an attribute. If so, get the type of the set method for the attribute in additionalParameters.
- If parameterType 1 does not exist, check whether parameterType has TypeHandler for that type. Mybatis comes with some basic TypeHandler by default
- JavaType = javaType = javaType = javaType = javaType = javaType = javaType = java.sql
- 3 If no, check whether the attribute is specified or parameterType is of Map type. If yes, the type is Object.class
- 4 If no, check whether parameterType has a GET method for the specified type. If yes, obtain the return type of the GET method
- If none of the above exists, the Java type for the attribute is object.class
- If a javaType attribute is specified, the value is taken directly.
If typeHandler is specified, the specified type is directly obtained. If typeHandler is not specified, javaType is obtained to look up typeHandler of the type.
// src/main/java/org/apache/ibatis/mapping/ParameterMapping.java
private ParameterMapping buildParameterMapping(String content) {
private ParameterMapping buildParameterMapping(String content) {
Map<String, String> propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get("property");
// Get the type of the attributeClass<? > propertyType;if (metaParameters.hasGetter(property)) {
propertyType = metaParameters.getSetterType(property);
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)){
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
propertyType = Object.class;
} else {
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
if (metaClass.hasGetter(property)) {
propertyType = metaClass.getGetterType(property);
} else{ propertyType = Object.class; }}...returnbuilder.build(); }}Copy the code
At this point all the #{} parameters in SQL are resolved to ParameterMapping and replaced with ‘? ‘, then encapsulate the SQL replaced with #{} and all ParameterMapping into the StaticSqlSource, which provides subsequent support.
// src/main/java/org/apache/ibatis/builder/StaticSqlSource.java
public class StaticSqlSource implements SqlSource {
// Parsed SQL
private final String sql;
// all #{} arguments in SQL
private final List<ParameterMapping> parameterMappings;
private final Configuration configuration;
}
Copy the code
When do #{} parse
What triggers the parsing of #{}? #{} parsing time is divided into two cases, static statements and dynamic statements. When SqlSessionFactoryBuilder().build() initializes mybatis, all configured statements will be parsed as DynamicSqlSource and static statements as RawSqlSource. Both of these are SQL statements that the SqlSource represents, and ultimately the #{} parameters are parsed with SqlSourceBuilder and the StaticSqlSource is generated.
For RawSqlSource, because it contains no dynamic content, the final SQL can be obtained during the parsing phase. Therefore, the #{} parameter can be parsed initially and the StaticSqlSource can be generated, which is faster than DynamicSqlSource.
DynamicSqlSource contains dynamic content, so the SQL to be executed can only be known at execution time, i.e., which #{} will be used at execution time.
3. Set parameter values
Mybatis has three types of statements: STATEMENT, PREPARED and CALLABLE, which correspond to JDBC STATEMENT, PreparedStatement or CallableStatement. There are three types of these three types in Mybatis with corresponding StatementHandler for special processing. SimpleStatementHandler, PreparedStatementHandler, and CallableStatementHandler, respectively. Parameterize is a menu interface for processing and compiling parameters defined in StatementHandler. Parameterize is called to set parameter values before statement execution.
Public interface StatementHandler {// Set precompiled parameters void parameterize(Statement Statement) throws SQLException; }Copy the code
The SimpleStatementHandler pair is implemented as an empty method, so if the statement contains the #{} parameter, set the statement type to PREPARED or CALLABLE. In StatementHandler, use ParameterHandler to set the parameters. Set the setParameters method on the ParameterHandler interface. It is DefaultParameterHandler for xmL-configured statements.
ParameterMapping is used to set the value of the parameterHandler parameter. The value of the parameterHandler parameter is set using the setParameter Settings of the typeHandler. For example, StringTypeHandler for String uses the setString method:
// src/main/java/org/apache/ibatis/type/StringTypeHandler.java
public class StringTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException { ps.setString(i, parameter); }}Copy the code
DefaultParameterHandler iterates through all ParameterMapping in sequence and sets them one by one. TypeHandler (bind); TypeHandler (bind); TypeHandler (bind); TypeHandler (bind) If it does not, the task’s current property is one of the passed parameters and calls the get method to get it from the passed parameters.
public class DefaultParameterHandler implements ParameterHandler { public void setParameters(PreparedStatement ps) { ... for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() == ParameterMode.OUT) { continue; } Object value; String propertyName = parameterMapping.getProperty(); If (boundSql hasAdditionalParameter (propertyName)) {/ / from the extra first gets an attribute value specified in the parameters value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry hasTypeHandler (parameterObject getClass ())) {/ / a ypeHandler this property directly as parameterObject value = parameterObject; } else {/ / from the get method to obtain MetaObject MetaObject = configuration. NewMetaObject (parameterObject); value = metaObject.getValue(propertyName); } / / / retrieve attributes typeHandler typeHandler typeHandler = parameterMapping. GetTypeHandler (); JdbcType jdbcType = parameterMapping.getJdbcType(); try { typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (TypeException | SQLException e) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); }}}}Copy the code
At this point, the #{} parameter in the statement is parsed and set, and an executable statement is obtained, which is then executed and parsed.