写在前面
说到mybatis,那可太熟悉了,几乎是所有的java开发从业者的必备框架之一。面试也非常喜欢的mybatis的内部实现和组成。我们在使用mybatis的时候往往都是配合SpringBoot一起。SpringBoot的自动配置会为我们日常的开发提供极大的便利,包括mybatis的配置,SpringBoot都为我们配置好了,我们只需要写写mapper就能优雅的请求到mysql的数据,而不需要走建立连接,配置sql,请求数据,处理结果等复杂流程。所以本文想尽可能的从最初的起点开始,看看mybatis是怎么实现整个流程的。
什么是mybatis
它是一款半自动的ORM持久层框架,具有较高的SQL灵活性,我们可以在配置中定制化sql代码,隐藏繁杂的数据连接请求过程,并且支持缓存、延迟加载等特性。
那什么是ORM呢,ORM指的是对象-关系映射(Object-Relational Mapping,简称ORM),可以描述为一个java对象与mysql中的一个表的映射,一个对象就像是表中的一条记录,对象的每个参数就与表中的每个字段保持一致。想象下如果不使用这种框架,每一次请求都需要重新处理返回值,校验等。代码重复率高且不便于维护。至于为什么叫半自动化,因为mybatis的sql是可以由我们自己控制的,那就可以控制返回的数据集,相比Hibernate更加灵活,Hibernate则是一种全自动的ORM框架,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取。
一个简单的实现-实例代码
我们看看mybatis是怎么使用的 项目结构
依赖配置
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.8</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
</dependencies>
User实体对象
package com.df.mybatis.dao.model;
import lombok.Data;
@Data
public class User {
private Long id;
private Long userId;
private String userName;
private Long tel;
private Integer created;
private Integer updated;
private Integer isDeleted;
}
User的查询query
package com.df.mybatis.dao.query;
import lombok.Data;
@Data
public class UserQuery {
private Long id;
private Long userId;
}
UserMapper接口
package com.df.mybatis.dao.mapper;
import com.df.mybatis.dao.model.User;
import com.df.mybatis.dao.query.UserQuery;
import org.apache.ibatis.annotations.Param;
import java.io.IOException;
import java.util.List;
public interface UserMapper {
List<User> selectByCondition(UserQuery userQuery) throws IOException;
}
用于sql映射的xml文件
<?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">
<!--- 命名空间 namespace -->
<mapper namespace="com.df.mybatis.dao.mapper.UserMapper">
<sql id="tableName">
`User`
</sql>
<select id="selectByCondition" parameterType="com.df.mybatis.dao.query.UserQuery" resultType="com.df.mybatis.dao.model.User" >
select * from
<include refid="tableName"/>
<where>
<if test="id != null">AND `id` = #{id}</if>
<if test="userId != null">AND `userId` = #{userId}</if>
</where>
limit 500
</select>
</mapper>
mybatis配置 - 需要配置数据源,以及设置映射mapper
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 环境配置:事务管理器和数据源配置 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!-- 映射器 -->
<mappers>
<mapper resource="properties/mapper/UserMapper.xml" />
</mappers>
</configuration>
UserMapper接口的实现
package com.df.mybatis.dao.Impl;
import com.df.mybatis.dao.mapper.UserMapper;
import com.df.mybatis.dao.model.User;
import com.df.mybatis.dao.query.UserQuery;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class UserMapperImpl implements UserMapper {
@Override
public List<User> selectByCondition(UserQuery userQuery) throws IOException {
String resource = "properties/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
List<User> userList = sqlSession.selectList("com.df.mybatis.dao.mapper.UserMapper.selectByCondition", userQuery);
sqlSession.close();
return userList;
}
}
最后整个test运行一下
import com.df.mybatis.dao.Impl.UserMapperImpl;
import com.df.mybatis.dao.mapper.UserMapper;
import com.df.mybatis.dao.model.User;
import com.df.mybatis.dao.query.UserQuery;
import java.io.IOException;
import java.util.List;
public class Test {
@org.junit.Test
public void testMybatisSelect() throws IOException {
UserMapper userMapper = new UserMapperImpl();
UserQuery userQuery = new UserQuery();
userQuery.setUserId(1L);
List<User> users = userMapper.selectByCondition(userQuery);
System.out.println(users);
}
}
表结构
CREATE TABLE `User` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`userId` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户id',
`userName` varchar(64) NOT NULL DEFAULT '' COMMENT '用户名',
`age` int unsigned NOT NULL DEFAULT '0' COMMENT '年龄',
`tel` bigint unsigned NOT NULL DEFAULT '0' COMMENT '手机号',
`created` int unsigned NOT NULL DEFAULT '0' COMMENT '记录创建时间',
`updated` int unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
`isDeleted` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `idx_userId` (`userId`)
) ENGINE=InnoDB AUTO_INCREMENT=117 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
运行
代码分析
从结果上看,我们就是执行了一段sql,并得到了一条数据。学习的切入点可以从运行的test文件来看,我们new了一个UserMapperImpl对象,并执行了它的查询方法。在方法的内部,我们可以比较清晰的看到首先定义了我们mybatis配置文件的地址,并转换为了stream流的形式。
String resource = "properties/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
我们可以大致看看 Resources.getResourceAsStream(resource); 的内部是怎么解析mybatis配置文件的。
public static InputStream getResourceAsStream(String resource) throws IOException {
return getResourceAsStream(null, resource);
}
public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
return getResourceAsStream(resource, getClassLoaders(classLoader));
}
}
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
for (ClassLoader cl : classLoader) {
if (null != cl) {
InputStream returnValue = cl.getResourceAsStream(resource);
if (null == returnValue) {
returnValue = cl.getResourceAsStream("/" + resource);
}
if (null != returnValue) {
return returnValue;
}
}
}
return null;
}
上述代码通过线程获取的类加载器得到了我们mybatis-config.xml的流数据。后会执行生成SqlSessionFactory的代码,那么SqlSessionFactory到底是什么呢?
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
}
}
}
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
mapperElement(root.evalNode("mappers"));
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
解析得到SqlSessionFactory(Configuration)之后,下一步就是得到SqlSession,来看看获取SqlSession的过程又经历了什么。
SqlSession sqlSession = sqlSessionFactory.openSession();
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
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);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
从上面的流程得知,其实sqlSession是对jdbcTransaction(配置的是jdbc类型)的一个封装,包括配置了sqlSession级的本地缓存,延迟队列等…具体还是要看一下是怎么使用sqlSession去执行sql语句的。
List<User> userList = sqlSession.selectList("com.df.mybatis.dao.mapper.UserMapper.selectByCondition", userQuery);
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
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();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.<E> handleResultSets(ps);
}
整体看下来,就是对我们最古老写的代码的封装,本质还是通过PreparedStatement进行查询,只是不需要我们每次都配置这么底层的东西,方便我们的开发。 最后就是对sqlSession的关闭啦。
sqlSession.close();
结束
今天通过一个mybatis最简单的查询例子,简单的了解了一下内部的实现原理。当然在日常的开发中,会结合SpringBoot等开源框架使用,这些就等到下一次再解析吧~
|