JAVA常用知识总结(六)——Mybatis

  • 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里?

  Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。

  • 物理分页和逻辑分页的区别?

   mybatis自带的分页RowBounds;//逻辑分页

Java:   
        RowBounds rb=new RowBounds(offset, limit);  //offset(从多少条开始);limit(获取多少条)

        SqlSession sqlSession=sqlSessionFactory.openSession();//通过读取mybatis配置文件的输入流然后通过new SqlSeesionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));最终得到SqlSessionFactory

         List<Student> studentlist=sqlSession.selectList("xx.xx.Mapper.findStudent",null,rb);//第一个参数为具体Mapper文件的下的findStudent的ID,第二个参数为提供的条件参数,第三个参数为我们要进行对获取学生进行分页

         sqlSession.close();

         return studentlist;
Mapper:   

        <select id="findStudent" resultType="Student">

            select * from Student

         </select>

备注:通过以上例子,很明显的看出,在分页的时候,我们是把所有的数据都查询出来,然后通过RowBounds进行在内存分页.通过源码查看,也是通过ResuleSet结果集进行分页;

  mybatis自写sql或者通过分页插件PageHelper:   //物理分页


 Java

        List<Student> findStudent();

        Service层:

           PageHelper.startPage(pageNum,pageSize);//pageNum 页数  pageSize 数量

           List<Student> stu=studentDao.findStudent();  //studentDao @Autowried注解获取; 在执行查询数据时,就会自动执行2个sql;执行上述Mapper下的ID为findStudent的sql 自动执行分页,通过PageHelper进行识别是何数据库拼接分页语句,若是mysql,自动通过limit分页,若是oracle自动通过rownum进行分页,另一个会自动拼接Mapper下不存在的ID为findStudent_COUNT,查询的总数;可以通过打印的日志进行跟踪;

           PageInfo<Student> page = new PageInfo<Student>(stu); //自动封装总数count以及分页,数据返回页面

           return page;//返回分页之后的数据

 Mapper:

            <select id="findStudent" resultType="Student">

                select * from Student

           </select>

 备注:查看如上例子代码,我们就发现了是直接通过SQL进行在数据库中直接分页,得到的数据就是我们想要分页之后的数据,就是物理分页;

总结:

    1:逻辑分页 内存开销比较大,在数据量比较小的情况下效率比物理分页高;在数据量很大的情况下,内存开销过大,容易内存溢出,不建议使用

    2:物理分页 内存开销比较小,在数据量比较小的情况下效率比逻辑分页还是低,在数据量很大的情况下,建议使用物理分页

  • Mybatis是如何进行分页的?分页插件的原理是什么?

  在使用Java Spring开发的时候,Mybatis算是对数据库操作的利器了。不过在处理分页的时候,Mybatis并没有什么特别的方法,一般需要自己去写limit子句实现,成本较高。好在有个PageHelper插件。

1 List<Country> list;
2 if(param1 != null){
3   PageHelper.startPage(1, 10);   //如果放外面就会导致 PageHelper 生产了一个分页参数,但是没有被消费,只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。如果代码在进入 Executor 前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到 MappedStatement 时), 当前写的这种情况由于线程不可用,也不会导致 ThreadLocal 参数被错误的使用。
4   list = countryMapper.selectIf(param1);
5 } else {
6   list = new ArrayList<Country>();
7 }

分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。

  eg:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10

  • MyBatis自定义插件(Plugin)详解? 

1:实现类SQLStatsInterceptor实现接口Interceptor

 1 import java.sql.Connection;
 2 import java.util.Properties;
 3 
 4 import org.apache.ibatis.executor.statement.StatementHandler;
 5 import org.apache.ibatis.mapping.BoundSql;
 6 import org.apache.ibatis.plugin.Interceptor;
 7 import org.apache.ibatis.plugin.Intercepts;
 8 import org.apache.ibatis.plugin.Invocation;
 9 import org.apache.ibatis.plugin.Plugin;
10 import org.apache.ibatis.plugin.Signature;
11 import org.slf4j.Logger;
12 import org.slf4j.LoggerFactory;
13 
14 @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class}) })
15 public class SQLStatusInterceptor implements Interceptor {
16     private final Logger logger = LoggerFactory.getLogger(this.getClass());
17 
18     @Override
19     public Object intercept(Invocation invocation) throws Throwable {
20         StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
21         BoundSql boundSql = statementHandler.getBoundSql();
22         String sql = boundSql.getSql();
23         logger.info("mybatis intercept sql:", sql);
24         return invocation.proceed();
25     }
26 
27     @Override
28     public Object plugin(Object target) {
29         return Plugin.wrap(target, this);
30     }
31 
32     @Override
33     public void setProperties(Properties properties) {
34         String dialect = properties.getProperty("dialect");
35         logger.info("mybatis intercept dialect:", dialect);
36     }
37 }

代码分析:

1)首先SQLStatsInterceptor类实现了接口Interceptor;

2)需要重写3个方法,核心的拦截处理方法是intercept,在这个方法中可以获取到对应的绑定的sql,在这里作为演示只是打印了SQL,如果需要可以保存起来。

3)在方法上有一个很重要的注解@Intercepts,在此注解上配置的注解说明了要拦截的类(type=StatementHandler.class),拦截的方法(method="prepare"),方法中的参数args={Connection.class}),也就是此拦截器会拦截StatementHandler类中的如下方法:

 1 public interface StatementHandler {
 2 
 3   Statement prepare(Connection connection)
 4       throws SQLException;
 5 
 6   void parameterize(Statement statement)
 7       throws SQLException;
 8 
 9   void batch(Statement statement)
10       throws SQLException;
11 
12   int update(Statement statement)
13       throws SQLException;
14 
15   <E> List<E> query(Statement statement, ResultHandler resultHandler)
16       throws SQLException;
17 
18   BoundSql getBoundSql();
19 
20   ParameterHandler getParameterHandler();
21 
22 }

不光可以拦截StatementHandler,总共可以拦截的类有:

StatementHandler (prepare, parameterize, batch, update, query)

ResultSetHandler (handleResultSets, handleOutputParameters)

ParameterHandler (getParameterObject, setParameters)

Executor (update, query, flushStatements, commit, rollback,getTransaction, close, isClosed)

2:这样一个插件就开发完成了,接下来需要在 mybatis-config.xml 文件中增加 plugins节点,完整配置如下:

<configuration>
    <!-- 插件管理 -->
       <plugins>
         <plugin interceptor="com.xx.common.interceptor.SQLStatusInterceptor">
            <property name="dialect" value="mysql" />
        </plugin>
       </plugins>

</configuration>

到这里就可以测试使用了,Mybatis插件实现了拦截器的功能。一旦懂了自定义Plugin后,那么对于PageHelper的实现也就明白了七八分了。

  • TKMybatis的原理?

tkmybatis是在mybatis框架的基础上提供了很多工具,让开发更加高效

数据源的配置,只需要将org.mybatis.spring.mapper.MapperScannerConfigurer改成tk.mybatis.spring.mapper.MapperScannerConfigurer

    <bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.cnten.**.dao"/>
        <property name="sqlSessionFactoryBeanName" value="masterDbSqlSessionFactory" /> //mapper接口可以直接调用
        <property name="markerInterface" value="tk.mybatis.mapper.common.Mapper"/>
    </bean>  

  此框架为我们实现这些功能所有的改动都在Mapper层面,所有的Mapper都继承了tk.mybatis.mapper.common.Mapper

public interface TemplateMapper extends Mapper<Template>{
    
}

Mapper接口的声明如下,可以看到Mapper接口实现了所有常用的方法

public interface Mapper<T> extends
        BaseMapper<T>,
        ExampleMapper<T>,
        RowBoundsMapper<T>,
        Marker {

}

看一下完整的UML图,太大了,可以用新窗口打开,放大之后再看 

这里选择一个接口UpdateByPrimaryKeyMapper 对源码进行分析:

public interface UpdateByPrimaryKeyMapper<T> {

    @UpdateProvider(type = BaseUpdateProvider.class, method = "dynamicSQL")
    int updateByPrimaryKey(T record);

}

注解中的参数:

    type参数指定的Class类,必须要能够通过无参的构造函数来初始化;

    method参数指定的方法,必须是public的,返回值必须为String,可以为static。

Mybatis3中增加了使用注解来配置Mapper的新特性,有@SelectProvider、@UpdateProvider、@InsertProvider和@DeleteProvider 用于灵活的设置sql来源,这里设置了服务提供类和方法,但这个库并没有直接用method指定的方法来返回sql,而是在运行时进行解析的,代码如下

public class BaseUpdateProvider extends MapperTemplate {

    public BaseUpdateProvider(Class<?> mapperClass, MapperHelper mapperHelper) {
        super(mapperClass, mapperHelper);
    }

    public String updateByPrimaryKey(MappedStatement ms) {
        Class<?> entityClass = getEntityClass(ms);
        StringBuilder sql = new StringBuilder();
        sql.append(SqlHelper.updateTable(entityClass, tableName(entityClass)));
        sql.append(SqlHelper.updateSetColumns(entityClass, null, false, false));
        sql.append(SqlHelper.wherePKColumns(entityClass));
        return sql.toString();
    }
}

到这里我们就大概知道了这个库为我们提供便利的原理了,总的来说就是这个库帮我们提供了对表的基本操作的sql,帮我们省了很多工作量,而且维护起来也很方便,否则我们的xml文件动不动就几百行甚至上千行

对源码的探索不能到这里停止,最起码要分析到与另一个框架的整合点

我们知道,mybatis的mapper接口是在启动的时候被框架以JdkProxy的形式封装了的,具体对应的类是MapperFactoryBean,这个类中有一个checkDaoConfig()方法,是从父类继承并重写了该方法,继承结构如下

MapperFactoryBean -> SqlSessionDaoSupport -> DaoSupport
这里的DaoSupport就是spring提供的Dao的抽象,代码如下

public abstract class DaoSupport implements InitializingBean {

// spring 完成属性设置后会调用此方法
    @Override
    public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
        // 这里提供了接口供子类去实现
        checkDaoConfig();

        // Let concrete implementations initialize themselves.
        try {
            initDao();
        }
        catch (Exception ex) {
            throw new BeanInitializationException("Initialization of DAO failed", ex);
        }
    }

    protected abstract void checkDaoConfig() throws IllegalArgumentException;

    protected void initDao() throws Exception {
    }

}

框架自定义的MapperFactoryBean重写了checkDaoConfig()方法,完成对所有sql语句的设置,代码如下

@Override
    protected void checkDaoConfig() {
        super.checkDaoConfig();
        //通用Mapper
        if (mapperHelper.isExtendCommonMapper(getObjectType())) {
        //这里去处理该类所对应的MappedStatement,封装在helper类中处理
            mapperHelper.processConfiguration(getSqlSession().getConfiguration(), getObjectType());
        }
    }

MapperHelper的processConfiguration方法如下

public void processConfiguration(Configuration configuration, Class<?> mapperInterface) {
        String prefix;
        if (mapperInterface != null) {
            prefix = mapperInterface.getCanonicalName();
        } else {
            prefix = "";
        }
        for (Object object : new ArrayList<Object>(configuration.getMappedStatements())) {
            if (object instanceof MappedStatement) {
                MappedStatement ms = (MappedStatement) object;
                //检查这个MappedStatement是否属于此映射对象
                if (ms.getId().startsWith(prefix) && isMapperMethod(ms.getId())) {
                    if (ms.getSqlSource() instanceof ProviderSqlSource) {
                       //去设置该statement的sql语句
                        setSqlSource(ms);
                    }
                }
            }
        }
    }

设置sql的逻辑,提供了几种不同类型的sqlsource

/**
     * 重新设置SqlSource
     *
     * @param ms
     * @throws java.lang.reflect.InvocationTargetException
     * @throws IllegalAccessException
     */
    public void setSqlSource(MappedStatement ms) throws Exception {
        if (this.mapperClass == getMapperClass(ms.getId())) {
            throw new RuntimeException("请不要配置或扫描通用Mapper接口类:" + this.mapperClass);
        }
        Method method = methodMap.get(getMethodName(ms));
        try {
            //第一种,直接操作ms,不需要返回值
            if (method.getReturnType() == Void.TYPE) {
                method.invoke(this, ms);
            }
            //第二种,返回SqlNode
            else if (SqlNode.class.isAssignableFrom(method.getReturnType())) {
                SqlNode sqlNode = (SqlNode) method.invoke(this, ms);
                DynamicSqlSource dynamicSqlSource = new DynamicSqlSource(ms.getConfiguration(), sqlNode);
                setSqlSource(ms, dynamicSqlSource);
            }
            //第三种,返回xml形式的sql字符串
            else if (String.class.equals(method.getReturnType())) {
                String xmlSql = (String) method.invoke(this, ms);
                SqlSource sqlSource = createSqlSource(ms, xmlSql);
                //替换原有的SqlSource
                setSqlSource(ms, sqlSource);
            } else {
                throw new RuntimeException("自定义Mapper方法返回类型错误,可选的返回类型为void,SqlNode,String三种!");
            }
            //cache
            checkCache(ms);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e.getTargetException() != null ? e.getTargetException() : e);
        }
    }

到这里整个sql的获取流程就分析完了,可以节省开发的工作量,而且DAO层的结构更加清晰简洁

  • #{}和${}的区别是什么?

  1. #将传入的数据当成一个字符串,会对自动传入的数据加一个双引号。例如

    order by #id#,如果传入的值是111,那么解析成sql时的值变为order by "111",如果传入的值是id,在解析成sql为order by "id"

    其实原sql语句通常写成 order by #{id} 与order by #id#的效果一样

  2. $将传入的数据直接显示在sql语句中。例如 order by ${id},如果传入的值是9则解析成sql语句为order by 9
  3. #方式能够很大程度上防止sql注入,而$无法防止sql的注入,

      $一般用于传入数据库对象,例如传入表名

     一般能用#就别用$

    mybatis排序时使用order by动态参数时需要注意,使用$而不是#

  • Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签?

<resultMap>、<parameterMap>、<sql>、<include>、<selectKey>、<if>、<foreach>等

<?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.cnten.platform.common.expExcel.tpl.dao.ExcelTplMapper" >
    <!-- 库表和对象关系映射 -->
      <resultMap id="BaseResultMap" type="com.cnten.platform.common.expExcel.tpl.model.ExcelTpl" >
        <id column="TPL_ID"                 property="tplId"         jdbcType="VARCHAR" />
        <result column="TPL_CODE"             property="tplCode"         jdbcType="VARCHAR" />
        <result column="TPL_NAME"             property="tplName"         jdbcType="VARCHAR" />
        <result column="EXCEL_NAME"         property="excelName"     jdbcType="VARCHAR" />
        <result column="STATE"                 property="state"         jdbcType="INTEGER" />
      </resultMap>
      
      <!-- 用户基本字段 -->
      <sql id="Base_Column_List" >
        TPL_ID, TPL_CODE, TPL_NAME, EXCEL_NAME, STATE
      </sql>
      
      <!-- 根据模板编码 获取 相关信息 -->
      <select id="getExcelTplByCode" resultMap="BaseResultMap" parameterType="java.lang.String">
           select 
               <include refid="Base_Column_List" />
        from ct_excel_tpl
        where TPL_CODE = #{tplCode,jdbcType=VARCHAR }
      </select>
        
    <select id="getExcelTpl" resultMap="BaseResultMap" parameterType="java.lang.String">
        select 
               <include refid="Base_Column_List" />
        from ct_excel_tpl
        where TPL_ID = #{tplId,jdbcType=VARCHAR }
      </select>
      
      <select id="getAllExcelTpls" resultMap="BaseResultMap" parameterType="com.cnten.platform.common.expExcel.tpl.model.ExcelTpl">
        select 
               <include refid="Base_Column_List" />
        from ct_excel_tpl
        where 1 = 1
       <if test="tplCode != null and tplCode != ''">
            and TPL_CODE like "%"#{tplCode,jdbcType=VARCHAR}"%"
        </if>
        <if test="tplName != null and tplName != ''" >
            and TPL_NAME like "%"#{tplName,jdbcType=VARCHAR}"%"
        </if>
      </select>
       
      <delete id="batchDeleteExcelTpls" parameterType="java.util.List">
        delete from ct_excel_tpl where TPL_ID in
        <foreach collection="list" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
       </delete>
       
      <insert id="insert" parameterType="com.cnten.platform.common.expExcel.tpl.model.ExcelTpl" >
        insert into ct_excel_tpl (
         TPL_ID, TPL_CODE, TPL_NAME, EXCEL_NAME, STATE
        )values (#{tplId,jdbcType=VARCHAR }, 
              #{tplCode,jdbcType=VARCHAR },
              #{tplName,jdbcType=VARCHAR },
              #{excelName,jdbcType=VARCHAR },
              #{state,jdbcType=INTEGER }
        )
      </insert>
      
      <update id="updateByPrimaryKey" parameterType="com.cnten.platform.common.expExcel.tpl.model.ExcelTpl" >
        update ct_excel_tpl
        set 
              TPL_CODE = #{tplCode,jdbcType=VARCHAR },
              TPL_NAME = #{tplName,jdbcType=VARCHAR },
              EXCEL_NAME = #{excelName,jdbcType=VARCHAR },
              STATE = #{state,jdbcType=VARCHAR }
        where TPL_ID = #{tplId,jdbcType=VARCHAR }
      </update>
</mapper>
  • 最佳实践中,通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?

  Dao接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名,就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement,举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。在Mybatis中,每一个<select>、<insert>、<update>、<delete>标签,都会被解析为一个MappedStatement对象。

Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。

Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。

 1 public interface ExcelTplMapper {   
 2 
 3     List<ExcelTpl> getAllExcelTpls(ExcelTpl excelTpl);
 4 
 5     int insert(ExcelTpl excelTpl);
 6     
 7     int updateByPrimaryKey(ExcelTpl excelTpl); 
 8     
 9     void batchDeleteExcelTpls(List<String> ids);
10 
11     ExcelTpl getExcelTpl(String tplId);
12     
13     ExcelTpl getExcelTplByCode(String tplCode);
14 }
  • mybatis几个新特性?

从3.4.0开始,mybatis提供对外部表的alias引用方法,多表联合查询就方便多了,我们先看原始的方式是怎样做的

select a.id,a.name,b.bid,b.bname .....
from user a 
left join room b

新特性

select id="selectUsers" resultType="map">
  select
    <include refid="user_col_sql_id"><property name="alias" value="t1"/>,
    <include refid="room_col_sql_id"><property name="alias" value="t2"/>
  from user t1
    left join room t2
</select>

 


注释一:这里需要看mybatis的版本,3.4.0之后的分页为 args = { Connection.class,Integer.class},具体看 StatementHandler接口的参数的写法;

注释二:本例是基于SpringMVC的配置型写法,在SpringBoot中,可以定义配置类进行注入

 1 import java.util.Properties;
 2 
 3 import org.springframework.context.annotation.Bean;
 4 import org.springframework.context.annotation.Configuration;
 5 
 6 @Configuration
 7 public class MyBatisConfiguration {
 8     @Bean
 9     public SQLStatusInterceptor sqlRecordInterceptor() {
10         SQLStatusInterceptor sqlRecordInterceptor = new SQLStatusInterceptor();
11         Properties properties = new Properties();
12         properties.setProperty("dialect","mysql");
13         sqlRecordInterceptor.setProperties(properties);
14         return sqlRecordInterceptor;
15     }
16 }

 注释三:

basePackage 属性是让你为映射器接口文件设置基本的包路径。 你可以使用分号或逗号 作为分隔符设置多于一个的包路径。每个映射器将会在指定的包路径中递归地被搜索到。

MapperScannerConfigurer 属性不支持使用了 PropertyPlaceholderConfigurer 的属 性替换,因为会在 Spring 其中之前来它加载。但是,你可以使用 PropertiesFactoryBean 和 SpEL 表达式来作为替代。

如果你使 用了一个 以上的 DataSource ,那 么SqlSessionFactory 自动 装配可 能会失效 。这种 情况下 ,你可 以使用 sqlSessionFactoryBeanName 或 sqlSessionTemplateBeanName 属性来设置正确的 bean 名 称来使用。这就是它如何来配置的,注意 bean 的名称是必须的,而不是 bean 的引用,因 此,value 属性在这里替代通常的 ref:

说到mappper接口可以直接调用的原因,接下来就是上文提到的:

MapperFactoryBean

为了代替手工使用 SqlSessionDaoSupport 或 SqlSessionTemplate 编写数据访问对象 (DAO)的代码,MyBatis-Spring 提供了一个动态代理的实现:MapperFactoryBean。

这个类 可以让你直接注入数据映射器接口到你的 service 层 bean 中。当使用映射器时,你仅仅如调 用你的 DAO 一样调用它们就可以了,但是你不需要编写任何 DAO 实现的代码,因为 MyBatis-Spring 将会为你创建代理。
上面的配置有一个很大的缺点,就是系统有很多的配置文件时 全部需要手动编写,所以上述的方式替换成MapperScannerConfigurer 形式的配置

posted @ 2018-11-21 16:19  用心记录每一天  阅读(569)  评论(0编辑  收藏  举报