1 单参数
当我们的Mapper接口,只有一个参数的时候,mybatis不会做任何处理,都会将参数映射到sql语句中
比如:
- mapper接口
TbUser selectById(Long id);
- sql映射文件,通过
#{id}
可以将参数动态的映射到sql语句中,由于这里只有一个参数,所以大括号里面就所谓是啥,都可以
映射到sql语句中,比如我下面随便写的#{dashjdkas}
,也可以将参数动态的映射到sql中
<select id="selectById" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" parameterType="long">
select
*
from
tb_user
where
id = #{id}
</select>
2 多个参数
2.1 多参数映射:
TbUser selectByUsernameAndPhone(String username,String phone);
<select id="selectByUsernameAndPhone" resultType="study.wyy.mybatis.sqlmapper.model.TbUser">
select
*
from
tb_user
where
`username` = #{username} AND
`phone` = #{phone}
</select>
测试:
@Before
public void getMapper() throws IOException {
// 加载全局配置文件
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
// 构建sqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 生成Mapper接口的代理对象
tbUserMapper = sqlSession.getMapper(TbUserMapper.class);
}
@Test
public void test02() {
TbUser tbUser = tbUserMapper.selectByUsernameAndPhone("kobe","13100001111");
System.out.println(tbUser);
}
抛出异常:
Cause: org.apache.ibatis.binding.BindingException: Parameter 'username' not found. Available parameters are [arg1, arg0, param1, param2]
参数username没有找到,合法的参数是[arg1, arg0, param1, param2]
尝试修改一下sql映射的取值方式改为#{param1} 和 #{param2}:
<select id="selectByUsernameAndPhone" resultType="study.wyy.mybatis.sqlmapper.model.TbUser">
select
*
from
tb_user
where
`username` = #{param1} AND
`phone` = #{param2}
</select>
或者使用arg0和arg1取值
<select id="selectByUsernameAndPhone" resultType="study.wyy.mybatis.sqlmapper.model.TbUser">
select
*
from
tb_user
where
`username` = #{arg0} AND
`phone` = #{arg1}
</select>
再次测试,上面两种方式没有报错,并且参数成功映射到sql语句中
总结
多参数mybatis会进行处理:
- 多个参数为封装成一个Map
- map的key就是:param1,param2,param3 … paramN
- map的value就是传入的参数值
- 所以使用#{}取值的时候,就使用#{param1},#{param2}这种方式取值映射到sql语句中
- 或者使用使用#{arg0}和#{arg1}取值
2.2 命名参数
还是期望#{参数名}这样取值,更加清晰:
<select id="selectByUsernameAndPhone" resultType="study.wyy.mybatis.sqlmapper.model.TbUser">
select
*
from
tb_user
where
`username` = #{username} AND
`phone` = #{phone}
</select>
mybatis提拱了一个注解Param
,来为参数命名,指定封装成Map时候使用的key
TbUser selectByUsernameAndPhone2(@Param("username") String username, @Param("phone") String phone);
<select id="selectByUsernameAndPhone2" resultType="study.wyy.mybatis.sqlmapper.model.TbUser">
select
*
from
tb_user
where
`username` = #{username} AND
`phone` = #{phone}
</select>
3 参数为pojo或者Map
如果参数过多,还是封装成pojo比较好,当然mybatis也支持传递pojo。
此时取值就使用#{属性名}
或者#{key}
TbUser selectByPojo(TbUser tbUser);
TbUser selectByMap(Map<String,Object> map);
<select id="selectByPojo" resultType="study.wyy.mybatis.sqlmapper.model.TbUser">
select
*
from
tb_user
where
`username` = #{username} AND
`phone` = #{phone}
</select>
<select id="selectByMap" resultType="study.wyy.mybatis.sqlmapper.model.TbUser">
select
*
from
tb_user
where
`username` = #{username} AND
`phone` = #{phone}
</select>
测试:
@Test
public void test04() {
TbUser user = new TbUser();
user.setUsername("kobe");
user.setPhone("13100001111");
TbUser tbUser = tbUserMapper.selectByPojo(user);
System.out.println(tbUser);
}
@Test
public void test05() {
Map<String, Object> map = new HashMap<>();
map.put("username","kobe");
map.put("phone","13100001111");
TbUser tbUser = tbUserMapper.selectByMap(map);
System.out.println(tbUser);
}
3 参数为list或者数组
也会进行处理,会把list或者数组封装到map中,key的取值:
- 如果是Collection:key为collection
- 如果是List:key为list
- 如果是数组:key为array
比如:
TbUser selectByIds(List<Long> id);
<select id="selectByIds" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" >
select
*
from
tb_user
where
<!--取出第一个元素-->
id = #{list[0]}
</select>
测试
@Test
public void test06() {
List<Long> ids = new ArrayList<>();
ids.add(1L);
TbUser tbUser = tbUserMapper.selectByIds(ids);
System.out.println(tbUser);
}
同样也支持使用Param
注解,来为参数命名:
TbUser selectByIds(@Param("ids") List<Long> id);
<select id="selectByIds" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" >
select
*
from
tb_user
where
<!--取出第一个元素-->
id = #{ids[0]}
</select>
4 组合使用场景
TbUser select1(@Param("id") Long id, String phone);
TbUser select2(Long id, TbUser tbUser);
TbUser select3(@Param("id") Long id, @Param("user") TbUser tbUser);
<select id="select1" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" >
select
*
from
tb_user
where
`id` = #{id} AND
`phone` = #{param2}
</select>
<select id="select2" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" >
select
*
from
tb_user
where
`id` = #{param1} AND
`phone` = #{param2.phone}
</select>
<select id="select3" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" >
select
*
from
tb_user
where
`id` = #{id} AND
`phone` = #{user.phone}
</select>
5 源码解读
就是用这个方法来debug,解读一下源码,这个方法一个参数使用了Param注解,一个没有使用
TbUser select1(@Param("id") Long id, String phone);
@Test
public void test07() {
TbUser tbUser = tbUserMapper.select1(1L,"13100001111");
}
- org.apache.ibatis.binding.MapperProxy的 invoker方法:
/****
Object proxy: mybatis根据Mapper接口生成的动态代理类
Method method: 要执行的方法,比如这里就是select1方法
Object[] args:就是当前执行的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 判断当前要执行的方法是不是Object类的方法(比如toString等Object
//类中声明的方法),如果是那么就直接执行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 如果不是Object类的方法,那么就执行cachedInvoker(method)
// 显然我们接口中声明的方法都会执行这块逻辑
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
- cachedInvoker方法内部:
MapperMethodInvoker:两个实现: DefaultMethodInvoker和PlainMethodInvoker
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// computeIfAbsent:首先会从缓存中获取MapperMethodInvoker对象, 如果缓存中没有那么就是构建MapperMethodInvoker并放入到methodCache(就是个Map)缓存中
return methodCache.computeIfAbsent(method, m -> {
// 当前方法是不是Mapper接口的default方法,如果是default方法,就构造DefaultMethodInvoker对象返回
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
// 不是接口的默认方法,就构造MapperMethodInvoker的另一个实现:PlainMethodInvoker
// 先去构造MapperMethod
// mapperInterface: mapper接口的class对象
// method: 当前要执行的mapper接口中的方法
// sqlSession.getConfiguration(): 获取mybaits的全局配置
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
- 构造MapperMethod,来到构造方法
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
// 构造SqlCommand: 解析mapper接口,封装statementId,以及操作类型(Select,update,delete。。),不多介绍
this.command = new SqlCommand(config, mapperInterface, method);
// 方法签名,
this.method = new MethodSignature(config, mapperInterface, method);
}
SqlCommand: 解析mapper接口,封装statementId,以及操作类型(Select,update,delete。。)这里不多介绍:
- 构建MethodSignature:
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
// 这些先不关心,解析当前执行的Mapper接口的方法的返回值,这里就是返回TbUser
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
this.returnType = method.getReturnType();
}
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
this.returnsCursor = Cursor.class.equals(this.returnType);
this.returnsOptional = Optional.class.equals(this.returnType);
this.mapKey = getMapKey(method);
this.returnsMap = this.mapKey != null;
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
// 核心在这构建paramNameResolver:参数名字解析器
this.paramNameResolver = new ParamNameResolver(configuration, method);
}
- 核心在这构建paramNameResolver:参数名字解析器
public ParamNameResolver(Configuration config, Method method) {
// 1 获取参数类型,比如当前执行的这个select1方法的类型就是Long.class, String.class
final Class<?>[] paramTypes = method.getParameterTypes();
// TbUser select1(@Param("id") Long id, String phone);
// 2 获取方法形参上的注解,id这个参数存在有一个注解Param,phone参数没有注解
// 解释一下返回值二维数组:
// 比如这里有两个参数id 和 phone,二维数组的长度就是2
// 每个参数可以有多个注解,所以就是二维数组
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<>();
int paramCount = paramAnnotations.length;
// 遍历这个二维数组
// get names from @Param annotations 遍历获取从@Param注解获取name
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
// 判断每一个参数类型是不是特殊的类型:RowBounds.class 或者ResultHandler.class
if (isSpecialParameter(paramTypes[paramIndex])) {
// skip special parameters:如果是特殊的就跳过
continue;
}
String name = null;
// 遍历每个参数上的每个注解
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
// 当前参数的当前注解是Param注解,标记hasParamAnnotation设置为true
hasParamAnnotation = true;
// name 就是Param注解的value值
name = ((Param) annotation).value();
// 跳出当前循环
break;
}
}
// 如果name是null
if (name == null) {
// isUseActualParamName是全局配置文件的一个配置项,默认是true
// protected boolean useActualParamName = true;
if (config.isUseActualParamName()) {
// 没有使用Param注解指定参数名称,就调用这个方法生成name
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,key是参数的索引,value就是name
map.put(paramIndex, name);
}
// private final SortedMap<Integer, String> names; names属性就赋值为这个map
names = Collections.unmodifiableSortedMap(map);
}
- 关于paramAnnotations二维数组图解:
- 加了Param注解的参数处理:
- 没有使用Param注解的参数处理:
- names属性就赋值为这个map
- getActualParamName: 生成参数名称的逻辑
private String getActualParamName(Method method, int paramIndex) {
// 生成list:[arg0,arg1],
return ParamNameUtil.getParamNames(method).get(paramIndex);
}
// ParamNameUtil
public static List<String> getParamNames(Method method) {
return getParameterNames(method);
}
private static List<String> getParameterNames(Executable executable) {
// 生成list:[arg0,arg1] 为啥是arg0,java编译之后,所有的形参都会编译成 arg0,arg0...,按照参数定义顺序排下去
return Arrays.stream(executable.getParameters()).map(Parameter::getName).collect(Collectors.toList());
}
- 到此为止,MapperMethodInvoker就构造完成,就回到了这里,执行MapperMethodInvoker的invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 执行MapperMethodInvoker的invoke
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
// 实际就是执行MapperMethod属性的execute方法
return mapperMethod.execute(sqlSession, args);
}
// MapperMethod 封装了SqlCommand和MethodSignature
// args: 当前的实参
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
// method 方法签名的狗仔,前面已经提过了,判断当前方法的返回值类型
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
// 还是集合
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
// convertArgsToSqlCommandParam:转换java参数到sql命令参数
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
- convertArgsToSqlCommandParam:转换java参数到sql命令参数
public Object convertArgsToSqlCommandParam(Object[] args) {
// 就是调用paramNameResolver参数解析的getNamedParams方法
return paramNameResolver.getNamedParams(args);
}
- paramNameResolver参数解析的getNamedParams方法, 终于到了最核心的地方
// args: 实参
public Object getNamedParams(Object[] args) {
// names是个Map,key是参数索引值,value是参数名称,上面已经分析这个map的生成
final int paramCount = names.size();
// 如果接口方法没有形参,或者参数个数是0,就直接返回
if (args == null || paramCount == 0) {
return null;
// 如果hasParamAnnotation这个标记是false并且,形参个数只有一个,那么就直接返回names的第一个key
// 所以这就是一开始说的,如果只有一个参数,#{}使用任何名称都可以取到参数,映射到sql中
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
// 如果参数不只一个的时候,
// param就是前面说的mybatis多参数的时候会把参数封装到一个map,现在就开始构造这个map
// 首先这个map的value就是参数的值,主要看key的生成, 也就是使用#{}取值时,{}里面要写的是啥
final Map<String, Object> param = new ParamMap<>();
int i = 0;
// 遍历names
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// param的key就是参数名称,value是参数的值
// args是实参数组,names的key就是参数索引
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
// public static final String GENERIC_NAME_PREFIX = "param";
// 拼接上param,这就是多参数的时候使用的#{paramN}来取参数的值
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++;
}
// 返回这个map
return param;
}
}
6 扩展:useActualParamName
回头看一个东西:isUseActualParamName,是全局配置文件的一个配置项,默认是true
// 如果name是null
if (name == null) {
// isUseActualParamName是全局配置文件的一个配置项,默认是true
// protected boolean useActualParamName = true;
if (config.isUseActualParamName()) {
// 没有使用Param注解指定参数名称,就调用这个方法生成name
name = getActualParamName(method, paramIndex);
}
// 如果这个isUseActualParamNamefalse,并且没有使用Param注解使用指定参数名称,
// 那么就不能使用#{arg0}方式取值,而是#{参数索引}取值,或者#{param0}这种方式
if (name == null) {
// use the parameter index as the name ("0", "1", ...)
// gcode issue #71
name = String.valueOf(map.size());
}
}
看一下官网对useActualParamName这个配置的介绍:
允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1)
意思是可以直接通过形参的名称取值,但是两个前提条件
- useActualParamName为true:默认就是true,如果想要配置该如何配置:
<settings>
<setting name="useActualParamName" value="true"/>
</settings>
- 使用java8编译,并且加上
-parameters
编写选项,可以使用maven编译插件配置
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<!--使用1.8-->
<target>1.8</target>
<source>1.8</source>
<encoding>utf-8</encoding>
<compilerArgs>
<!--添加编译参数-->
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
测试一下:
// 没有使用注解指定参数名
TbUser select4(Long id,String phone);
<select id="select4" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" >
select
*
from
tb_user
where
<!--这里也没使用#{param1}这种方式取值-->
`id` = #{id} AND
`phone` = #{phone}
</select>
可以用idea看一下编译后的class:
- TbUser select4(Long id, String phone);
之前:
TbUser select4(Long var1, String var2);
所以:就不再支持#{arg0}这种取值了,所以啊不要用#{arg0}这种方式取值,存在兼容
<select id="select4" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" >
select
*
from
tb_user
where
`id` = #{arg0} AND
`phone` = #{arg1}
</select>