MyBatis架构
- 功能架构:
Mybatis的功能架构分为三层:
API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
- 体系结构:
传统的MyBatis API
通过调用 MyBatis 中 SqlSession 对象的方法从而达到与数据库交互的方式,有一些类似DBUtils的操作!
<!-- namespace="dao接口路径"
还可以连dao接口都不定义,只定义此mapper文件,则此处的namespace可以随意写一个“xxx”,
执行方式sqlSession.selectList("xxx.queryAll")
-->
<mapper namespace="xxx">
<select id="queryAll" resultType="User">
select id,name,gender,create_time as createTime
from tt2
</select>
...
如下的非原生API方法:
@org.junit.Test
public void testqueryAllUsers(){
SqlSession sqlSession = sqlSessionFactory.openSession();
UserDao userDao = sqlSession.getMapper(UserDao.class);
System.out.println(userDao.getClass());//class com.sun.proxy.$Proxy4动态生成的代理类对象
List<User> users = userDao.queryAllUsers();//断点所在处
users.stream().forEach(System.out::println);
sqlSession.close();
}
底层实现:
Statement参数与返回值
- 书写规范:
mapper接口的全限定名要和mapper映射文件的namespace的值相同。
mapper接口的方法名称要和mapper映射文件中的statement的id相同;
mapper接口的方法参数只能有一个,且类型要和mapper映射文件中statement的parameterType的值保持一致。
mapper接口的返回值类型要和mapper映射文件中statement的resultType值或resultMap中的type值保持一致;
- 类型解析:
- mybatis 自动处理 java.util.Date
- mybatis 自动识别 1 : 0 为 true / false 。数据库中存 1 / 0 ,java 中只接收 true / false
- parameterType 和 resultType 解析
<!--
parameterType类型:jdk8中基本类型+String :double,float,int,short,boolean,long,date,string
-->
<select id="queryOne" parameterType="int" resultType="User">
select id,name,gender,create_time as createTime
from tt2
where id=#{id}
</select>
1、会类型字符转小写,TypeAliasRegistry初始化构造方法中注册了很多类型,先从中检查
2、如果如上没有,则用Resources.classForName(string)通过反射得到类型(核心原理是 Class.forName()类加载)
String 不在注册类型中,但程序自动补足 java.lang ,java.lang 是 java 的默认包。
- 参数:
如果DAO方法中只有一个参数,则在mapper文件中【#{随意}】:如下的#{abc},#{def},#{hig}
如是
public List<User> xx(Integer id);
public List<User> xx(String name);
public List<User> xx(Date createTime);
<select id="xx" parameterType="int" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where id=#{abc}
</select>
<select id="xx" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where name=#{def}
</select>
<select id="xx" parameterType="date" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where create_time=#{hig}
</select>
当方法中有多个参数时,不能再向如上那样随意,且也不能如常规课程中直接使用参数名;有两种解决方案:
1> 使用#{arg0} #{arg1} #{arg2}
2> 使用 @Param
注解
public List<User> test1(@Param("id")Integer id, @Param("name") String name,@Param("gender") Boolean gender);
public List<User> test2(Integer id, String name,Boolean gender);
<select id="test1" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where id=#{id} and name=#{name} and gender=#{gender}
</select>
<select id="test2" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where id=#{arg0} and name=#{arg1} and gender=#{arg2}
</select>
- # 和 $:
//如果用$,则必须用 @Param注解,否则${name}会认为是要从参数中取名为name的属性
public List<User> test3(@Param("name") String a);
<!-- 注意${} 就是在做字符拼接,所以此处用了【‘${name}’】而不是【${name}】
类比jdbc的sql语句的拼接:
String name="zhj";
String sql = "select ... from tt2 where name='"+name+"'";//此处是要加单引号的
注意:sql拼接时,有sql注入的风险
-->
<select id="test3" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where name = '${name}'
</select>
<!-- 注意#{} 就是在做占位符,所以此处用了【#{name}】而不是【’#{name}‘】
类比jdbc的sql语句的拼接:
String name="zhj";
String sql = "select ... from tt2 where name=?“;//此处是不加单引号的
...
prepareStatement.executeQuery(sql,name);//占位符赋值
-->
<select id="test3" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where name = #{name}
</select>
必须使用$场景:
<select id="test3" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
order by id ${name}
</select>
<select id="test4" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from ${tn}
where name = #{name}
</select>
<!-- 用在列名上亦可 -->
userDAO.test3("desc");
userDAO.test3("asc");
userDAO.test4("t_user");
userDAO.test4("t_admin");
Statement其他
- bind:
<bind name="name_pattern" value="'%'+name+'%'"/> 创建一个变量,并绑定在当前上下文
<select id="queryUsers" parameterType="User" resultType="User">
<bind name="name_pattern" value="'%'+name+'%'"/>
SELECT id,name,gender,regist_time
FROM t_user2
<trim prefix="where" prefixOverrides="and|or">
<if test="name != null">
name like #{name_pattern}
</if>
<if test="id>=10">
OR id>#{id}
</if>
<if test="gender == false">
AND gender=#{gender}
</if>
</trim>
</select>
- Foreach-Map:
public int insertUsers(Map<String,List<User>> users);
<insert id="insertUsers">
insert into t_user2 (name,gender,regist_time) values
<foreach collection="userMaps" item="user" index="ind" close="" open="" separator=",">
(#{user.name},#{user.gender},#{user.registTime,jdbcType=DATE})
</foreach>
</insert>
HashMap<String, List<User>> userMaps = new HashMap<>();
userMaps.put("users",users);
userDAO.insertUsers(userMaps);
Statement 选型
发生在初始加载过程中,每个MappedStatement中存储自己的 StatementType
MyBatis中,除非明确设置 statementType="STATEMENT"
否则默认都是用 PreparedStatement
{
//在mapper标签中获取’statementType‘属性值,如果没有设置则默认取’StatementType.PREPARED.toString()‘
StatementType statementType =
StatementType.valueOf(context.getStringAttribute("statementType",
StatementType.PREPARED.toString()));
}
MappedStatement执行时SimpleExecutor中:
//doQuery() 或 doUpdate() 方法中
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler
resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 通过当前MappedStatement=ms 中的StatementType属性,创建一个StatementHandler
// 会创建一个RoutingStatementHandler,其构造方法中会根据ms的StatementType构建:
// SimpleStatementHandler 或 PreparedStatementHandler.他们各自负责创建一种Statement
// 如下hanlder属性中存储的是一个RoutingStatementHandler对象,可实际工作时,是其内部的
// SimpleStatementHandler 或 PreparedStatementHandler完成执行。
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds,
resultHandler, boundSql);
// 此方法中的handler会负责创建Statement,即,决定用哪种Statement
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
}....
MyBatis事务控制
- 起点:
sqlSessionFactory.openSession();
- DefaultSqlSessionFactory:
// 类:DefaultSqlSessionFactory = SqlSessionFactory的实际类型
public SqlSession openSession() {
// ================ openSession()方法实现 ================
// Executor 隔离级别 自动提交
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
- openSessionFromDataSource:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 获得数据库相关配置
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// ============== 获得事务对象,(此处需要重点关注,继续跟进 ================
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 执行器
final Executor executor = configuration.newExecutor(tx, execType);
// 返回构建好的SqlSession,实际类型为DefaultSqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
- JdbcTransactionFactory:
//类:JdbcTransactionFactory
public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
// =============== 构建事务对象,实际类型:JdbcTransaction ==============
return new JdbcTransaction(ds, level, autoCommit);
}
- JdbcTransaction:
//类:JdbcTransaction,事务对象
// 构造方法:以上传来的各项参数:【数据源+事务隔离级别+是否自动提交事务】
public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
// 在自己的属性中 保持了如上3项参数
dataSource = ds;
level = desiredLevel;
autoCommmit = desiredAutoCommit;
}
// 事务对象中,会打开一个 Connection,其中设置了如上的参数
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
// 1.通过数据源,获得一个Connection对象
connection = dataSource.getConnection();
if (level != null) {
// 2.设置事务隔离级别
connection.setTransactionIsolation(level.getLevel());
}
// 3.设置事务的自动提交属性,默认为false,即mybatis中默认是不自动提交事务的
setDesiredAutoCommit(autoCommmit);
}
// 事务对象中设置 自动提交属性
protected void setDesiredAutoCommit(boolean desiredAutoCommit) {
....
connection.setAutoCommit(desiredAutoCommit);
....
}
// 事务对象中 提交事务
public void commit() throws SQLException {
....
connection.commit();
....
}
//事务对象中 回滚事务
public void rollback() throws SQLException {
...
connection.rollback();
....
}
//事务对象中 关闭资源
public void close() throws SQLException {
...
connection.close();
}
综上,在创建 sqlSession 时,便会创建一个 Transaction 对象。
Transaction 对象负责创建 Connection 对象,并借之实现事务管理:事务属性设置(隔离级别,自动提交);事务行为控制(提交,回滚)
缓存
<!-- 二级缓存如上所示默认是开启的,可以通过以下配置关闭二级缓存 -->
<settings>
<setting name="cacheEnabled" value="false"/>
</settings>
二级缓存需要提交事务,才会生效。
用于全局共享的数据,还是在操作数据的事务结束后再生效为好。
查询时,数据存入一级缓存中。
如果sqlsession在commit或close后,数据存入进入二级缓存。
如果sqlsession执行rollback,则数据不会进入二级缓存。
- cache-ref:
如果<collection>
中没有使用select
关联查询,则不存在此问题
<mapper namespace="com.zhj.dao.UserDAO">
<cache/>
<resultMap id="user_orders" type="User">
<id property="id" column="uid"/>
<result property="name" column="name"/>
<result property="gender" column="gender"/>
<result property="registTime" column="registTime"/>
<collection property="orders" select="com.zhj.dao.OrderDAO.queryOrderOfUser" column="id" fetchType="eager"/>
</resultMap>
<select id="queryOne" parameterType="int" resultMap="user_orders">
select id,name,gender,regist_time as registTime
from t_user
where id=#{id}
</select>
</mapper>
<mapper namespace="com.zhj.dao.OrderDAO">
<!-- 使用cache-ref 则OrderDAO的缓存数据,会存放于com.zhj.dao.UserDAO分支下,
与UserDAO的缓存数据存储于同一个Perpetual对象中
-->
<cache-ref namespace="com.zhj.dao.UserDAO"/>
<select id="queryOrderOfUser" parameterType="int" resultType="Order">
select id as oid,note,price,create_time as createTime
from t_order
where user_id = #{userId}
</select>
</mapper>
UserDAO userDAO = sqlSession1.getMapper(UserDao.class);
userDAO.queryOne(1);//User 和 关系属性Order 都被缓存
sqlSession.commit();
OrderDAO orderDAO = sqlSession2.getMapper(OrderDAO.lass);
orderDAO.queryOrderOfUser(1); //有二级缓存数据可用 (OrderDAO中必须有 <cache>或<cache-ref>)
UserDAO userDAO = sqlSession1.getMapper(UserDao.class);
userDAO.queryOne(1);
//数据改动,此时会清空User缓存,并清空Order缓存(因为order中是 <cache-ref>,如果是<cache>则此处只会清空User缓存)
userDAO.insertUser(new User(null,"new_user",true,new Date()));
sqlSession.commit();
OrderDAO orderDAO = sqlSession2.getMapper(OrderDAO.lass);
orderDAO.queryOrderOfUser(1); //重新查询数据库,<cache-ref namespace="A"/> 会由于A的改动而被清空缓存
源码解析
- 配置文件加载:
SqlSessionFactoryBuilder().`build(reader);`
|-- XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);//封装解析器
return build(parser.`parse()`);//解析配置,返回一个SqlSessionFactory.则SSF是由mybatis的全部配置支撑的。
|-- XMLConfigBuilder .`parse()`//解析配置文件
|-- XMLConfigBuilder .`parseConfiguration`(parser.evalNode("/configuration"));
//解析配置,从configuration标签开始,解析整个配置文件
//其中加载配置个各个节点都是用了单独的方法
//加载properties标签,加载typealias标签等,可以在每个加载方法中看到标签解析的细节。
//比如properties标签加载时,会解析 resource属性 或 url属性,以及会如何判断用户用的是哪个。
- 映射文件加载:
XMLConfigBuilder .`parseConfiguration`(parser.evalNode("/configuration"));//其中有一项是加载mapper文件
|-- XMLConfigBuilder .`mapperElement`(root.evalNode("mappers")); //加载mapper文件
|-- XMLMapperBuilder.`buildStatementFromContext(context.evalNodes("select|insert|update|delete"))`
//其中会加载所有的 select,update,delete,insert标签, 返回`List<XNode>`,然后遍历该List,执行下一步
|-- XMLStatementBuilder.`parseStatementNode()`//每遍历一个XNode执行一次该方法,解析出一个MappedStatement
//其中会解析上一步加载的每个标签(`XNode`),和其中的属性(resultTeype,id,paremeterType....),
//其中还有很重要的一步,是决定当前mapstatement使用的Statement类型。
//解析完毕后,有一步:
|-- parseStatementNode()#`builderAssistant.addMappedStatement(XNode中的所有属性 id,resultTpe,...)`
|-- MapperBuilderAssistant.`addMappedStatement()`
|-- 其中会根据传入的(id,resultType,...)构建一个`MappedStatement.Builder statementBuilder`然后:
|-- `statementBuilder.build()` 返回一个MappedStatement
|-- `configuration.addMappedStatement(statement);`//将MappedStatement 存入Configuration对象
- 动态sql解析:
**在映射文件加载过程中,还会有sql语句的解析,主要是针对,动态sql的解析**
|-- XMLStatementBuilder.`parseStatementNode()` 中:
|-- `langDriver.createSqlSource(configuration, context, parameterTypeClass);`
|-- 如上方法实现中:
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
|-- XMLScriptBuilder.`parseScriptNode()`
|-- 如上方法中:
List<SqlNode> contents = parseDynamicTags(context);//解析动态sql
if (isDynamic) {// 如果有 $ 或 动态标签
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); //其中解析 #{ }
}
//得到一个SqlSource,其中存储了 : sql语句;sql中的参数名(#{xx})
//还持有当前SqlSessionFactory中的configuration对象( 缓存信息,延迟加载信息等配置都可以在其中获)
【parseDynamicTags方法中调用nodeHandlers()//获取所有的动态sql标签处理的 handler,每种动态标签都对应一种handler
handler会解析动态标签】
- SqlSession执行:
UserDAO userDAO = sqlSession.getMapper(UserDAO.class);
|-- userDAO.queryOne(1); //mapper类型:MapperProxy,基于jdk动态代理构建
|-- MapperProxy.`invoke`
//其中获得的MapperMethod对象( 其中封装了:当前方法接口,当前方法对象, Configuration)
|-`MapperMethod.execute()`
//其中会真正执行对应方法,执行的真正实现者是sqlSession.xxxx();[insert,udate,delete,selectOne,.....]
|- 在传统API中调用`Excutor(CachingExcutor=>SimpleExcutor)`
|- `PrepareStatementHandler` => 获取PrepareStatement.execute()
PageHelper
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>RELEASE</version>
</dependency>
<!--
plugins在配置文件中的位置必须符合要求,否则会报错,顺序如下:
properties?, settings?,
typeAliases?, typeHandlers?,
objectFactory?,objectWrapperFactory?,
plugins?,
environments?, databaseIdProvider?, mappers?
-->
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
<!-- spring等价配置 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注意其他配置 -->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor"></bean>
</array>
</property>
</bean>
//使用:
PageHelper.startPage(2,3);// 第2页,每页3条数据
PageHelper.orderBy("regist_time");//可以选择设置排序
List<User> users = mapper.queryAllUsers();//PageHelper后的第一个查询语句,会被PageHelp增强处理(可观测mysql日志)
for (User user : users) {// users中已经是分页数据
System.out.println(user);
}
//包装一个PageInfo,其中会持有所有分页会用到的信息:当前页号,每页多少条,共多少页,是否为第一页/最后一页,是否有下一页等。
PageInfo<User> pageInfo=new PageInfo<User>(users);
注意:如果是多表查询,则会有如下效果:
select t_user.id,name,gender,regist_time,
t_order.id orderId,price,note,create_time
from t_user JOIN t_order
ON t_user.id = t_order.user_id LIMIT 2, 2
#是对大表做了分页,此时数据可能不完整,比如用户有10个订单,却只能查到2个,或部分。
### `PageHelper.startPage`方法重要提示
只有紧跟在`PageHelper.startPage`方法后的**第一个**Mybatis的**查询(Select)**方法会被分页。
### 请不要配置多个分页插件
请不要在系统中配置多个分页插件(使用Spring时,`mybatis-config.xml`和`Spring<bean>`配置方式,请选择其中一种,不要同时配置多个分页插件)!
### 分页插件不支持带有`for update`语句的分页
对于带有`for update`的sql,会抛出运行时异常,对于这样的sql建议手动分页,毕竟这样的sql需要重视。
### 分页插件不支持嵌套结果映射
由于嵌套结果方式会导致结果集被折叠,因此分页查询的结果在折叠后总数会减少,所以无法保证分页结果数量正确。