MyBatisReview

零散点记录

入门

1、接口式编程
原生: Dao -------> DaoImpl
mybatis:Mapper --------> xxxMapper.xml
2、SqlSession代表和数据库的一次会话,用完必须关闭
3、SqlSession和Connection一样都是非线程安全,若声明为成员变量,就有可能存在资源竞争,每次使用都应该获取新的对象。
4、mapper接口没有实现类,但是mybatis会为这个接口生成一个代理对象。

  • 代理对象的产生(接口和xml绑定)
  • session.getMapper(xxxMapper.class)
    5、两个重要的配置文件:
    1)mybatis的全局配置文件,包含数据库连接池信息,事务管理器信息等。
    2)sql映射文件,保存了每一个sql语句的映射信息。

动态SQL

1、对于增删改,<insert>updatedelete没有resultType属性,在使用原始JDBC记性操作的时候通常是返回int类型的值,代表受结果影响的行数,但是MyBatis对于增删改支持自定义返回值类型Integer Boolean Long void(包括它们的基本类型)。

<insert id="insert" keyProperty="id" parameterType="Person">
        insert into person(username,password) values(#{username},#{password}),(#{username},#{password});
 </insert>
// 返回结果受影响的行数(Long同理)
Integer insert(Person person);
// 影响行数>0返回true,否则返回false
boolean insert(Person person);

2、数据库自增主键,对于Oracle不支持自增主键,可以通过预先生成一个id之后在进行插入。

<insert id="insertAuthor" databaseId="oracle">
    <!--
        keyProperty:查出的主键值封装给JavaBean的哪个属性
        order="BEFORE|AFTER":当前sql在插入sql之前还是之后运行
        resultType:返回值类型
    -->
    <selectKey keyProperty="authorId" resultType="int" order="BEFORE">
    	<!--Oracle 利用序列获取自增主键-->
        select AUTHOR.nextval from dual 
    </selectKey>
    insert into author(authorId,name,email)
    values(#{id},#{name},#{email})
</insert>

提供了对于不支持数据库自增主键获取的一种解决方案。

3、Mybatis映射文件参数处理

1.
单个参数(除了集合):mybatis不会做特殊处理
#{参数名}:取出参数值,名字符合常规定义就行,不强制要和函参名一致。
2.
多个参数或者集合类型:mybatis会做特殊处理,将参数封装成一个map,map的
key为param1...paramN,或者参数的索引 arg0..arg1
value等于传入的参数值
#{param1 | paramN}取出第N个键为paramN的值
3.
命名参数:明确指定封装参数时map的key:@Param("id")
	多个参数会封装成一个map
	key:使用@Param注解指定的值
	value:参数值
#{指定的key}取出对应的参数值
4.
POJO:
如果多个参数正好是我们业务逻辑的数据模型,我们就可以直接传入pojo
	#{属性名}:取出传入的pojo的属性值
5.
Map:
如果多个参数不是业务模型中的数据,没有对应的pojo,为了方便,也可以传入map
	#{key}
6.
TO:
如果多个参数不是业务模型中的数据,但是经常要使用,推荐来编写一个TO(Transfer Object)数据传输对象
比如分页对象
Page{
	int index;
	int limit;
	...
}
public Employee getEmp(@Param("id")Integer id, String lastName);
    取值方式:==>   #{id|param1},            #{param2}
public Employee getEmp(Integer id, @Param("e")Employee emp);
    取值方式:==>    #{param1}               #{param2.lastName|e.lastName}
/*
 注意:如果是Collection(List、Set)或者数组类型,Mybatis在类型转化的时候
 也会特殊处理,也是把传入的list或者数组封装在map中,
 key: collection | list | array
 示例如下:
 */
public Employee getEmpById(List<Integer> ids);
    取值方式:#{list[0]}

Mybatis参数封装源码解析

Mapper文件
<select id="query" resultType="mybatis.po.Person">
    select * from person where username=#{param1} and password=#{param2}
</select>

Dao接口
class PersonDao{
	Person query(String username, String password);
}

当dao接口中的方法是多个参数时,这时Mapper文件中<select>标签取值必须是param1,arg0才能取到调用的方法第一个实参的值,如果用#{username},#{password}等方式取值的话,则会抛出如下异常

 Parameter 'username' not found. Available parameters are [arg1, arg0, param1, param2]

要想使用上述方法取值,可以通过在方法形参上增加@Param("username")注解。

为什么在没有使用注解的前提下,使用方法形参名取值就会出现取不到传递的具体实参值呢?

多个参数的情况下,传递的实参值会被封装为map作为具体的值value属性,方法形参如果没有使用@Param注解,那么最终封装的map会以param1或者arg0做为map的键key属性,实参的值做为map具体的值,加了注解则是以@Param注解里面的值当做键。当最后对sql语句的参数进行注入赋值时,是把#{参数名}里面的参数名当做key去map中取值,如果方法参数没有@Param注解,那么map中的可以就是{param1,arg0...}之类的键。

比如#{username},在最后对sql语句中的参数进行赋值的时候preparedStatement.setXxx(),会将username当做键去之前封装实参的map中寻找实参值,显然这是找不到的,因为实参中没有username这个键,实参在封装的时候因为没有具体的@param注解,默认的key则会是param1,paramN之类的,当加了@param(username)注解后,则封装实参的map会以username为键,并且还会额外封装上param1,paramN的键,所以即使用了注解,仍然能使用#{param1...}之类的方式取值。

源码分析:

PersonDao mapper = session.getMapper(PersonDao.class);
// session.getMapper得到PersonDao代理对象mapper
Person p = mapper.query("stronger", "123");

当执行具体的查询方法时,会被MapperProxy的inovke方法拦截,MapperProxy实现了InvocationHandler接口,所以说它是基于JDK的动态代理来对方法拦截进行增强的。

public class MapperProxy<T> implements InvocationHandler, Serializable {
	
  ...
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // method.getDeclaringClass()得到方法所在的类对象,如果该类是Object类,则放行,因为Object存在
        //toString,euqals...,为了不拦截基类的方法所以放行
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (method.isDefault()) {
        if (privateLookupInMethod == null) {
          return invokeDefaultMethodJava8(proxy, method, args);
        } else {
          return invokeDefaultMethodJava9(proxy, method, args);
        }
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    // 获得MapperMethod对象,里面封装了执行sql的必要属性,比如操作是CRU的哪一种
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // 调用execute方法
    return mapperMethod.execute方法(sqlSession, args);
  }
  ...
}

mapperMethod.execute方法属于类MapperMethod,这里主要对sql命令的类型CRUD进行判断,然后将实参值转化为sql命令的参数,就是将实参值进行了一步具体的封装。

public class MapperMethod {
...
  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:
        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对实参值进一步的封装
          // 调用本类下的convertArgsToSqlCommandParam
          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());
    }
	
    
    public Object convertArgsToSqlCommandParam(Object[] args) {
      // paramNameResolver返回封装的实参值
      return paramNameResolver.getNamedParams(args);
    }
    
    
...
}

ParamNameResolver类存在一个有参的构造器,主要完成将方法形参值的封装,目的是将其作为封装map的key。这里面存在着两步转化

1)构造器中,首先会将这个方法的形参值以键值对形式存入SortedMap<Integer, String> names中,key是Integer类型,代表形参的位置;value是方法形参值,这里的形参值并不是真实的形参值;如果形参值上标有@param注解,则形参值为注解的值,否则形参值默认为参数位置。

第一步将方法的形参值有注解则为注解的值,无注解则为默认参数位置作为value存入names中,下面注释也有说明。

{0:“username”}(有注解的情况),无注解则是 {0:“0”}

2)将names中的值做为一个键,值为实参的值存入getNamedParams方法的param中,param就是一个map。

第二步将names中的值作为key,值为具体的实参值,存入param中。

{username:“stronger”}(有注解),无注解则是{param1:“stronger”}

源码如下:

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<>();
    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);
  }

  /**
   * <p>
   * A single non-special parameter is returned without a name.
   * Multiple parameters are named using the naming rule.
   * 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;
    // 1.对于单参并且没有注解的情况,直接返回args[0],并不是存入map中取,所以
    // 单参取值不依赖于具体的参数名,如#{name},name与方法形参名称相同与否没有关系。  
    } else if (!hasParamAnnotation && paramCount == 1) {
      return args[names.firstKey()];
    } else {
      // 2. 多参或者存在注解(包括单参注解)
      // 都是封装为map,之后注入取值的时候根据key去map中取值
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      // 下面就解释了为什么用username(没有加注解)的情况下,取值取不到,
      // 因为并没有这个键,此时只能使用通用键值的形式取值(param1, param2, ...)。
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        // 将names的value属性作为key,names的键作为数组的下标去args取具体的实参值。
        param.put(entry.getValue(), args[entry.getKey()]);
        // add generic param names (param1, param2, ...)
        // 下面还会增加一个通用的键和值 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;
    }
  }
}

字符串替换

#{}${}的区别:
1、#{}:是以预编译的形式,将参数设置到sql语句中;使用 #{} 参数语法时,MyBatis 会创建 PreparedStatement 参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。 这样做更安全,更迅速,通常也是首选做法。
2、${}:将取出的值直接拼接在sql语句中,存在安全问题。
建议

  • 一般情况下,使用#{}能有效防止sql注入

  • 对于想要直接在SQL语句中插入一个不转义的字符串,比如动态地查询更改表名,字段排序等。

    order by ${columnName}
    
Mapper文件
<select id="query" resultType="mybatis.po.Person">
    select * from person where username=#{param1} and password=#{param2}
</select>

Dao接口
class PersonDao{
	Person query(String username, String password);
}

当dao接口中的方法是多个参数时,这时Mapper文件中<select>标签取值必须是param1,arg0才能取到调用的方法第一个实参的值,如果用#{username},#{password}等方式取值的话,则会抛出如下异常

 Parameter 'username' not found. Available parameters are [arg1, arg0, param1, param2]

要想使用上述方法取值,可以通过在方法形参上增加@Param("username")注解。

为什么在没有使用注解的前提下,使用方法形参名取值就会出现取不到传递的具体实参值呢?

多个参数的情况下,传递的实参值会被封装为map作为具体的值value属性,方法形参如果没有使用@Param注解,那么最终封装的map会以param1或者arg0做为map的键key属性,实参的值做为map具体的值,加了注解则是以@Param注解里面的值当做键。当最后对sql语句的参数进行注入赋值时,是把#{参数名}里面的参数名当做key去map中取值,如果方法参数没有@Param注解,那么map中的可以就是{param1,arg0...}之类的键。

比如#{username},在最后对sql语句中的参数进行赋值的时候preparedStatement.setXxx(),会将username当做键去之前封装实参的map中寻找实参值,显然这是找不到的,因为实参中没有username这个键,实参在封装的时候因为没有具体的@param注解,默认的key则会是param1,paramN之类的,当加了@param(username)注解后,则封装实参的map会以username为键,并且还会额外封装上param1,paramN的键,所以即使用了注解,仍然能使用#{param1...}之类的方式取值。

源码分析:

PersonDao mapper = session.getMapper(PersonDao.class);
// session.getMapper得到PersonDao代理对象mapper
Person p = mapper.query("stronger", "123");

当执行具体的查询方法时,会被MapperProxy的inovke方法拦截,MapperProxy实现了InvocationHandler接口,所以说它是基于JDK的动态代理来对方法拦截进行增强的。

public class MapperProxy<T> implements InvocationHandler, Serializable {
	
  ...
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // method.getDeclaringClass()得到方法所在的类对象,如果该类是Object类,则放行,因为Object存在
        //toString,euqals...,为了不拦截基类的方法所以放行
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (method.isDefault()) {
        if (privateLookupInMethod == null) {
          return invokeDefaultMethodJava8(proxy, method, args);
        } else {
          return invokeDefaultMethodJava9(proxy, method, args);
        }
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    // 获得MapperMethod对象,里面封装了执行sql的必要属性,比如操作是CRU的哪一种
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // 调用execute方法
    return mapperMethod.execute方法(sqlSession, args);
  }
  ...
}

mapperMethod.execute方法属于类MapperMethod,这里主要对sql命令的类型CRUD进行判断,然后将实参值转化为sql命令的参数,就是将实参值进行了一步具体的封装。

public class MapperMethod {
...
  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:
        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对实参值进一步的封装
          // 调用本类下的convertArgsToSqlCommandParam
          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());
    }
	
    
    public Object convertArgsToSqlCommandParam(Object[] args) {
      // paramNameResolver返回封装的实参值
      return paramNameResolver.getNamedParams(args);
    }
    
    
...
}

ParamNameResolver类存在一个有参的构造器,主要完成将方法形参值的封装,目的是将其作为封装map的key。这里面存在着两步转化

1)构造器中,首先会将这个方法的形参值以键值对形式存入SortedMap<Integer, String> names中,key是Integer类型,代表形参的位置;value是方法形参值,这里的形参值并不是真实的形参值;如果形参值上标有@param注解,则形参值为注解的值,否则形参值默认为参数位置。

第一步将方法的形参值有注解则为注解的值,无注解则为默认参数位置作为value存入names中,下面注释也有说明。

{0:“username”}(有注解的情况),无注解则是 {0:“0”}

2)将names中的值做为一个键,值为实参的值存入getNamedParams方法的param中,param就是一个map。

第二步将names中的值作为key,值为具体的实参值,存入param中。

{username:“stronger”}(有注解),无注解则是{param1:“stronger”}

源码如下:

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<>();
    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);
  }

  /**
   * <p>
   * A single non-special parameter is returned without a name.
   * Multiple parameters are named using the naming rule.
   * 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;
    // 1.对于单参并且没有注解的情况,直接返回args[0],并不是存入map中取,所以
    // 单参取值不依赖于具体的参数名,如#{name},name与方法形参名称相同与否没有关系。  
    } else if (!hasParamAnnotation && paramCount == 1) {
      return args[names.firstKey()];
    } else {
      // 2. 多参或者存在注解(包括单参注解)
      // 都是封装为map,之后注入取值的时候根据key去map中取值
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      // 下面就解释了为什么用username(没有加注解)的情况下,取值取不到,
      // 因为并没有这个键,此时只能使用通用键值的形式取值(param1, param2, ...)。
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        // 将names的value属性作为key,names的键作为数组的下标去args取具体的实参值。
        param.put(entry.getValue(), args[entry.getKey()]);
        // add generic param names (param1, param2, ...)
        // 下面还会增加一个通用的键和值 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;
    }
  }
}
  • 一般情况下,使用#{}能有效防止sql注入

  • 对于想要直接在SQL语句中插入一个不转义的字符串,比如动态地查询更改表名,字段排序等。

    order by ${columnName}
    

#{}:规定参数的一些规则,参数也可以指定一个特殊的数据类型。

#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
javaType jdbcType mode(存储过程) numericScale
resultMap typeHandler jdbcTypeName expression(未来支持的功能)

大多时候,你只须简单指定属性名,顶多要为可能为空的列指定 jdbcType,其他的事情交给 MyBatis 自己去推断就行了。

jdbcType:当插入的数据为空时,有些数据库不能很好地支持mybatis对null的默认处理,比如Oracle在插入空值的时候会报错JdbcType OTHER,无效的数据类型,因为mybatis对所有的nulll映射的都是原生JDBC的OTHER类型,但是Oracle不支持。

由于全局配置中 其值为 OTHER,Oracle不支持 所以会报错,但是MySQL支持。

在这里插入图片描述

解决方案:

  • 修改全局配置中的设置项 jdbcTypeForNull=NULL

  • 指定插入的数据类型

    #{email, jdbcType=NULL}

结果映射

resultTyperesultMap是对select结果进行封装的两个属性。

1、resultType:期望从这条语句中返回结果的类全限定名或别名

1) 如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。 如果返回的是map,则表示将查询结果封装到以column_name-value的形式封装到map中。

Map<Object, Object> selectReturnMap(Integer id);
<select id="selectReturnMap" resultType="map">
    select * from employee where id=${id}
</select>
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Map<Object, Object> emp = mapper.selectReturnMap(1);
// 输出:将结果集中的列直接封装到map,并没有做任何转化
// {address=石家庄, gender=1, name=张三, id=1, dept_id=1, age=23}
//@MapKey value值可以作为整个结果的key
@MapKey("id")
Map<Object, Object> selectReturnMap(Integer id);
// {1={{address=石家庄, gender=1, name=张三, id=1, dept_id=1, age=23}}}

结果能成功地映射到map上,但是更推荐使用JavaBean或者pojo映射。

2)这个属性,并不能详细地规定具体每一列应该封装成什么类中的具体某个属性。

如数据库下划线命名转实体类驼峰命名:可以有两种方式进行配置这种数据库字段到实体类字段的封装。

  • 开启下划线转驼峰命名规范,缺陷是数据库命名和实体类命名一致性要求比较高。
  • 查询结果配置别名,select a_name as aName…,配置成mybatis能够自动封装的字段。
  • 使用resultMap属性,对列和实体属性进行详细映射。

3)如果封装的对象里面包括非Java自带类的引用类型,那么设置改属性往往就不能成功地将结果封装到对象。

这时,就可以使用resultMap来解决。

2、resultMap:自定义结果集映射规则

一个员工对应一个部门,实体类中包含有部门的引用,在对emp表和dept表连接查询时,查询的结果集如何封装到员工类中的部门属性。

  1. 将需要封装到复合引用对象的某个结果列重命名为"引用对象.属性名的方式"

    <select id="selectUserAndRoleById" resultType="wjx.mybatis.model.SysUser">
        select
        u.id,
        u.user_name userName,
        u.user_password userPassword,
        u.user_email userEmail,
        u.user_info userInfo,
        u.head_img headImg,
        u.create_time createTime,
        r.id "role.id",
        r.role_name "role.roleName",
        r.enabled "role.enabled",
        r.create_by "role.createBy",
        r.create_time "role.createTime"
        from sys_user u
        inner join sys_user_role ur on u.id=ur.user_id
        inner join sys_role r on r.id=ur.role_id
        where u.id=#{id}
    </select>
    
  2. 使用<resultMap>标签

    子标签:

    <id property="" column=""/>:id定义对象的唯一标识也就是主键。

    <result property="" column=""/>:定义普通列结果集映射规则。

    可以将JavaBean的全部属性都在resultMap中声明封装规则(推荐),也可以只写一部分,其余部分采用默认封装规则,但是推荐写的话就全写上。

    <resultMap id="userMap" type="wjx.mybatis.model.SysUser">
        <id property="id" column="id"/>
        <result property="userName" column="user_name"/>
        <result property="userPassword" column="user_password"/>
        <result property="userEmail" column="user_email"/>
        <result property="userInfo" column="user_info"/>
        <result property="headImg" column="head_img" jdbcType="BLOB"/>
        <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
        <result property="role.id" column="id"/>
        <result property="role.roleName" column="role_name"/>
        <result property="role.enabled" column="enabled"/>
        <result property="role.createBy" column="create_by"/>
        <result property="role.createTime" column="create_time" jdbcType="TIMESTAMP"/>
    </resultMap>
    
  3. 方式2的进一步优化,<assocation>标签嵌套查询

    <!--方式 1-->
    <assocation property="role" javaType="xx.xx.Role">
    	<result property="id" column="id"/>
        <result property="roleName" column="role_name"/>
        <result property="enabled" column="enabled"/>
        <result property="createBy" column="create_by"/>
        <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
    </assocation>
    <!--方式 2:结合标签中的select属性-->
    <assocation property="role" select="xx.xx.RoleDao.queryById" column="role_id"/>
    <!-- select:dao接口下全限定方法名-->
    

注意:resultMap中的嵌套查询(分布查询),默认是及时加载,可以使用fetchType="lazy|eager"局部或者lazyLoadingEnabled全局设置,配置嵌套查询的执行时机。

assocation用来配置一对一的结果映射,一对多的结果映射是通过collection标签配置的,用法都是大同小异。

关联assocation

关联的不同之处是,你需要告诉 MyBatis 如何加载关联。MyBatis 有两种不同的方式加载关联:

  • 嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
  • 嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集。

集合collection

集合元素和关联元素几乎是一样的,他们的不同之处如下。

  1. ofType属性指定集合中的元素类型,javaType指定具体的集合类型。

    ofType”属性。这个属性非常重要,它用来将 JavaBean(或字段)属性的类型和集合存储的类型区分开来。 所以你可以按照下面这样来阅读映射:

    <collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
    

    读作: “posts 是一个存储 Post 的 ArrayList 集合”

动态SQL

1、<if test="" />

test:判断表达式(OGNL,Apache)

  • 作用:从参数中取值进行判断

  • 注意点:遇见特殊符号应该写转移字符

    <if test="id != null"/>
    <if test="name != null and name != ''"/>
    <if test="name != null &amp;&amp name != &quot;&quot;"/>
    

2、mybatis条件查询的时候可能出现的问题

如果条件查询中没有匹配的条件,查询语句就有可能变成这样:

SELECT * FROM BLOG
WHERE

解决方法:

1)where后新增 1 = 1

2)使用<where>标签,mybatis会将<where>标签后第一个出现的and去除,只是去除第一个紧挨着的,不会去除条件之后的

where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。

当name不为空时,则进行查询,如果后面的if判断条件不成立,sql语句就可能为where name like ... and,后面的and并不会被去掉,如果其后为and order by ..,就会出现查询错误。

<where>
    <if test="name != null and name != ''">
        and name like #{name} and
    </if>
    <if test="...">
    	..
   	</if>

显然<where>标签不能解决上面的这个问题,这时就可以自定义trim元素定制where元素的功能。

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。

所以上面的例子可以改写为

<trim prefix="where" suffixOverrides="AND |OR ">
    <if test="name != null and name != ''">
        and name like #{name} and
    </if>
    <if test="...">
    	..
   	</if>

3、<set>标签:set标签会动态的在行首插入set关键字,并会删除额外的逗号包括头和尾的逗号,所以说不会出现where标签只去头不去尾的情况。

同样<set>标签也可以用<trim>代替

<trim prefixOverrides="," prefix="set" suffixOverrides="," suffix="where">
    <if test="name != null">
        ,name=#{name},
    </if>
    <if test="age != null">
        age=#{age}
    </if>
</trim>

总结:

where标签主要是阶解决拼接动态SQL时的and关键字遗留问题。

set标签主要是解决拼接动态SQL时遗留的逗号问题。

trim标签可以实现了对where标签和set标签高度自定义。

4、foreach标签:对集合中的元素进行遍历

使用场景:批量插入(MySQL),构建IN条件语句。

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  WHERE ID in
  <foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">
        #{item}
  </foreach>
</select>

元素属性说明:

  • collection:遍历的集合对象,可以是(List、Set、Map)

    注意:这里的值不能随便写,前面曾经提到过mybatis在对参数进行封装的时候,集合类型的也会封装成map,在没有@Param注解标注的前提下,取值的时候集合类型必须写collection、list,对于Map值为map。

  • item:本次迭代获取到的元素,当使用map对象时,item表示值,index表示键

  • index:当前迭代的序号(索引,相对于list来说)

  • open:指定字符串开始的字符

  • close:指定字符串结尾的字符

  • separator:遍历的元素与元素之间的分隔符

示例:批量插入

<!--第一种写法-->
<insert id="insertBatch">
    insert into employee(name, age, gender, address, dept_id)
    values
    <foreach collection="list" item="e" separator=",">
        (#{e.name},#{e.age},#{e.gender},#{e.address},#{e.deptId})
    </foreach>
</insert>
<!--第二种写法
	这种方式,必须配置数据库的连接属性
 jdbc:mysql:///db?characterEncoding=utf8&allowMultiQueries=true
allowMultiQueries详情可见官网https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-configuration-properties.html
-->
<insert id="insertBatch">
    <foreach collection="list" item="e" separator=";">
        insert into employee(name, age, gender, address, dept_id)
        values
        (#{e.name},#{e.age},#{e.gender},#{e.address},#{e.deptId})
    </foreach>
</insert>

Oracle批量插入的方式:

Oracle不支持MySQL形式的批量插入,可以通过两种方式实现批量插入。

1)多个insert放在begin - end块中。

begin
	insert ...;
	insert ...;
end;

动态SQL形式

<insert>
<foreach collection="list" 
         item="item" 
         seperator=";"
         open="begin" end="end;">
    insert ...
</foreach>
</insert>

2)利用中间表

insert into employees(id, name, email)
	select employees.nextval,name,email from(
    	select '插入的名字' name, '邮箱' email from dual
        union
        select '插入的名字' name, '邮箱' email from dual
        union
        ...
    ); 

动态SQL形式

<insert id="...">
    insert into employees(id, name, email)
	select employees.nextval,name,email from
	<foreach collection="list" 
         item="item" 
         seperator="union"
         open="(" end=")">
		select ...
</foreach>
</insert>

5、mybatis的两个内置参数_parameter_databaseId

_parameter:代表整个参数
单个参数:_parameter就是这个参数
多个参数:参数会被封装为一个map:_parameter就是代表这个map,_paramter.get(0)可以得到第一个参数对象。

_databaseId:如果配置了databaseIdProvider标签
_databaseId 就是代表当前数据库的别名。

应用场景:动态sql if标签条件判断中,往往都是对参数中的属性进行判断比如判空,没有一个对象能够代表方法参数,_parameter就可以用来代表方法上的参数对象。

6、bind:从OGNL表达式中创建一个变量,并将其绑定到上下文中,常用于模糊查询的SQL中。

模糊查询的处理方式:

假设要查询姓名为姓张的员工

错误的模糊查询

xml
<select id="selectEmpByName" resultMap="baseMap">
    select id,name,gender,age,dept_id,address
    from employee
    where name like '%#{name}%';
</select>
//dao接口定义
List<Employee> selectEmps(String name);   

EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
List<Employee> list = mapper.selectEmpByName("张");

上述代码运行后存在一个Cause: java.sql.SQLException: Parameter index out of range (1 > number of parameters, which is 0)异常,这种方式并不能'%#{name}%'不能支持模糊查询

#{}是以预编译的形式通过PreparedStatement对象进行设置参数。

1)like '%#{name}%'修改为like #{name},调用查询API的时候传入"%"

mapper.selectEmpByName("张%");

2)使用${}字符串拼接的形式

name like '%${name}%';

这样在调用API的时候直接传入要查的姓即可。

3)通过bind标签将值取出二次修改后,在上下文中引用这个修改后的值

<select id="selectEmpsByName" resultMap="baseMap">
    <!--将name值取出进行%拼接,下文中在通过#{_name}的形式引用定义在name属性中的变量-->
    <bind name="_name" value="'%'+name+'%'"/>
    select id,name,gender,age,dept_id,address
    from employee
    where name like #{_name};
</select>

7、<sql><include refid="">

<sql>:定义可重用的代码片段,以便在其他语句中使用。参数可以静态地(在加载的时候)确定下来,并且可以在不同的 include 元素中定义不同的参数值。

应用场景:比如插入和查询常用的列片段,可以将其定义在sql中,之后直接可以通过<include refid="">标签进行引用。

<include refid="">:引用sql定义的代码片段,包含的子标签

  • <property name="" value=""/>,可以用于对引用的代码片段赋值。

示例1:片段的引用

<sql id="cols">
    id,name,gender,age,dept_id,address
</sql>
<select id="selectEmpsByName" resultMap="baseMap">
    select
    <!--通过include标签进行对sql片段进行引用-->
        <include refid="cols"/>
    from employee
    where name like #{_name};
</select>

示例2:片段的赋值

<sql id="cols">
    ${tb_emp}.name,${tb_dept}.name
</sql>

<select id="selectName" resultType="map">
    select
        <include refid="cols">
            <property name="tb_emp" value="employee"/>
            <property name="tb_dept" value="dept"/>
        </include>
    from
        employee,dept
    where
        employee.dept_id=dept.dept_id;
</select>

sql语句

select employee.name,dept.name from employee,dept where employee.dept_id=dept.dept_id; 

缓存

MyBatis默认有两级缓存:

一级缓存

一级缓存(本地缓存):SqlSession级别的缓存。一级缓存是一直开启的,不能关闭;与数据库同一次会话期间查询到的数据会放在本地缓存中。以后如果需要获取相同的数据,直接从缓存中拿,不需要查询数据库。

一级缓存失效情况(没有使用到当前一级缓存的情况,效果就是,还需要再向数据库发出查询)

  1. 通过SqlSessionFactory拿到不同的SqlSession对象,不同的SqlSession都有自己的缓存,它们之间互不干扰。

  2. SqlSession相同,查询条件不同。(当前一级缓存中没有这个数据)

  3. SqlSession相同,两次查询之间进行了增删改操作,缓存也会失效。以我们的角度去想,增删改可能会对当前查询的数据造成影响,所以要去数据库中查询。

  4. SqlSession相同,查询期间执行了清空缓存操作。

二级缓存

二级缓存(全局缓存):基于namespace级别的缓存,一个namespace对应一个二级缓存。

工作机制:首次查询数据,数据就会放在当前会话的一级缓存中,如果会话关闭,一级缓存中的数据会被保存到二级缓存中,开启一个新的会话查询信息,此时就会去二级缓存中查找。

不同的namespace查出的数据会放在自己对应的缓存中(map)
sqlSession == EmployeeMapper ==> Employee
			  DepartmentMapper ==> Department

二级缓存的使用步骤

1)在全局配置文件中开启缓存

设置名描述有效值默认值
cacheEnabled全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。true | falsetrue
<settings>
    <!--cacheEnabled:默认是true,但是为了防止以后版本变化所带来的影响,
		一般还会显示声明一下-->
	<setting name="cacheEnabled" value="true"/>
</settings>

2)二级缓存基于namespace的缓存,在需要使用二级缓存的SQL映射文件中添加一行:

<cache/>

3)一定要实现序列化接口

<cache>标签的属性:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"
  type="自定义Cache接口的实现类全类名"/>

eviction:清除策略,默认的是LRU
可用的清除策略有:

  • LRU – 最近最少使用:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。

可读写的缓存会(通过序列化)返回缓存对象的拷贝,所以说使用二级缓存涉及到的实体对象要实现序列化接口。 速度上会慢一些,但是更安全,因此默认值是 false。

注意点只有会话被关闭,一级缓存中的数据会被保存到二级缓存中

缓存有关的属性设置

1、全局配置cacheEnabled掌管着二级缓存的开启关闭,若全局二级缓存处于关闭状态,那么即便在标签中<select useCache="true">也不能使用二级缓存。这个属性对一级缓存没有影响。

2、<select>标签的useCache="true|false"属性,决定是否使用二级缓存,对一级缓存没有影响。

3、每个增删改标签的flushCache="true"清空一二级缓存;查询标签flushCache="false",如果将查询标签的这个属性设置为true,那么在执行一次查询后也会清空一二级缓存。

4、sqlSession.clearCache();只会清除一级缓存。

5、对于增删改操作,默认都会清空缓存,但是多了一步去缓存中查找的过程,尽管有命中率,但是缓存中实际没有数据,所以还会再次发送一条sql语句。

缓存的原理

在这里插入图片描述

MyBatis运行原理

1、获取SqlSessionFactory

//===============================SqlSessionFactory
//1.SqlSessionFactory.buid
public SqlSessionFactory build(InputStream inputStream) {
  return build(inputStream, null, null);
}
...
//2.构建XMLConfigBuilder,实例化该对象的同时会初始化重要的类成员比如
    //configuration,XPath对象等
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    // 调用parser.parse
      return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}
//=============================XMLConfigBuilder
//3.
public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //parser.evalNode("/configuration")调用XPath解析获得根节点对象root
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

//4.解析配置文件的每个标签中的每个属性放入到configuration中
  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

//5.解析完毕之后返回Configuration对象给buid方法,最后生成一个DefaultSqlSessionFactory实例

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

)

在这里插入图片描述

2、获取SqlSession

//==================================DefaultSqlSessionFactory

public SqlSession openSession() {
  /*
  configuration.getDefaultExecutorType()获取默认执行器类型,有三种
  SIMPLE/BATCH/官网有介绍 defaultExecutorType	配置默认的执行器。
  SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。	SIMPLE REUSE BATCH
  
  */
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    // 获取执行器
    final Executor executor = configuration.newExecutor(tx, execType);
    // 最后返回DefaultSqlSession对象,里面封装了configuration,executor重要的属性
    return new DefaultSqlSession(configuration, executor, autoCommit);
  } catch (Exception e) {
    closeTransaction(tx); // may have fetched a connection so lets call close()
    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}
//================================Configuration.newExecutor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // !!!拦截器重要的一个环节
    // 调用拦截器链的pluginAll方法保证Executor
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

在这里插入图片描述

在这里插入图片描述

3、获取MapperProxy

//=============================DefaultSqlSession.getMapper

@Override
public <T> T getMapper(Class<T> type) {
  return configuration.getMapper(type, this);
}
//=============================Configuration.getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

//=============================MapperRegistry.getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    //根据dao类获取MapperProxyFactory,前面提到过,MapperRegistry有两个重要属性,config和knownMappers
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
        // 调用mapperProxyFactory生成MapperProxy,MapperProxy就是一个实现了InvocationHandler的
        //接口
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }


public class MapperProxyFactory<T> {


  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
      //JDK动态代理
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值