什么是 Mybatis?
Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发只需要关注 SQL 语句本身,不用去处理加载驱动、创建连接、创建Statement等繁杂的过程。
Mybatis 动态 sql 是做什么的?
1)Mybatis动态sql可以让我们在Xml映射文件中,以标签形式编写动态sql,完成逻辑判断和动态拼接sql的功能
2)Mybatis提供了9中动态sql标签:
trim、where、set、foreach、if、choose、when、outherwise、bind
#{}和${}的区别是什么?
#{} 是预编译处理,${} 是字符串替换。Mybatis 处理 #{} 时,会将 sql 中的 #{} 替换为 ?号,调用PreparedStatement 的 set 方法来赋值,使用#{}可以有效的防止sql注入,提高系统安全性
sql 注入:sql 注入即使指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在 web 应用程序中事先定义好的查询语句的结尾添加额外的 sql 语句,在管理员不知情的情况下实现非法操作
工作流程:
- 在 SqlSessionFactoryBuilder().build() 的时候会根据mybatis.xml文件生成一个 Configuration 对象,所有的配置都会在此对象里面,相当于一个全局变量,然后把 Configutation 对象当做参数构造出一个DefaultSqlSessionFactory 对象
在生成 Configuration 对象时会根据标签里的属性来进行解析指定的mapper.xml 文件,解析之后的sql语句都会生成一个 mappedStatement 对象。
mappedStatement 是 sql 方法的封装,不管是 xml 里的标签形式的sql语句,还是注解形式的 sql 语句,都会被翻译成mappedStatement 对象,该对象有个ID属性,接口的全路径加上方法的名字就是 ID,所有的mappedStatement 对象都会被put到一个叫 mappedStatements 的 map 里,以 ID 为 key,对象自身为value,该 map 继承了 hashmap,重写了 put 方法,如果 key 存在就会直接抛出异常,所以方法不能重载。- 然后是 sqlSessionFactory.openSession(),会根据 Configuration 对象获取 DefaultSqlSession
- 然后 sqlsession 调用 getMapper 方法,利用JDK动态代理生成代理
- 最后执行方法时,sqlSession 会根据 id 选择合适的 sql 语句,id 就是映射文件 sql 语句的 id,就是方法名来执行。是走的 MapperProxy,就是生成的代理对象的 invoke 方法,如果是查询方法,会先查二级缓存,也没有,再查一级缓存,都没有才会查询数据库,放到一级缓存中
缓存
一级缓存
1、一级缓存的生命周期有多长?
- MyBatis 在开启一个数据库会话时,会 创建一个新的 SqlSession 对象,SqlSession 对象中会有一个新的Executor 对象。Executor 对象中持有一个新的 PerpetualCache 对象;当会话结束时,SqlSession 对象及其内部的 Executor 对象还有 PerpetualCache 对象也一并释放掉。
- 如果 SqlSession 调用了close()方法,会释放掉一级缓存 PerpetualCache 对象,一级缓存将不可用。
- 如果 SqlSession 调用了clearCache(),会清空 PerpetualCache 对象中的数据,但是该对象仍可使用。
- SqlSession 中执行了任何一个 update 操作 (update()、delete()、insert()) ,都会清空 PerpetualCache 对象的数据,但是该对象可以继续使用
mybatis 一级缓存默认是开启的,但与 spring 整合后,每次查询都是新的 SQLSession,所以一级缓存会失效。 而在方法上加 @Transactional 将原本的 DefaultSqlSession 替换成了 SqlSessionTemplate,并且在 SqlSessionTemplate 将 sqlSession替换成了代理对象,当我们执行 sqlSession.selectList 方法的时候会调用到 SqlSessionInterceptor的invoke方法, 在 invoke 方法的fianlly中调用了 SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory) 将我们的 session 关闭了。原生的 mybatis 之所以没有关闭session 是因为它把 session 暴露给我们了,而和 spring 结合使用的时候并没有提供暴露 session 的方法,所以只能在这里关,而一旦 session 关闭了,那一级缓存自然也就失效了。
2、怎么判断某两次查询是完全相同的查询?
mybatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询。
- 相同的sql和参数
- 会话级别缓存,必须是同一个会话sqlSession
- 相同的方法
- 必须是相同的命名空间
二级缓存
-
命中条件
1.1 必须设置@CacheNamespace
1.2 会话关闭,才会填充到二级缓存
存储过程
insert语句的 statementType 默认为 PREPARED,要用 ${} 可以把 statementType 改为 statement,也就是原生的sql,当用存储过程时 需改为 CALLBLE 例:
<insert id="addUser" statementType="CALLABLE">
{
call add_user(
#{username,mode=IN,jdbcType=VARCHAR},
#{password,mode=IN,jdbcType=VARCHAR},
#{salt,mode=IN,jdbcType=VARCHAR}
)
}
</insert>
循环插入
<insert id="addOptions" parameterType="java.util.List">
insert into options(oname,count,sid) values
<foreach collection="optionName" item="option" index="index" separator=",">
(
#{option},0,#{subjectId}
)
</foreach>
</insert>
配置文件:
mybatis.configuration.map-underscore-to-camel-case //下划线转成驼峰命名
pagehelper 分页插件使用
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.10</version>
</dependency>
@Bean
public PageHelper getPageHelper() {
PageHelper pageHelper = new PageHelper();
Properties properties = new Properties();
properties.setProperty("helperDialect", "mysql");
properties.setProperty("reasonable", "true");
properties.setProperty("supportMethodsArguments", "true");
properties.setProperty("params", "count=countSql");
pageHelper.setProperties(properties);
return pageHelper;
}
import com.alibaba.fastjson.JSON;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.yuanqiao.dao.UserDao;
import com.yuanqiao.entity.User;
import com.yuanqiao.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public PageInfo<User> getAllUsers(int pageNum,int pageSize) {
//当前页,每页展示的数据
PageHelper.startPage(pageNum,pageSize);
List<User> allUsers = userDao.getAllUsers();
PageInfo<User> pageInfo=new PageInfo<>(allUsers);
return pageInfo;
}
}
递归查询子节点(只是提供这种写法,但是一般不用)
<mapper namespace="com.wzl.interview.dao.RegionDao">
<!-- 这里的id的值作为下面的查询返回结果resultMap的值 -->
<!-- collection中的column属性可以为多个值,这里只有一个,它作为下面递归查询传递进去的参数 -->
<!-- ofType和javaType属性正好联合构成了数据Bean类Category中的childrenList属性的数据类型 -->
<!-- select的值为下面递归查询的select标签的id值 -->
<resultMap id="regionMapping" type="com.wzl.interview.pojo.Region">
<id column="id" property="value"/>
<result column="name" property="label"/>
<collection property="children" ofType="com.wzl.interview.pojo.Region" select="selectChildren" column="id"/>
</resultMap>
<!-- 先查询菜单根级目录 -->
<!-- 这里的返回结果必须为resultMap,并且值为上面构建的resultMap的id的值 -->
<select id="selectRegion" resultMap="regionMapping">
select * from region where pid is null
</select>
<!-- 再利用上次查询结果colliection中column的值cid做递归查询,查出所有子菜单 -->
<!-- 这里的返回结果必须为resultMap,并且值为上面构建的resultMap的id的值 -->
<select id="selectChildren" resultMap="regionMapping">
select * from region where pid = #{id};
</select>
</mapper>