回答
Mybatis 的 Mapper 支持四种参数传递方式。
- 单个参数:当 Mapper 方法只有一个参数时,SQL 语句中支持使用 #{} 占位符来引用该参数。
public interface UserMapper {
User selectUserById(Integer id);
}
<select id="selectUserById" parameterType="java.lang.Integer" resultMap="User">
SELECT id, name, email FROM t_user WHERE id = #{id}
</select>
- **多个参数:**当 Mapper 方法有多个参数时,使用 @Param 注解为每个参数指定名称。MyBatis 会将这些参数放入 Map 中,键为参数名称。
public interface UserMapper {
User selectUserByNameAndEmail(@Param("name") String name, @Param("email") String email);
}
<select id="selectUserByNameAndEmail" parameterType="java.util.HashMap" resultMap="User">
SELECT id, name, email FROM t_user WHERE name = #{name} AND email = #{email}
</select>
如上,name 和 email 参数会被放入一个 Map 中,键为 name 和 email,值为对应的参数值。然后在 SQL 语句中通过 #{name} 和 #{email} 引用。
- **对象参数:**当 Mapper 方法的参数是一个对象时,可在 SQL 语句中使用对象的属性名作为参数占位符。
public class UserQuery {
private String name;
private String email;
// getters and setters
}
public interface UserMapper {
User getUserByQuery(UserQuery query);
}
<select id="getUserByQuery" parameterType="UserQuery" resultType="User">
SELECT id, name, email FROM t_user WHERE name = #{name} AND email = #{email}
</select>
如上,UserQuery 对象的 name 和 email 属性会被传递给 SQL 语句中的 #{name} 和 #{email} 占位符。
- **Map 参数:**当 Mapper 方法的参数是一个 Map 时,可以在 SQL 语句中通过 Map 的键来引用参数。
public interface UserMapper {
User getUserByMap(Map<String, Object> params);
}
<select id="getUserByMap" parameterType="java.util.HashMap" resultType="User">
SELECT id, name, email FROM t_user WHERE name = #{name} AND email = #{email}
</select>
如上,Map 中的键 name 和 email 会被传递给 SQL 语句中的 #{name} 和 #{email} 占位符。
扩展
Mybatis 参数传递
MyBatis 在处理 Mapper 参数时,会将参数封装成一个 ParamMap 对象,并在 SQL 语句解析时进行替换。
- **单个参数:**直接封装成 ParamMap,键为 "param1"。
- 多个参数:使用 @Param 注解时,参数会被封装成 ParamMap,键为注解指定的名称。
- 对象参数:对象参数的属性会被解析并封装到 ParamMap 中。
- Map 参数:直接使用传入的 Map。
Mybatis 参数绑定
- 解析参数名:通过 ParamNameResolver 解析 Mapper 方法的参数名并封装成 ParamMap。
Mybatis 通过 ParamNameResolver 遍历方法参数,根据 @Param 注解和参数位置生成参数名。解析后的参数会存储在一个 Map 中,用于执行 SQL 语句时正确替换参数占位符。
public class ParamNameResolver {
public static final String GENERIC_NAME_PREFIX = "param";
private final boolean useActualParamName;
private final SortedMap<Integer, String> names;
private boolean hasParamAnnotation;
public ParamNameResolver(Configuration config, Method method) {
this.useActualParamName = config.isUseActualParamName();
// 获取参数类型和注解
final Class<?>[] paramTypes = method.getParameterTypes();
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
// 初始化 SortedMap 存储参数的位置和名称。
final SortedMap<Integer, String> map = new TreeMap<>();
int paramCount = paramAnnotations.length;
// get names from @Param annotations
// 参数遍历
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
if (isSpecialParameter(paramTypes[paramIndex])) {
// skip special parameters
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
if (name == null) {
// @Param was not specified.
if (useActualParamName) {
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// use the parameter index as the name ("0", "1", ...)
// gcode issue #71
name = String.valueOf(map.size());
}
}
//
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) { // 如果参数为空或数量为 0,返回 null。
return null;
} else if (!hasParamAnnotation && paramCount == 1) { // 单个参数无注解@Param
Object value = args[names.firstKey()];
return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
} else {
// 多个参数, 创建 ParamMap 来存储参数名和参数值
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
// 省略其他代码……
}
- 生成 BoundSql:执行 SQL 语句前,通过 BoundSql 封装 SQL 语句和参数信息。
Mybatis 通过 DynamicSqlSource 和 SqlSourceBuilder 解析动态 SQL,生成最终的 SQL 语句和参数信息。其中,StaticSqlSource 生成包含 SQL 和参数信息的 BoundSql 对象。
解析占位符:MyBatis 首先通过 MappedStatement 获取 SQL 语句,并解析语句中的参数占位符。
生成 SqlSource:SqlSourceBuilder 解析 SQL 语句的参数占位符,并生成一个新的 SqlSource 对象。
public class SqlSourceBuilder extends BaseBuilder {
private static final String PARAMETER_PROPERTIES = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";
public SqlSourceBuilder(Configuration configuration) {
super(configuration);
}
/**
*
* 参数占位符解析
**/
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// 将参数占位符 #{} 替换为 ?,
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
// 省略其他代码……
}
- 生成 BoundSql:利用上述生成的 StaticSqlSource 生成 BoundSql 对象,其中包含了最终的 SQL 语句和参数信息。
public class StaticSqlSource implements SqlSource {
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Configuration configuration;
public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
this.configuration = configuration;
this.sql = sql;
this.parameterMappings = parameterMappings;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 新建 BoundSql
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
}
备注:对于动态 SQL,MyBatis 使用 DynamicSqlSource 来处理。它会解析动态 SQL 节点并生成最终的 SQL 语句。
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
}
- 绑定参数:通过 ParameterHandler 将参数值设置到 PreparedStatement 中。
public class DefaultParameterHandler implements ParameterHandler {
private final TypeHandlerRegistry typeHandlerRegistry;
private final MappedStatement mappedStatement;
private final Object parameterObject;
private final BoundSql boundSql;
private final Configuration configuration;
public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
this.mappedStatement = mappedStatement;
this.configuration = mappedStatement.getConfiguration();
this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
this.parameterObject = parameterObject;
this.boundSql = boundSql;
}
@Override
public Object getParameterObject() {
return parameterObject;
}
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
//
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
// 遍历 BoundSql 中的 ParameterMapping 列表,
// 将每个参数的值设置到 PreparedStatement 中。
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
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);
}
}
}
}
}
}
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。