• 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 形式的配置

  • 相关阅读:
    【机器学习】关联规则挖掘(二):频繁模式树FP-growth
    【机器学习】关联规则分析(一):Apriori
    【机器学习】聚类算法——K均值算法(k-means)
    【机器学习】分类器组合——AdaBoost
    tensorflow 中 Cross Entropy算法理解
    修改文件夹中的文件名
    poj 2635
    噪音样本
    流量录制回放助力接口自动化测试
    git
  • 原文地址:https://www.cnblogs.com/king-brook/p/9995808.html
Copyright © 2020-2023  润新知