【MyBatis】学习纪要四:结合源码教你参数处理

引言

今天我们来说 MyBatis 接收参数这一块。我打算这样说给你听,我们先看一下MyBatis源码是如何处理参数的,然后我们通过例子来教你。

Parameters.png

实际上,我们这一节讲的就是:Mapper.xml 如何获取 Dao 中的参数呢?

另外,如果你还没有开始学习MyBatis,觉得MyBatis还不错,可以看 【MyBatis】学习纪要一:SpringBoot集成MyBatis完成增删查改 这篇教程,起步。

源码分析

  • 第一步:我们先找到源码。

我先将处理参数的类复制到下面,然后我们再一起来分析。

/**
 *    Copyright 2009-2017 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package org.apache.ibatis.reflection;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

public class ParamNameResolver {

  private static final String GENERIC_NAME_PREFIX = "param";

  /**
   * <p>
   * The key is the index and the value is the name of the parameter.<br />
   * The name is obtained from {@link Param} if specified. When {@link Param} is not specified,
   * the parameter index is used. Note that this index could be different from the actual index
   * when the method has special parameters (i.e. {@link RowBounds} or {@link ResultHandler}).
   * </p>
   * <ul>
   * <li>aMethod(@Param("M") int a, @Param("N") int b) -&gt; {{0, "M"}, {1, "N"}}</li>
   * <li>aMethod(int a, int b) -&gt; {{0, "0"}, {1, "1"}}</li>
   * <li>aMethod(int a, RowBounds rb, int b) -&gt; {{0, "0"}, {2, "1"}}</li>
   * </ul>
   */
  private final SortedMap<Integer, String> names;

  private boolean hasParamAnnotation;

  public ParamNameResolver(Configuration config, Method method) {
    final Class<?>[] paramTypes = method.getParameterTypes();
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
    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 (config.isUseActualParamName()) {
          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);
  }

  private String getActualParamName(Method method, int paramIndex) {
    if (Jdk.parameterExists) {
      return ParamNameUtil.getParamNames(method).get(paramIndex);
    }
    return null;
  }

  private static boolean isSpecialParameter(Class<?> clazz) {
    return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz);
  }

  /**
   * Returns parameter names referenced by SQL providers.
   */
  public String[] getNames() {
    return names.values().toArray(new String[0]);
  }

  /**
   * <p>
   * A single non-special parameter is returned without a name.<br />
   * Multiple parameters are named using the naming rule.<br />
   * In addition to the default names, this method also adds the generic names (param1, param2,
   * ...).
   * </p>
   */
  public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
      return args[names.firstKey()];
    } else {
      final Map<String, Object> param = new ParamMap<Object>();
      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 + String.valueOf(i + 1);
        // ensure not to overwrite parameter named with @Param
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }
}

  • 第二步:我们先来看构成方法。
public ParamNameResolver(Configuration config, Method method) {
    final Class<?>[] paramTypes = method.getParameterTypes();
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
    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 (config.isUseActualParamName()) {
          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);
  }

这部分代码还是挺好理解的,可以先大概看一篇,然后主要看注释的几处。

1、获取有 @Param 注解的值

2、每次解析一个参数,并且保存到map中(key:索引,value:name)
name:标注了Param注解:注解的值
没有标注:
1.全局配置:useActualParamName(jdk1.8):name=参数名
2.name=map.size():相当于当前元素的索引

例如:
method(@Param("id") id, @Param("name") name, Integer age);
之后的Map,大概就这样:{0=id, 1=name}

  • 第三步:我们具体分析
// args[1, "Tom", 20]

public Object getNamedParams(Object[] args) {
    int paramCount = this.names.size();
    
     // 参数为null,直接返回
    if (args != null && paramCount != 0) {

          // 如果只有一个元素,并且没有 `@Param` 注解,args[0],单个参数直接返回
        if (!this.hasParamAnnotation && paramCount == 1) {
            return args[(Integer)this.names.firstKey()];
         
           // 多个参数或者有 `@Param` 标注
        } else {
            Map<String, Object> param = new ParamMap();
            int i = 0;
               // 遍历 names 集合{0=id, 1=name}
            for(Iterator var5 = this.names.entrySet().iterator(); var5.hasNext(); ++i) {
               
               // names集合的value作为key;
               // names集合的key又作为取值的参考args[0]:args[1, "Tom"]
               // eg:{id=args[0]:1, name=args[1]:Tome}
                Entry<Integer, String> entry = (Entry)var5.next();
                param.put(entry.getValue(), args[(Integer)entry.getKey()]);
               
                // 额外的将每一个参数也保存到map中,使用新的key:param1 ... paramN
                String genericParamName = "param" + String.valueOf(i + 1);
                if (!this.names.containsValue(genericParamName)) {
                    param.put(genericParamName, args[(Integer)entry.getKey()]);
                }
            }

            return param;
        }
    } else {
        return null;
    }
}

通过读源码中的注释,相信你大概懂了吧。如果不懂也没关系,我们通过实例演示给你。

演示

这一步,我们先搭建起项目,先看搭建之后的目录结构。如果你已经搭建起来,可以调到下一步。

module.png
structure.png

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_demo
spring.datasource.username=root
spring.datasource.password=xfsy2018
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

mybatis.type-aliases-package=com.fengwenyi.mybatis.demo1.domain
mybatis.mapper-locations=classpath:mapper/*.xml

User

package com.fengwenyi.mybatis.demo1.domain;

/**
 * @author Wenyi Feng
 */
public class User {

    private Integer id;
    private String name;
    private Integer age;
    // getter & setter
    // toString
}

UserDao

package com.fengwenyi.mybatis.demo1.dao;

import com.fengwenyi.mybatis.demo1.domain.User;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author Wenyi Feng
 */
@Mapper
public interface UserDao {

    // 查询
    User findById (Integer id);

}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.fengwenyi.mybatis.demo1.dao.UserDao" >
    <resultMap id="BaseResultMap" type="User" >
        <id column="id" property="id" jdbcType="INTEGER" />
        <result column="name" property="name" jdbcType="VARCHAR" />
        <result column="age" property="age" jdbcType="INTEGER" />
    </resultMap>

    <select id="findById" resultMap="BaseResultMap">
        SELECT
          id, name, age
        FROM
          user
        WHERE
          id = #{id}
    </select>

</mapper>

数据库

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4;

UserService

package com.fengwenyi.mybatis.demo1.service;

import com.fengwenyi.mybatis.demo1.dao.UserDao;
import com.fengwenyi.mybatis.demo1.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author Wenyi Feng
 */
@Service
public class UserService {

    @Autowired
    private UserDao userDao;

    public User findById (Integer id) {
        if (id != null && id > 0) {
            return userDao.findById(id);
        }
        return null;
    }

}

看一下测试代码

Demo1ApplicationTests

package com.fengwenyi.mybatis.demo1;

import com.fengwenyi.mybatis.demo1.domain.User;
import com.fengwenyi.mybatis.demo1.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class Demo1ApplicationTests {

    @Test
    public void contextLoads() {

    }

    @Autowired
    private UserService userService;

    @Test
    public void test1 () {
        User user = userService.findById(1);
        System.out.println(user.toString());
    }

}

运行:

Demo1ApplicationTests > test1()

只有一个参数

上面例子就是一个参数标准实例,我们抽样一下。

UserDao

    User findById (Integer id);

UserMapper.xml

    <select id="findById" resultMap="BaseResultMap">
        SELECT
          id, name, age
        FROM
          user
        WHERE
          id = #{id}
    </select>

一个参数,不论你用什么都可以取到,例如 id=#{ssss}

多个参数

UserDao

    User findByNameAndAge (String name, Integer age);

这样写怎么获取参数呢?

你可能会这样,我们写一个例子测试下

UserMapp.xml

    <select id="findByNameAndAge" resultMap="BaseResultMap">
        SELECT
          id, name, age
        FROM
          user
        WHERE
          name = #{name}
          AND
          age = #{age}
    </select>

运行会报错:

Caused by:
org.apache.ibatis.binding.BindingException:
Parameter 'name' not found.
Available parameters are [arg1, arg0, param1, param2]

这样写,不对,那我们该怎么写呢?

    <select id="findByNameAndAge" resultMap="BaseResultMap">
        SELECT
          id, name, age
        FROM
          user
        WHERE
          name = #{param1}
          AND
          age = #{param2}
    </select>

原因,开始我们就说了。

@Param

我们改善一下

UserDao

    User findByNameAndAge2 (@Param("name") String name, @Param("age") Integer age);

UserMapper.xml

    <select id="findByNameAndAge2" resultMap="BaseResultMap">
        SELECT
          id, name, age
        FROM
          user
        WHERE
          name = #{name}
          AND
          age = #{age}
    </select>

POJO

UserDao

    User findByPojo (User user);

UserMapper.xml

    <select id="findByPojo" resultMap="BaseResultMap">
        SELECT
          id, name, age
        FROM
          user
        WHERE
          name = #{name}
          AND
          age = #{age}
    </select>

Map

看源码我们已经知道,MyBatis会将我们传递的参数封装成Map,那我们直接传Map会怎么样呢?

UserDao

    User findByMap (Map<String, Object> map);

UserMapper.xml

    <select id="findByMap" resultMap="BaseResultMap">
        SELECT
          id, name, age
        FROM
          user
        WHERE
          name = #{name}
          AND
          age = #{age}
    </select>

List

下面这两种是特殊的,但我还是说给你听

    User findByList (List<Object> list);
    <select id="findByList" resultMap="BaseResultMap">
        SELECT
          id, name, age
        FROM
          user
        WHERE
          name = #{list[0]}
          AND
          age = #{list[1]}
    </select>

Array

数组怎么处理

    User findByArray (Object [] array);

    <select id="findByArray" resultMap="BaseResultMap">
        SELECT
          id, name, age
        FROM
          user
        WHERE
          name = #{array[0]}
          AND
          age = #{array[1]}
    </select>

$ AND #

取值我们都会用 #{}

在我们习惯用的 ${},你们觉得这个很特别吗?有没有想过为什么呢?

  • #

UserDao

    User testGetValue1 (String name);

UserMapper.xml

    <select id="testGetValue1" resultMap="BaseResultMap">
        SELECT
          id, name, age
        FROM
          user
        WHERE
          name = #{name}
    </select>

看下运行日志:

==> Preparing: SELECT id, name, age FROM user WHERE name = ?
==> Parameters: AAA(String)
<== Columns: id, name, age
<== Row: 1, AAA, 20
<== Total: 1

我们继续看

  • $

UserDao

    User testGetValue2 (@Param("name") String name);

UserMapper.xml

    <select id="testGetValue2" resultMap="BaseResultMap">
        SELECT
          id, name, age
        FROM
          user
        WHERE
          name = '${name}'
    </select>

看下sql日志:

==> Preparing: SELECT id, name, age FROM user WHERE name = 'AAA'
==> Parameters:
<== Columns: id, name, age
<== Row: 1, AAA, 20
<== Total: 1

对比看下,你也许会发现各自的用法。

值得注意的是,这里只是 $ 的测试示例,在实际中,不推荐这么写。

$的用法

既然有 ${},那应该怎么用呢?我们再看一个例子

UserDao

    User testGetValue3 (@Param("name") String name);

UserMapper.xml

    <select id="testGetValue3" resultMap="BaseResultMap">
        SELECT
          id, name, age
        FROM
          ${name}
        WHERE
          name = "AAA"
    </select>

看下sql日志:

==> Preparing: SELECT id, name, age FROM user WHERE name = "AAA"
==> Parameters:
<== Columns: id, name, age
<== Row: 1, AAA, 20
<== Total: 1

总结

到这里就算差不过结束了。总结一下吧:

参数多时,封装成Map,为了不混乱,我们可以使用 @Param 来指定封装时使用的key,使用 #{key} 就可以取出Map中的值

测试源码

MyBatis系列测试源码·参数

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • MyBatis是一个可以自定义SQL、存储过程和高级映射的持久层框架。MyBatis 摒除了大部分的JDBC代码、...
    七寸知架构阅读 6,691评论 6 56
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 只要说到跨域,就必须聊到JSONP,就必须讲一下JSONP的实现原理,以及在项目中哪个需求使用了JSONP,简单讲...
    Mr夜空阅读 643评论 0 2
  • 之前,很努力的告诉自己,为了把写作这门手艺练好,不论别的,首先要做日更一篇。 而这种鸡汤式的激励方式,于我大概坚持...
    浆糊郎中阅读 116评论 4 2