深入学习Mybatis-mybatis开发实战


1. Mybatis三大对象作用域

为什么要将SqlSession对象放到ThreadLocal当中呢?

这是为了保证一个线程对应一个SqlSession,在一个session会话中,一个线程对应一个sqlSession

1.1 SqlSessionFactoryBuilder

这个类的使命是用来创建SqlSessionFactory的,因此这个类的最佳作用域就是方法作用域,它的build()方法实际上就是在读取xml文件,最好不会一直保留它,要保证所有的XML解析资源被释放给其他进程使用

1.2 SqlSessionFactory

这个对象的使命是用来创建SqlSession,它一旦被创建就应该在应用的运行期间一直存在,没有任何理论丢弃它或者重新创建新的实例,它在运行期间不要重复创建多次,因为每一次的创建都涉及到IO的操作,因此它的最佳作用域就是应用作用域,可以使用单例模式或者静态单例模式来维护这个对象

1.3 SqlSession

SqlSession的最佳实践就是:每个线程都拥有自己的SqlSession实例,它的实例不是线程安全的,因此不能被共享,它的最佳作用域是请求域或者方法域,绝对不能将SqlSession实例的引用放在一个类的静态域中,甚至一个类的实例变量也是不允许的。

它类似于HttpSession,你可以考虑将SqlSession放在一个和Http请求相似的作用域中,每次收到Http请求,就可以打开一个SqlSession,返回一个响应后就关闭它

本质上,SqlSession是对Conncetion对象的封装,因此其本身就是不安全的,由于一次缓存是session级别的,所以如果有多个线程同时使用session,当A进行set(),而B进行get()的时候,这时候就会产生缓存与数据库数据不一致的问题,产生线程不安全的问题。

为什么Connection是线程不安全的

connection.commit()是提交事务,假设线程A开启了事务,而线程B此时使用commit()提交了事务,在这种情况下,由于使用的是描述同一次连接,因此线程A就相当于没有开启事务,最终就会导致程序错误。

2. 使用javassist生成类

javassist是一个开源的分析、编辑和创建Java字节码的类库,通过使用Javassist对字节码操作为JBoss实现动态的AOP框架

2.1 示例代码

//1.获取类池,这个类池就是用来生成class的
ClassPool pool = ClassPool.getDefault();
//2.制造类,需要指定类名
CtClass ctClass = pool.makeClass("dao.StudentDAOImpl");
//3.制造方法,src里面写的是方法代码
String methodCode = "public void insert(){System.out.println(123);}";
CtMethod ctMethod = CtMethod.make(methodCode, ctClass);
ctClass.addMethod(ctMethod);
//生成类,在内存中生成class
ctClass.toClass();
//类加载,返回字节码
Class<?> clazz = Class.forName("dao.StudentDAOImpl");
Object newInstance = clazz.newInstance();
//获取方法
clazz.getDeclaredMethod("insert").invoke(newInstance);

2.2 用类去实现接口

public void testGenerate() throws Exception {
    //1.获取类池,这个类池就是用来生成class的
    ClassPool pool = ClassPool.getDefault();
    //2.制造类,需要指定类名
    CtClass ctClass = pool.makeClass("dao.TestDAOImpl");
    //制造接口
    CtClass ctInterface = pool.makeInterface("dao.TestDAO");
    //3.制造方法,src里面写的是方法代码
    String methodCode = "public void insert(){System.out.println(123);}";
    CtMethod ctMethod = CtMethod.make(methodCode, ctClass);
    ctClass.addInterface(ctInterface);//让这个类去实现这个接口
    ctClass.addMethod(ctMethod);
    //生成类,在内存中生成class,同时将生成的类加载到JVM中
    Class<?> clazz = ctClass.toClass();
    TestDAO testDAO = (TestDAO) clazz.newInstance();
    //获取方法
    testDAO.insert();
}

2.3 GenerateDaoProxy

作用就是:你只要给我一个接口,那我就能够根据你这个类生成一个Impl对象

public static Object generate(Class<?> interfaceDAO) throws Exception {
    ClassPool pool = ClassPool.getDefault();
    //制造类
    CtClass ctClass = pool.makeClass("dao.impl"+interfaceDAO.getName()+"Proxy");
    //制造接口
    pool.makeClass(interfaceDAO.getName());
    //实现接口
    ctClass.addInterface(ctClass);
    //实现接口中的所有方法
    Method[] methods = interfaceDAO.getDeclaredMethods();
    Arrays.stream(methods).forEach(method->{
        //将method进行实现
        try {
            StringBuilder code = new StringBuilder();
            code.append("public ");
            code.append(method.getReturnType());
            code.append(" ");
            code.append(method.getName());
            code.append("(");
            Class<?>[] parameterTypes = method.getParameterTypes();
            for (int i = 0;i<parameterTypes.length;i++) {
                Class<?> parameterType = parameterTypes[i];
                code.append(parameterType);
                code.append(" ");
                code.append("arg");
                code.append(i);
                if(i!=parameterTypes.length-1){
                    code.append(",");
                }
            }
            code.append(")");
            code.append("{");
            code.append("System.out.println(\"123\")");
            code.append("}");
            CtMethod ctMethod = CtMethod.make("", ctClass);
            ctClass.addMethod(ctMethod);
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
    //创建对象
    Object obj = ctClass.toClass().newInstance();
    return obj;
}
String sqlId = interfaceDAO.getName()+"."+method.getName();
SqlCommandType sqlCommandType = sqlSession.getConfiguration().getMappedStatement(sqlId).getSqlCommandType();
if(sqlCommandType == SqlCommandType.INSERT){

}else if(sqlCommandType == SqlCommandType.DELETE){

}//...

在Mybatis中,规定了namespace必须是接口的全限定类名,sql的Id必须是接口方法名称,这样的话底层才能够定位到你所需要实现的方法

2.4 getMapper方法

Mybatis提供了相关的机制,为我们提供了生成dao接口的实现类

实际上采用了代理模式,在内存中生成了dao接口的代理类,然后创建代理类的实例,使用mybatis的这种代理机制的前提,SqlMapper.xml文件中的namespace必须是dao接口的全限定类名,id是接口中的方法名

SqlSessionFactory build = new SqlSessionFactoryBuilder().build(ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml"));
build.openSession().getMapper(StudentDAO.class);

有什么用?

使用场景是这样的,当实现类中的代码相对固定的时候,这时候就不需要创建新的Impl类了,只需要使用代理对象,在内存中生成这段类代码,然后执行即可

3. Mybatis小技巧

3.1 #{}和${}有什么区别?

#{}:它是先编译语句然后再给占位符进行传值的,其底层是PreparedStatement,可以防止sql注入

${}:先进行sql语句的拼接,然后再编译sql语句,底层是Stament实现的,存在sql注入的现象,只有在需要进行sql语句关键字的拼接的情况下才会使用

为什么PreparedStatement能够防止sql的注入?

这是因为PreparedStatement程序在第一次查询数据库之前sql语句就被数据库进行了分析、编译、优化、以及具体的查询计划都形成了,只有参数位置才会使用占位符

当程序真正发起请求的时候,这时候传递过来的参数会被认为是某个字段的值,因而不会重新编译和优化,所以不会被认为是一个sql指令

如果传递进来的参数无法被看作是sql指令,那么就无法形成sql的注入了,比如说'1=1'这样的参数值会被认为是一个字符串类型的值,而不会看作是sql查询条件

正是因为sql的执行计划是在sql编译和优化执行的,因此sql的注入只是在编译阶段起作用,解决办法就是不让参数参与编译的阶段,而是让占位符参与编译,从而解决了sql的注入问题。

3.2 什么时候使用${}

  • 当sql的指令不确定的时候,需要前端传入动态的指令的时候,必须使用${}
  • 向sql语句中拼接表名,在实际的业务上,可能会存在分表存储数据的情况,当一张表进行查询的时候,查询效率比较低

3.3 批量删除

#根据id批量删除
delete
from t_car
where id in(${ids})

3.4 模糊查询like

select * 
from t_car
where brand_name like%${brandName}%

或者是使用concat函数,专门进行字符串的拼接

select * 
from t_car
where brand_name like concat('%','#{brandName}','%');

3.5 别名

<!--起别名,别名不区分大小写-->
<typeAliases>
    <!--type:原type alias:别名-->
    <!--当你省略alias的时候,直接就是类的简名-->
    <typeAlias type="domain.Student" alias="Student"/>
    <!--对包下的所有alias自动起别名-->
    <package name = "domain"></package>
</typeAliases>

3.6 mapper的配置

mapper的配置方式有

  • resource:从类的根路径下开始查找资源,配置文件需要放到类路径当中才行
  • url:这种方式是绝对路径方式,只需要提供绝对路径就行
  • class:全限定接口名,带有包名

作用机制如下:如果class指定的是mapper.CarMapper,那么它就会去mapper下查找CarMapper.xml文件

使用这种方法必须使得接口和xml文件在同一个目录下

<mappers>
    <!--前提是你的包下的文件和xml文件在同一级目录,并且名字是一致的-->
    <package name = "mapper"></package>
</mappers>

3.7 使用自动生成的主键

当在插入一条新的记录的时候,自动生成的主键,而这个主键需要在其他表中使用

插入一个用户数据的同时需要给该用户分配角色,需要将生成的用户的id插入到角色表的user_id字段上

<insert id = "insert" userGeneratedKeys="true" keyProperty="id">
    <!--用car的id属性接收这个生成的主键-->
    insert into t_car values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>

4. Myabtis参数处理

4.1 单个简单类型参数

简单类型包括有

  • byte short int long float double char
  • Byte Shrot Integer Long Float Double Character
  • String
  • java.util.Date
  • java.sql.Date

parameterType属性的作用:就是告诉mybatis,这个方法的参数类型是什么类型,但是其具有自动类型推断的机制,所以大部分情况下都是可以省略不写的,其本质是为ps.setXxx()服务的

mybatis框架实际上内置了很多别名

一个sql语句的完整写法是

<select id = "select" resultType = "student" >
    select * from t_student where name = #{name,java_type=String,jdbcType=VARCHAR}
</select>

4.2 Map参数

<select id = "select" resultType = "student" parameterType="map">
    select * from t_student where name = #{map集合的key}
</select>

4.3 实体类参数

如果传入的参数是实体类的话,那么就会自动调用getXxx()的方法获取实体类内部的属性值

<select id = "select" resultType = "student" parameterType="student">
    select * from t_student where name = #{name}
</select>

4.4 多参数

如果是多个参数,mybatis底层是这样做的:

框架会自动创建一个map集合,并且map集合是以这样的方式存储参数的

map.put("arg0",name);
map.put("arg1",sex);
//或者是
map.put("param0",name);
map.put("param1",sex);

4.5 @Param注解(命名参数)

List<Student> selectByNameAndSex(@Param("name") String name,@Param("sex") Character sex);

当你加上这个@Param的参数之后,这时候arg0arg1…的对应参数都被替换掉了

4.6 @Param源代码

public class MapperProxy<T> implements InvocationHandler, Serializable {//jdk动态代理
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
        } catch (Throwable var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    }
}
  • proxy:代理对象
  • method:目标方法
  • args[]:目标方法的参数

当执行mapper的方法的时候,这时候就会将值传递给args

@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
  return mapperMethod.execute(sqlSession, args);
}
public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {//获取sql语句的类型
    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 {
        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;
}
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
  List<E> result;
  //将args转换为sql的命令
  Object param = method.convertArgsToSqlCommandParam(args);
  if (method.hasRowBounds()) {
    RowBounds rowBounds = method.extractRowBounds(args);
    result = sqlSession.selectList(command.getName(), param, rowBounds);
  } else {
    result = sqlSession.selectList(command.getName(), param);
  }
  // issue #510 Collections & arrays support
  if (!method.getReturnType().isAssignableFrom(result.getClass())) {
    if (method.getReturnType().isArray()) {
      return convertToArray(result);
    } else {
      return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
    }
  }
  return result;
}
public Object getNamedParams(Object[] args) {
  final int paramCount = names.size();
  if (args == null || paramCount == 0) {
    return null;
  } else if (!hasParamAnnotation && paramCount == 1) {//有没有注解?如果没有注解而且是单个参数
    Object value = args[names.firstKey()];
    return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
  } else {//否则的话就是有注解的,那么就走这个分支
    final Map<String, Object> param = new ParamMap<>();
    int i = 0;
    for (Map.Entry<Integer, String> entry : names.entrySet()) {//遍历0->paramName的集合拿出来
      param.put(entry.getValue(), args[entry.getKey()]);//名字->args[i]:参数的值
      // add generic param names (param1, param2, ...)
      final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);//加param->args[i]
      // ensure not to overwrite parameter named with @Param
      //标准通用参数名
      if (!names.containsValue(genericParamName)) {
        param.put(genericParamName, args[entry.getKey()]);
      }
      i++;
    }
    return param;
  }
}  

5. Mybatis返回值处理

5.1 可以返回map

如果编写的Java类中没有属性能够与返回值一一对应,而且确定只返回一条数据,那么就可以用map进行存储

5.2 返回Map列表

当有多条数据的时候,可以使用List<Map>进行返回值的接收

5.3 返回大Map

可以设计一种Map<String,Map>,可以让StringKey存储id即可

5.4 结果映射

为了使得返回的结果中的字段名和类的属性名能够对得上,可以:

  • 用as起别名
  • resultMap进行结果集的映射
  • 开启驼峰命名自动映射
<!--使用resultMap-->
<!--指定数据库表的字段名和Java类的属性名的对应关系-->
<!--type:指定pojo类的类名-->
<!--id:指定resultMap的唯一标识,在select标签中使用-->
<resultMap id="StudentResult" type="domain.Student">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
</resultMap>

<select id="selectOne" resultMap="StudentResult">
    select * from student where id = #{id};
</select>
<settings>
    <setting name="logImpl" value="SLF4J"/>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

6. 动态SQL

6.1 if标签

<!--如果为true,那么就把里面的内容拼接到sql里面去-->
<!--注意:里面的参数也是从函数签名的定义中取的-->
<!--因此如果你用paramsX也是可以取到相关的值的-->
<!--如果是传过来一个pojo,那么就只能够使用属性名了-->
<!--如果使用了@Parmas("name"),那么可以使用里面指定的参数名-->
<!--&&符号可以被写成为and-->
<if test="brand!=null and name !=null">
    brand like "%"#{brand}"%" and name like "%"#{param1}"%"
</if>

6.2 where标签

where标签是编写动态sql的核心:

  • 当所有的条件都为空的时候,where标签保证不会生成where子句
  • 自动去除某些条件前面多余的and 或者or
  • 不会去除条件后面多余的and或者or

6.3 trim标签

它是用来在标签的前面或者后面添加内容的

  • prefix:在trim标签中的语句前添加内容
  • suffix:在trim标签中的语句后添加内容
  • prefixOverrides:前缀覆盖掉(去掉)
  • suffixOverrides:后缀覆盖掉(去掉)
    • 将trim标签中内容的后缀或者前缀给去掉
<trim prefix="where" suffixOverrides="and|or"></trim>

6.4 set标签

主要用在update语句当中,用来生成关键字,同时去掉最后面多余的,

比如只更新提交不为空的字段,如果提交的字段为空或者是””,那么就不更新了

6.5 choose when otherwise

仅有一个分支会被走

<chose>
    <when></when>
    <when></when>
    <when></when>
    <when></when>
    <otherwise></otherwise>
</chose>

6.6 批量删除

<delete id = "delete">
    delete from t_car where id in(
    <!--collection:指定数组或者集合,写array或者argX,或者使用@Params中你所指定的名字-->
    <!--item:代表数组或者集合中的元素-->
    <!--separator:循环中的分割符-->
    <!--开始符和结束符可以用open和close来指定-->
    <foreach collection="ids" item="id" separator="," open = "(" close=")">
        #{id}
    </foreach>
    )
</delete>

6.7 批量插入

<insert id="insertBatch">
    insert into t_car values(id,name,type)
    <foreach collections="cars" item="car" separator="," >
        (#{car.id},#{car.name},#{car.type})
    </foreach>
</insert>

6.8 sql标签与include

sql标签是用来声明sql的片段的,include标签用来将声明的sql片段包含到某个sql语句当中

作用:代码复用,容易维护

<sql id = "carCol">
    id,
    car_num as carNum
</sql>
<select id ="select" resultType="car">
    select
    <include refid = "carCol"/>
    from t_car where id = #{id}
</select>

文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录