MyBatis介绍及基本使用
前言
关于MyBatis,其实去年学习的时候就开始使用了,不过当时由于刚开始学习框架,对一些东西的理解不是很好,所有在学习MyBatis的时候,虽然有想法想把笔记整理出来作为博客发布,但是一直不敢动手,一方面是对MyBatis的使用不是很熟悉,另一方面是懒吧,囧,这几天趁着有时间重新回顾了MyBaits并且加上接触一些项目,所以就把学习的过程笔记整理出来。
关于MyBatis
MyBatis的前身是Ibatis,是一款优秀的持久层框架官网地址,其对JDBC进行了一系列的封装,提供了自动化的参数注入以及结果集抽取,然而又与Hibernate不同,MyBatis并没有提供全自动的SQL生成,不过,这也提供了极大地便利,我们可以根据情况编写合适的SQL,尤其是对于大牛来说,可以写出性能高一点的SQL,当然,这个我还不行,囧。
关于MyBatis,就不做过多的介绍了,其背景对于我们使用也没有太大的意义
MyBatis基本使用
MyBatis是持久层框架,所有,必不可少要与数据库打交道,这里我使用的是MySQL,首先建立基本的测试库以及测试表
数据来自刘增辉老师的《MyBatis从入门到精通》
create database mybatis default character set utf8 collate utf8_general_ci;use mybatis;# 测试表create table `country`( `id` int not null auto_increment, `country_name` varchar(255) null , `country_code` varchar(255) null , primary key (`id`) );# 测试数据insert into country (country_name, country_code) values ('中国', 'CN'), ('美国', 'US'), ('俄罗斯', 'RU'), ('英国', 'GB'), ('法国', 'FR');
导入MyBatis以及数据库连接池依赖
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.13</version></dependency>
在MyBatis中,需要一个配置MyBatis的配置文件,用于指导MyBatis如何进行工作,如加载mapper文件的位置,类型处理器等等的操作,一般将其放置项目根目录就行
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="UNPOOLED"> <property name="driver" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://100.69.4.49:1080/mybatis?useUnicode=true&characterEncoding=utf8" /> <property name="username" value="root" /> <property name="password" value="huanfeng" /> </dataSource> </environment> </environments> <!--指定配置文件路径--> <mappers> <mapper resource="mapper/countryMapper.xml"/> </mappers></configuration>
有了MyBatis配置文件之后,还需要多个mapper文件,一般一个mapper对应一个dao对象,也就是一个mapper对应一个对象的操作
这里对应上面的Country表,建立一个CountryMapper.xml文件,放置路径位于mapper/
,也可以自己指定,然后修改上面的<mappers>中的路径即可
CountryMapper.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,一般是用对应的接口的全限定名--><mapper namespace="mapper.CountryMapper"> <!--查询语句,id在同个mapper文件中也要唯一,一般是方法的名称--> <select id="selectAll" resultType="Country"> select id, country_name as countryName, country_code as countryCode from country; </select></mapper>
对应的实例类Country.java
public class Country { private Long id; private String countryName; private String countryCode; // get() set() toString()}
虽然在简单的一个操作中,不需要配置接口文件也行,不过,一般一个mapper文件都会对应一个接口文件,这里就是CountryMapper.java
public interface CountryMapper { List<Country> selectAll(); }
接下来是加载配置文件并且启动MyBatis
public class CountryMapperTest{ private static SqlSessionFactory sqlSessionFactory; @BeforeClass public static void init() { try { // 通过MyBatis自带的Resources工具来加载配置文件 Reader reader = Resources.getResourceAsReader("config.xml"); // 通过SqlSessionFactoryBuilder()来构建sqlSessionFactory对象 sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); reader.close(); } catch (IOException e) { e.printStackTrace(); } } @Test public void testSelectAll() { // 打开一个session对象 SqlSession sqlSession = sqlSessionFactory.openSession(); try { // 通过session对象来获取mapper CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class); List<Country> countryList = countryMapper.selectAll(); System.out.println(countryList); }finally { // 记得关闭session sqlSession.close(); } } }
输出结果
[ Country{id=1, countryName='中国', countryCode='CN'}, Country{id=2, countryName='美国', countryCode='US'}, Country{id=3, countryName='俄罗斯', countryCode='RU'}, Country{id=4, countryName='英国', countryCode='GB'}, Country{id=5, countryName='法国', countryCode='FR'} ]
至此,一个简单的实例就跑起来了
比较常用的几个配置
在config.xml中,有非常多的配置可以进行设置,具体可以参考官方的配置,其中有几个比较常用的罗列如下
<configuration> <!-- 指定实体类别名, 在前面的配置中,如果我们的实体类存在于包中,那么在mapper文件中,需要使用的时候 需要使用类的全限定名来指定,配置了别名之后,可以直接使用实体类名就行 当然,实体类名也是需要全局唯一的,如果不唯一的话,就使用全限定类名来指定 --> <typeAliases> <package name="domain"/> </typeAliases> <!-- 指定接口的位置,需要注意与<mapper resource="" />的区别 需要注意的是,package是接口文件的包名,不是mapper.xml文件的位置 不过一般打包的时候,会将mapper.xml与接口文件放在一起,所以本质也是一样 --> <mappers> <package name="mapper"/> </mappers></configuration>
看到上面的例子,可能会觉得很奇怪,为什么通过接口文件就能进行操作,其实仔细分析也比较清楚,MyBatis通过扫描接口之后,通过动态代理技术生成对应的实例类,并且将其缓存起来。
MyBatis源码简单剖析
下面的内容是跟踪一个sqlSessionFactory的构建过程
SqlSessionFactory = new SqlSessionFactoryBuilder.build(reader)// 其中build过程如下public SqlSessionFactory build(Reader reader, String environment, Properties properties) { XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); return build(parser.parse()); // 省略其他操作}// parse过程如下public Configuration parse() { // 省略其他操作 parseConfiguration(parser.evalNode("/configuration")); return configuration; }// parseConfiguration过程如下private void parseConfiguration(XNode root) { // 省略其他操作 mapperElement(root.evalNode("mappers")); }// mapperElement过程private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { // 如果是 <mapper package name="" />则解析对应的包内的所有接口 String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } // 省略其他操作 } } }// addMappers过程如下public void addMappers(String packageName) { mapperRegistry.addMappers(packageName); }// addMapperspublic void addMappers(String packageName) { addMappers(packageName, Object.class); }public void addMappers(String packageName, Class<?> superType) { ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>(); resolverUtil.find(new ResolverUtil.IsA(superType), packageName); // 获取所有的class对象 Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses(); for (Class<?> mapperClass : mapperSet) { addMapper(mapperClass); } }// addMapperpublic <T> void addMapper(Class<T> type) { // 检查是否是接口 if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } // 参见下面代码 knownMappers.put(type, new MapperProxyFactory<T>(type)); MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); } }// mapperRegistry定义public class MapperRegistry { private final Configuration config; private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>(); }// mapperProxyFactorypublic class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); }
到了这里配置文件解析以及mapper接口的查找完成,接下来就是获取以及实例化的过程了,也就是调用sqlSession.getMapper
的过程了
sqlSession.getMapper(CountryMapper.class);// getMapper过程public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); }// 可以看到,其实就是从mapperRegistry中获取mapper了public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }// 通过mapperProxyFactory来实例化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 { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }// mapperProxyFactory#newInstance过程public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }// newInstance过程// 从这里就可以看到,其实就是通过JDK的动态代理来实例化接口对象protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }
到了这里,接口的实现就完成了,但是,分析的过程还没有结束,因为SQL语句和接口中的方法还没有对应上,继续分析
// 在JDK动态代理中,我们是需要传入一个InvocationHandler的实现类的,这里就是mapperProxy了public class MapperProxy<T> implements InvocationHandler, Serializable { }// invoke方法public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 如果是Object的方法,则直接调用 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { // 默认方法 return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // 将方法缓存 final MapperMethod mapperMethod = cachedMapperMethod(method); // 调用接口的方法 return mapperMethod.execute(sqlSession, args); }// 缓存方法private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); methodCache.put(method, mapperMethod); } return mapperMethod; }// MapperMethodpublic class MapperMethod { private final SqlCommand command; private final MethodSignature method; }// sqlCommand,主要就是sql的绑定啦public static class SqlCommand { private final String name; private final SqlCommandType type; public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { final String methodName = method.getName(); final Class<?> declaringClass = method.getDeclaringClass(); MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration); if (ms == null) { if (method.getAnnotation(Flush.class) != null) { name = null; type = SqlCommandType.FLUSH; } } else { name = ms.getId(); type = ms.getSqlCommandType(); } } }// 解析mapper语句private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, Class<?> declaringClass, Configuration configuration) { String statementId = mapperInterface.getName() + "." + methodName; if (configuration.hasStatement(statementId)) { return configuration.getMappedStatement(statementId); } else if (mapperInterface.equals(declaringClass)) { return null; } for (Class<?> superInterface : mapperInterface.getInterfaces()) { if (declaringClass.isAssignableFrom(superInterface)) { // 递归解析 MappedStatement ms = resolveMappedStatement(superInterface, methodName, declaringClass, configuration); if (ms != null) { return ms; } } } return null; } }// execute方法public Object execute(SqlSession sqlSession, Object[] args) { Object result; // 根据SQL中的类型来执行不同的操作 switch (command.getType()) { // 其他的操作 // select操作 case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } break; } return result; }
作者:颜洛滨
链接:https://www.jianshu.com/p/c115093326d0