1.什么是Mybatis?
(1)mybatis是一个跟数据库交互的持久层框架,是基于JDBC实现的,是对JDBC的封装。当我们需要使用mybatis持久层框架时,需要在使用端创建两个核心配置文件如下,mybatis在启动时会加载这两个文件,通过xml解析配置初始化相关的信息。大概总体是实现两个步骤,一个初始化数据库信息并封装成java对象到内存中,一个是初始化sql信息并封装成java对象到内存中。
- sqlMapConfig.xml 是用于存放数据库相关的配置信息
- 2.Mapper.xml 是用于存放sql信息
(2)作为一个半ORM框架,MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。称Mybatis是半自动ORM映射工具,是因为在查询关联对象或关联集合对象时,需要手动编写sql来完成。不像Hibernate这种全自动ORM映射工具,Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取。
(3)通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。
(4)由于MyBatis专注于SQL本身,灵活度高,所以比较适合对性能的要求很高,或者需求变化较多的项目,如互联网项目。
2、Mybaits的优缺点:
(1)优点:
① 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
② 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
③ 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
④ 能够与Spring很好的集成;
⑤ 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。
(2)缺点:
① SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
② SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
3、#{}和${}的区别是什么?
${}是字符串替换,#{}是预处理;
Mybatis在处理${}时,就是把${}直接替换成变量的值。而Mybatis在处理#{}时,会对sql语句进行预处理,将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;使用#{}可以有效的防止SQL注入,提高系统安全性,之所以能够防止SQL的注入是因为MyBatis启用了预编译功能,在SQL执行前,会先将#{}替换为 ?的SQL发送给数据库进行编译;执行时,直接使用编译好的SQL,替换占位符“?”就可以了。因为SQL注入只能对编译过程起作用,所以这样的方式就很好地避免了SQL注入的问题.
mybatis在解析xml中的sql时把#{}使用?占位符替换,然后#{}里的值放到的有序的集合中(list),最后通过反射把这些值拿到设置到sql的占位符?中。在mybatis的sqlmap文件的sql语句使用#{}的方式的占位符,而不直接使用?的方式的占位符是因为我们需要使用{}里的值来区别具体设置哪个参数值.
4、通常一个mapper.xml文件,都会对应一个Dao接口,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗
Mapper 接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,根据类的全限定名+方法名,唯一定位到一个MapperStatement并调用执行器执行所代表的sql,然后将sql执行结果返回。Mapper接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。Dao接口即Mapper接口。接口的全限名,就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中,每一个SQL标签,比如、select、insert、update标签,都会被解析为一个MapperStatement对象。
5、Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复?
不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;原因就是namespace+id是作为Map的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。
6.简述Mybatis的插件运行原理,以及如何编写一个插件。
Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件
自定义插件举例:
Intercepts ({//注意看这个?花括号,也就这说这?可以定义多个@Signature对多个地?拦截,都?
这个拦截器
@Signature (type = StatementHandler .class , //这是指拦截哪个接?method = "prepare",//这个接?内的哪个?法名,不要拼错了args = { Connection.class, Integer .class}), 这是拦截的?法的?参,按
顺序写到这,不要多也不要少,如果?法重载,可是要通过?法名和?参来确定唯?的
})
public class MyPlugin implements Interceptor {private final Logger logger = LoggerFactory.getLogger(this.getClass());// //这?是每次执?操作的时候,都会进?这个拦截器的?法内Overridepublic Object intercept(Invocation invocation) throws Throwable {//增强逻辑System.out.println("对?法进?了增强....");return invocation.proceed(); //执?原?法
}
* //主要是为了把这个拦截器?成?个代理放到拦截器链中* ^Description包装?标对象 为?标对象创建代理对象* @Param target为要拦截的对象* @Return代理对象*/Overridepublic Object plugin(Object target) {System.out.println("将要包装的?标对象:"+target);return Plugin.wrap(target,this);}/**获取配置?件的属性**///插件初始化的时候调?,也只调??次,插件配置的属性从这?设置进来Overridepublic void setProperties(Properties properties) {System.out.println("插件配置的初始化参数:"+properties );} }
配置文件配置
<plugins><plugin interceptor="com.lagou.plugin.MySqlPagingPlugin"><!--配置参数--><property name="name" value="Bob"/></plugin>
</plugins>
7.Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理,一般是在关联查询时才会需要延迟加载。
<!-- 开启?对多 延迟加载 -->
<resultMap id="userMap" type="user"><id column="id" property="id"></id><result column="username" property="username"></result><result column="password" property="password"></result><result column="birthday" property="birthday"></result><!--fetchType="lazy" 懒加载策略fetchType="eager" ?即加载策略--><collection property="orderList" ofType="order" column="id" select="com.lagou.dao.OrderMapper.findByUid" fetchType="lazy"></collection>
</resultMap> <select id="findAll" resultMap="userMap">SELECT * FROM `user`
</select>
在Mybatis的核?配置?件中可以使?setting标签修改全局的加载策略:
<settings><!--开启全局延迟加载功能--><setting name="lazyLoadingEnabled" value="true"/>
</settings>
8.Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不?
Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能,Mybatis提供了9种动态sql标trim|where|set|foreach|if|choose|when|otherwise|bind。其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。
9Mybatis都有哪些Executor执行器
Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象,默认一般使用的是SimpleExecutor执行器。
10..mybatis的分页
mybatis的分页分两大类:物理分页和逻辑分页(内存分页),mybatis默认是使用内存分页。
1.物理分页 :物理分页依赖的是某一物理实体,这个物理实体就是数据库,比如MySQL数据库提供了limit关键字,程序员只需要编写带有limit关键字的SQL语句,数据库返回的就是分页结果。也可以使用分页插件来完成物理分页。分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根 据 dialect 方言,添加对应的物理分页语句和物理分页参数。
2.逻辑分页(内存分页):逻辑分页依赖的是程序员编写的代码。数据库返回的不是分页结果,而是全部数据,然后再由程序员通过代码获取分页数据,常用的操作是一次性从数据库中查询出全部数据并存储到List集合中,因为List集合有序,再根据索引获取指定范围的数据。
两者的对比
1.数据库负担
物理分页每次都访问数据库,逻辑分页只访问一次数据库,物理分页对数据库造成的负担大。
2.服务器负担
逻辑分页一次性将数据读取到内存,占用了较大的内容空间,物理分页每次只读取一部分数据,占用内存空间较小。
3.实时性
逻辑分页一次性将数据读取到内存,数据发生改变,数据库的最新状态不能实时反映到操作中,实时性差。物理分页每次需要数据时都访问数据库,能够获取数据库的最新状态,实时性强。
4.适用场合
逻辑分页主要用于数据量不大、数据稳定的场合,物理分页主要用于数据量较大、更新频繁的场合。
MyBatis使用RowBounds实现的分页是逻辑分页,也就是先把数据记录全部查询出来,然在再根据offset和limit截断记录返回(数据量大的时候会造成内存溢出),不过可以用插件或其他方式能达到物理分页效果。
11、Mybatis 的一级、二级缓存:
一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session, 当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
当打开一个sqlSeesion会话执行操作时,按以下步骤执行
1、第?次发起查询?户id为1的?户信息,先去找缓存中是否有id为1的?户信息,如果没有,从数据库查询?户信息。得到?户信息,将?户信息存储到?级缓存中。
2、 如果中间sqlSession去执?commit操作(执?插?、更新、删除),则会清空SqlSession中的?级缓存,这样做的?的为了让缓存中存储的是最新的信息,避免脏读。
3、 第?次发起查询?户id为1的?户信息,先去找缓存中是否有id为1的?户信息,缓存中有,直接从 缓存中获取用户信息
一级缓存底层是用HASHMAP去储存的。既然是HASHMAP,那么就会是key,valuec形式的,sqlSeeion执行一条查询语句时,首先会调用一个createCacheKey方法创建一个CachKey对象作为这个HASHMAP的key。这个缓存HashMap中的key主要是由namespaceID,需要执行的sql,分页参数,以及数据库环境ID等组成
一级缓存的级别:
mybatis一级缓存有两种:一种是SESSION级别的,针对同一个会话SqlSession中,执行多次条件完全相同的同一个sql,那么会共享这一缓存,默认是SESSION级别的缓存;一种是STATEMENT级别的,缓存只针对当前执行的这一statement有效???????
设置一级缓存的级别
<settings><setting name="localCacheScope" value="STATEMENT"/>
</settings>
二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源, 如 Ehcache。 默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现 Serializable 序列化接口(可用来保存对象的状态),因为?级缓存数据存储介质多种多样,不?定只存在内存中,有可能存在硬盘中,如果我们要再取这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接?。可在它的映射文件中配置<cache/> ;对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存 Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。
在实际开发中,mybatis是和spring一起整合使用的,Spring将事务放到servcie中管理,对于每一个service中的sqlSession是不同的,这是通过mybatis-spring中的org.mybatis.spring.mapper.MapperScannerConfigurer创建sqlsession自动注入到service中的。每次查询之后都要关闭sqlSession,关闭之后数据被清空了。所以spring整合后,若果没有事务,一级缓存就没有任务意义。
二级缓存在多表关联查询时容易出现脏读,由于是因为二级缓存是mapper级别,在这个mapper查询出来的数据缓存后,再另一个mapper做了更新就会出现脏读。一般项目都不会开启二级缓存,但是可以用如下解决思路
-
在 Mapper1 定义时,手动配置 相应的关联 Mapper2
-
在 Mapper1 缓存 cache1 实例化时,读取 所关联的 Mapper2 的缓存 cache2相关信息
-
在 cache1 中存储 cache2 的引用信息
-
cache1 执行clear时,同步操作 cache2 执行clear
解决实现例子:
主要用到自定义注解CacheRelations,自定义缓存实现RelativeCache和缓存上下文RelativeCacheContext。
注解CacheRelations,使用时需标注在对应mapper上
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheRelations {// from中mapper class对应的缓存更新时,需要更新当前注解标注mapper的缓存Class<?>[] from() default {};// 当前注解标注mapper的缓存更新时,需要更新to中mapper class对应的缓存Class<?>[] to() default {};
}
自定义缓存RelativeCache实现 MyBatis Cache 接口
public class RelativeCache implements Cache {private Map<Object, Object> CACHE_MAP = new ConcurrentHashMap<>();private List<RelativeCache> relations = new ArrayList<>();private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);private String id;private Class<?> mapperClass;private boolean clearing;public RelativeCache(String id) throws Exception {this.id = id;this.mapperClass = Class.forName(id);RelativeCacheContext.putCache(mapperClass, this);loadRelations();}@Overridepublic String getId() {return id;}@Overridepublic void putObject(Object key, Object value) {CACHE_MAP.put(key, value);}@Overridepublic Object getObject(Object key) {return CACHE_MAP.get(key);}@Overridepublic Object removeObject(Object key) {return CACHE_MAP.remove(key);}@Overridepublic void clear() {ReadWriteLock readWriteLock = getReadWriteLock();Lock lock = readWriteLock.writeLock();lock.lock();try {// 判断 当前缓存是否正在清空,如果正在清空,取消本次操作// 避免缓存出现 循环 relation,造成递归无终止,调用栈溢出if (clearing) {return;}clearing = true;try {CACHE_MAP.clear();relations.forEach(RelativeCache::clear);} finally {clearing = false;}} finally {lock.unlock();}}@Overridepublic int getSize() {return CACHE_MAP.size();}@Overridepublic ReadWriteLock getReadWriteLock() {return readWriteLock;}public void addRelation(RelativeCache relation) {if (relations.contains(relation)){return;}relations.add(relation);}void loadRelations() {// 加载 其他缓存更新时 需要更新此缓存的 caches// 将 此缓存 加入至这些 caches 的 relations 中List<RelativeCache> to = UN_LOAD_TO_RELATIVE_CACHES_MAP.get(mapperClass);if (to != null) {to.forEach(relativeCache -> this.addRelation(relativeCache));}// 加载 此缓存更新时 需要更新的一些缓存 caches// 将这些缓存 caches 加入 至 此缓存 relations 中List<RelativeCache> from = UN_LOAD_FROM_RELATIVE_CACHES_MAP.get(mapperClass);if (from != null) {from.forEach(relativeCache -> relativeCache.addRelation(this));}CacheRelations annotation = AnnotationUtils.findAnnotation(mapperClass, CacheRelations.class);if (annotation == null) {return;}Class<?>[] toMappers = annotation.to();Class<?>[] fromMappers = annotation.from();if (toMappers != null && toMappers.length > 0) {for (Class c : toMappers) {RelativeCache relativeCache = MAPPER_CACHE_MAP.get(c);if (relativeCache != null) {// 将找到的缓存添加到当前缓存的relations中this.addRelation(relativeCache);} else {// 如果找不到 to cache,证明to cache还未加载,这时需将对应关系存放到 UN_LOAD_FROM_RELATIVE_CACHES_MAP// 也就是说 c 对应的 cache 需要 在 当前缓存更新时 进行更新List<RelativeCache> relativeCaches = UN_LOAD_FROM_RELATIVE_CACHES_MAP.putIfAbsent(c, new ArrayList<RelativeCache>());relativeCaches.add(this);}}}if (fromMappers != null && fromMappers.length > 0) {for (Class c : fromMappers) {RelativeCache relativeCache = MAPPER_CACHE_MAP.get(c);if (relativeCache != null) {// 将找到的缓存添加到当前缓存的relations中relativeCache.addRelation(this);} else {// 如果找不到 from cache,证明from cache还未加载,这时需将对应关系存放到 UN_LOAD_TO_RELATIVE_CACHES_MAP// 也就是说 c 对应的 cache 更新时需要更新当前缓存List<RelativeCache> relativeCaches = UN_LOAD_TO_RELATIVE_CACHES_MAP.putIfAbsent(c, new ArrayList<RelativeCache>());relativeCaches.add(this);}}}}}
缓存上下文RelativeCacheContext
public class RelativeCacheContext {// 存储全量缓存的映射关系public static final Map<Class<?>, RelativeCache> MAPPER_CACHE_MAP = new ConcurrentHashMap<>();// 存储 Mapper 对应缓存 需要to更新缓存,但是此时 Mapper 对应缓存还未加载// 也就是 Class<?> 对应的缓存更新时,需要更新 List<RelativeCache> 中的缓存public static final Map<Class<?>, List<RelativeCache>> UN_LOAD_TO_RELATIVE_CACHES_MAP = new ConcurrentHashMap<>();// 存储 Mapper 对应缓存 需要from更新缓存,但是在 加载 Mapper 缓存时,这些缓存还未加载// 也就是 List<RelativeCache> 中的缓存更新时,需要更新 Class<?> 对应的缓存public static final Map<Class<?>, List<RelativeCache>> UN_LOAD_FROM_RELATIVE_CACHES_MAP = new ConcurrentHashMap<>();public static void putCache(Class<?> clazz, RelativeCache cache) {MAPPER_CACHE_MAP.put(clazz, cache);}public static void getCache(Class<?> clazz) {MAPPER_CACHE_MAP.get(clazz);}}
UserMapper.java
@Repository
@CacheNamespace(implementation = RelativeCache.class, eviction = RelativeCache.class, flushInterval = 30 * 60 * 1000)
@CacheRelations(from = OrganizationMapper.class)
public interface UserMapper extends BaseMapper<UserEntity> {UserInfo queryUserInfo(@Param("userId") String userId);
}
UserMapper.xml
<mapper namespace="com.mars.system.dao.UserMapper"><cache-ref namespace="com.mars.system.dao.UserMapper"/><select id="queryUserInfo" resultType="com.mars.system.model.UserInfo">select u.*, o.name org_name from user u left join organization o on u.org_id = o.idwhere u.id = #{userId}</select>
</mapper>
@Repository
@CacheNamespace(implementation = RelativeCache.class, eviction = RelativeCache.class, flushInterval = 30 * 60 * 1000)
public interface OrganizationMapper extends BaseMapper<OrganizationEntity> {
}
CacheNamespace中flushInterval 在默认情况下是无效的,也就是说缓存并不会定时清理。ScheduledCache是对flushInterval 功能的实现,MyBatis 的缓存体系是用装饰器进行功能扩展的,所以,如果需要定时刷新,需要使用ScheduledCache给到 RelativeCache添加装饰。
至此,配置和编码完成。
12、Mapper 编写有哪几种方式
第一种:接口实现类继承 SqlSessionDaoSupport:使用此种方法需要编写 mapper 接口,mapper 接口实现类、mapper.xml 文件。