2024-12-22  阅读(67)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/2021601129

回答

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>

如上,nameemail 参数会被放入一个 Map 中,键为 nameemail,值为对应的参数值。然后在 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 对象的 nameemail 属性会被传递给 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 中的键 nameemail 会被传递给 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 通过 DynamicSqlSourceSqlSourceBuilder 解析动态 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] ,回复【面试题】 即可免费领取。

阅读全文