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层的结构更加清晰简洁
-
#{}和${}的区别是什么?
-
#将传入的数据当成一个字符串,会对自动传入的数据加一个双引号。例如
order by #id#,如果传入的值是111,那么解析成sql时的值变为order by "111",如果传入的值是id,在解析成sql为order by "id"
其实原sql语句通常写成 order by #{id} 与order by #id#的效果一样
-
$一般用于传入数据库对象,例如传入表名
一般能用#就别用$
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 形式的配置