Mybatis学习路线
相关框架
- mybatis
- mybatis-spring
- mybatis-spring-boot-starter
- mybatis-plus
mybatis
入门
快速入门
步骤1 引入maven
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>x.x.x</version>
</dependency>
步骤2 从 XML 中构建 SqlSessionFactory
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
mybatis-config.xml
<?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="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
或者 不使用 XML 构建 SqlSessionFactory
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
步骤3 从 SqlSessionFactory 中获取 SqlSession
try (SqlSession session = sqlSessionFactory.openSession()) {
Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
}
或者
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
}
分析–个人见解
以上部分是我们参考官方做的记录–>所以下面才是重点内容,不然直接看官方文档就OK了!!!以下是个人理解,不喜勿喷!!!
通过上面可以看出来,对于mybatis的基本使用包含如下几个类
- SqlSessionFactory
- SqlSession
- XXXMapper
所以问题来了
如何创建SqlSessionFactory?
需要DataSource(数据源)、事务管理器、Mapper配置。
当然还有其他配置信息,但是这里我们只讨论必要的配置信息。
什么是SqlSession?
SqlSession由SqlSessionFactory创建生成,SqlSession用来执行sql。Mapper操作需要在SqlSession下执行。
什么是Mapper?
实际SQL操作使用的最多的便是Mapper,SqlSessionFactory主要用于配置过程。SqlSession也是可以手动获取,但是通常是隐式获取,而Mapper才是开发使用最多的。
mybatis-spring
入门
快速开始
这里我们通过看xml配置,因为我觉得这样更清晰
使用 SqlSessionFactoryBean
来创建 SqlSessionFactory
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
创建Mapper
- 指定
sqlSessionFactory
- 指定
mapperInterface
<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" />
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
实操:构建一个基本的MyBatis配置
相关源码:https://gitee.com/xu_xiaobao/tobehero/tree/master/study-mybatis
引入maven
这里我们只体现mybatis相关
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.9</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.7</version>
</dependency>
准备Mapper接口类
public interface HeroMapper {
@Select("select * from hero where username = #{username}")
Hero getUser(@Param("username") String username);
}
配置javaBean
@Configuration
public class SimpleMyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
//使用下划线"_"转驼峰
configuration.setMapUnderscoreToCamelCase(true);
factoryBean.setConfiguration(configuration);
return factoryBean.getObject();
}
@Bean
public HeroMapper userMapper(SqlSessionFactory sqlSessionFactory) throws Exception {
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
sqlSessionFactory.getConfiguration().addMapper(HeroMapper.class);
return sqlSessionTemplate.getMapper(HeroMapper.class);
}
}
上面的Mapper处理方式,似乎不是很优雅,我们修改一下写法,在创建SqlSessionFactory
的时候,通过指定configuration#addMappers
完成Mapper注入。
修改后如下
@Configuration
public class SimpleMyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
//使用下划线"_"转驼峰
configuration.setMapUnderscoreToCamelCase(true);
//添加mapper包路径
configuration.addMappers("top.tobehero.study.mybatis.mapper");
factoryBean.setConfiguration(configuration);
return factoryBean.getObject();
}
@Bean
public HeroMapper userMapper(SqlSessionFactory sqlSessionFactory) throws Exception {
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
return sqlSessionTemplate.getMapper(HeroMapper.class);
}
}
总结Mapper创建方式
方式一:SqlSessionFactory/SqlSessionTemplate
正如上面所说:
实操:构建一个基本的MyBatis配置
补充:(个人理解,有误莫怪)
SqlSessionTemplate
:属于Spring概念,实际即为包装SqlSessionFactory,完成一些模板话操作,如打开Session/关闭之类的公共模板操作。
SqlSessionFactory
:核心类,完成真正的业务操作,即框架核心功能关注点。
一般情况下,一个SqlSessionFactory
的存在,对应这个一个SqlSessionTemplate
的存在,一对多的话也没有任何意义。
方式二:MapperFactoryBean
@Configuration
public class SimpleMyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
//使用下划线"_"转驼峰
configuration.setMapUnderscoreToCamelCase(true);
//添加mapper包路径
configuration.addMappers("top.tobehero.study.mybatis.mapper");
factoryBean.setConfiguration(configuration);
return factoryBean.getObject();
}
@Bean
public MapperFactoryBean userMapper(@Autowired SqlSessionFactory sqlSessionFactory) throws Exception {
MapperFactoryBean mfb = new MapperFactoryBean();
mfb.setMapperInterface(HeroMapper.class);
mfb.setSqlSessionFactory(sqlSessionFactory);
return mfb;
}
}
正如上面所说,SqlSessionTemplate
和SqlSessionFactory
具有相同的作用,换言之在创建MapperFactoryBean的时候,也可以使用SqlSessionTemplate
替代SqlSessionFactory
,具体API:MapperFactoryBean#setSqlSessionTemplate
,请自行实践,不再演示。
方式三:@MapperScans/@MapperScan
上面两种方式均存在使用繁琐的问题,那么框架必然是为了简化开发,所以@MapperScan
就是为了简化Mappers的注入过程。
使用如下,只需要注解指定扫描包路径即可完成Mappers的注入。
@Configuration
@MapperScan(basePackageClasses = {HeroMapper.class})
public class SimpleMyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
//使用下划线"_"转驼峰
configuration.setMapUnderscoreToCamelCase(true);
factoryBean.setConfiguration(configuration);
return factoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(@Autowired SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}
一探究竟:@MapperScan
- @MapperScan:主要完成扫描包路径的设置,以及其他的一些设置
- MapperScannerRegistrar:负责注入MapperScannerConfigurer
- MapperScannerConfigurer:最终完成Mapper类的注入
MapperScan.java
MapperScannerRegistrar.java
所以,下面重点是MapperScannerConfigurer
都做了些什么?
刨根问底:MapperScannerConfigurer
org.mybatis.spring.mapper.MapperScannerConfigurer#postProcessBeanDefinitionRegistry
这里出现了一个postProcessBeanDefinitionRegistry方法:
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
/**
* Modify the application context's internal bean definition registry after its
* standard initialization. All regular bean definitions will have been loaded,
* but no beans will have been instantiated yet. This allows for adding further
* bean definitions before the next post-processing phase kicks in.
* @param registry the bean definition registry used by the application context
* @throws org.springframework.beans.BeansException in case of errors
*/
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
}
大致意思:
在所有的Bean都完成了定义的注册,但是还未进行Bean的实例化前,允许再进行一些Bean的定义注册。(相信在这里不需要多做解释,这里就是可以程序化注册Mapper定义的切入点了)
这里我们可以看到有一个扫描动作。
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
...
//扫描器,注意还把registry传了进去(说明:这个scanner可能会注入一个Bean)
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
...
//扫描basePackage
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
- scanner:
org.mybatis.spring.mapper.ClassPathMapperScanner#scan
/**
* Perform a scan within the specified base packages.
* @param basePackages the packages to check for annotated classes
* @return number of beans registered
*/
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
//完成扫描
doScan(basePackages);
// Register annotation config processors, if necessary.
if (this.includeAnnotationConfig) {
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}
org.mybatis.spring.mapper.ClassPathMapperScanner#doScan
扫描得到所有的@Mapper注释的类,然后将其注册的IOC
/**
* Calls the parent search that will search and register all the candidates. Then the registered objects are post
* processed to set them as MapperFactoryBeans
*/
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
//得到所有的@Mapper注解声明的类定义
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
+ "' package. Please check your configuration.");
} else {
//处理BeanDefinitions
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
org.mybatis.spring.mapper.ClassPathMapperScanner#processBeanDefinitions
默认为注册MapperFactoryBean到IOC,可修改
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
String beanClassName = definition.getBeanClassName();
LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
+ "' mapperInterface");
// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
definition.setBeanClass(this.mapperFactoryBeanClass);
definition.getPropertyValues().add("addToConfig", this.addToConfig);
boolean explicitFactoryUsed = false;
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory",
new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
LOGGER.warn(
() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate",
new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
LOGGER.warn(
() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
definition.setLazyInit(lazyInitialization);
}
}
至此,Mapper的注入完成。
值得注意的是:如果默认情况下Mapper类并不需要用@Mapper
修饰,即可被@MapperScan扫描到,只需要当前类是接口类且没有默认实现方法就可以被代理。当然我们可以通过在使用@MapperScan
时指定属性annotationClass
来进行Mapper类的过滤,以避免错误的Mapper被注入并代理。
@MapperScan(basePackageClasses = {HeroMapper.class},annotationClass = Mapper.class)
Mapper到底是什么?
通过上面的讲解,Mapper是通过MapperFactoryBean创建出来的,而且我们只是编写了Mapper的接口类。那么首先肯定是通过代理产生,那么mapper到底是什么?
最终我们发现是通过org.apache.ibatis.binding.MapperRegistry#getMapper
来得到的。
MapperRegistry
Mapper对象登记处
既然我们知道怎么得到的,那么我们首先要知道怎么放进去的?
org.apache.ibatis.binding.MapperRegistry#addMapper
,最终由MapperProxyFactory
负责MapperProxy
的创建工作。
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
//Mapper代理工厂-->创建MapperProxy
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
再来看一下getMapper
传入一个SqlSession,返回Mapper对象
mapperProxyFactory.newInstance
值得注意:SqlSession也可以是一个代理,so不存在单个Template有并发问题,very nice!!!
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//创建MapperProxy
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
MapperProxyFactory
最终用MapperProxy对象来代理Mapper接口.
注意用的是JDK代理
public class MapperProxyFactory<T> {
... ...
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
//最终用MapperProxy对象来代理Mapper接口
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
//MapperProxy
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
MapperProxy
既然是JDK代理,那么必然实现了InvocationHandler
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
//缓存Invoker->执行调用
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
Invoker#invoke
这里以PlainMethodInvoker
为例,最终完成SQL的执行
private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
}
源码总结
- 首先MyBatis的核心为
SqlSessionFactory
,通过SqlSessionFactory
得到Mapper
类完成SQL处理,同时在使用Mapper的过程中,需要一个SqlSession
环境。 - Spring中增加了
SqlSessionTemplate
包装SqlSessionFactory,并在使用时屏蔽掉SqlSession
的操作,所以SqlSessionTemplate
对类似selectOne
类似的MyBatis操作也是需要通过jdk代理完成代理工作。 - Spring通过
@MapperScan
完成对Mappers类的扫描与注册,需要使用MapperScannerConfigurer
来完成注入,其中
3.1MapperScannerConfigurer
主要完成将符合条件的Mapper通过MapperFactoryBean
注入IOC中。
3.2MapperFactoryBean
是一个工厂,是Spring生产Bean的一种方式,我们需要关注的是它的getObject
方法,它是通过SqlSessionTemplate#getMapper
来得到的注入IOC中的Mapper对象。此时可以看到是由mapperRegistry.getMapper
得到的mapper。最终再由MapperProxyFactory
生产出一个被MapperProxy
代理的Mapper对象。
3.3 所以最终被注入到IOC的mapper对象是一个被MapperProxy
代理的Mapper对象。值得注意的是此时的SqlSessionTemplate
被作为SqlSession
被MapperProxy
所持有了 - 当我们调用Mapper中的方法,实际会通过
MapperProxy#invoke
被拦截执行。同时最终因为SqlSession
即为SqlSessionTemplate
,所以最终而言MapperProxy
负责找到映射sql-mapper的SQL语句,最终交由SqlSession
进行SQL的执行。
极致体验:面向Spring Boot!
mybatis-spring-boot-starter
官方文档
翻译后
正如您可能已经知道的,要将MyBatis与Spring结合使用,您至少需要一个
SqlSessionFactory
和至少一个mapper
接口。
MyBatis-Spring-Boot-Starter 将会:
- 自动检测到 一个已存在的
DataSource
- 通过使用
SqlSessionFactoryBean
传入DataSource
创建并注册一个SqlSessionFactory
实例。- 通过
SqlSessionFactory
来创建并注册一个SqlSessionTemplate
- 自动扫描Mappers,将其链接到
SqlSessionTemplate
,并将其注册到Spring上下文,以便将其注入到bean中
maven
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>