MyBatis 本是 Apache 的一个开源项目 iBatis,2010年这个项目由 Apache Software Foundation 迁移到 Google Code,并改变为 MyBatis 。
MyBatis 是支持普通 SQL查询,存储过程和高级映射的优秀持久层框架,MyBatis消除了几乎所有的 JDBC 代码和参数的手工设置以及结果集的检索,MyBatis 使用简单的 XML或注解用于配置和原始映射,将接口和 JAVA的 POJOs(Plain Old Java Objects,普通的 Java 对象)映射成数据库中的记录。
认识往往是从表面现象到内存本质的一个探索过程,所以对于 MyBatis 的掌握,我们从认识"表面现象", MyBatis 的基本构成开始,我们先了解一下 MyBatis 的核心组件 。
每个 MyBatis 的应用都以 SqlSessionFactory 的实例为中心的,SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得,但是读者们需要注意SqlSessionFactory 是一个工厂接口而不是实现类。它的任务是创建 SqlSession,SqlSession 类似于一个 JDBC 的 Connection对象,MyBatis 提供了两种模式去创建 SqlSessionFactory: 一种是 XML 配置方式,这是笔者推荐的方式,另一种是代码的方式,能够使用配置文件的时候,我们需要尽量的使用配置文件,这样一方面可以以免硬编码,一方面方便日后配置人员修改,避免重复编译代码。
这里我们的 Configuration 的类全限定名 org.apache.ibatis.session.Configuration,它在 MyBatis 中将以一个 Configuration 类对象的形式存在,而这个对象将存在于整个 MyBatis 应用的生命周期中,以便重复读取和运用,在内存中的数据是计算机系统中读取速度电子书的,我们可以解析一次配置的 XML 文件保存到 Configuration 类对象,方便我们从这个对象读取配置信息,性能高,单例占用空间小,基本不占用存储空间,而且可以反复的使用,Configuration 类对象保存着我们的配置在 MyBatis 的信息,在 MyBatis 中提供了两个 SqlSessionFactory 的实现类,DefaultSqlSessionFactory 和 SqlSessionManager,不过 SqlSessionManager 目前还没有使用,MyBatis中目前使用的是 DefaultSqlSessionFactory。
让我们来看看他们之前的关系图。
尽管我们接触的更多的是 MyBatis和 Spring的整合使用,但是 MyBatis 有它的独立使用方法,了解其独立使用的方法套路对分析 Spring 整合 MyBatis 非常的有帮助,因为 Spring 无非就是将这些功能进行封装,以简化我们的开发流程,MyBatis 独立的使用包括以下的几个步骤。
先创建数据库表吧
CREATE TABLE lz_user
(
id
bigint(20) unsigned NOT NULL AUTO_INCREMENT,
is_delete
tinyint(2) DEFAULT ‘0’,
gmt_create
datetime DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’,
gmt_modified
datetime DEFAULT CURRENT_TIMESTAMP,
username
varchar(32) DEFAULT NULL COMMENT ‘用户名’,
password
varchar(64) DEFAULT NULL COMMENT ‘密码’,
real_name
varchar(64) DEFAULT NULL,
manager_id
int(11) DEFAULT NULL COMMENT ‘管理员id’,
PRIMARY KEY (id
)
) ENGINE=InnoDB AUTO_INCREMENT=462 DEFAULT CHARSET=utf8mb4 COMMENT=‘公益口罩’;
1 .建立 User对象
用于对数据库中对数据的映射,使程序员更加关注对 Java 类的使用,现是不是对数据库的操作。
@Data public class User { private Long id; private Integer isDelete; private Date gmtCreate; private Date gmtModified; private String username; private String password; private String realName; private Long managerId; //必需要有无参的构造方法,不然,根据 UserMapper.xml 中的配置,在查询数据库时,将不能反射构造出 User 实例 }
2.建立 UserMapper
数据库操作映射文件,也就是我们常常说的 DAO,用于映射数据库操作。可以通过配置文件指定方法对应的 SQL指定。
public interface UserMapper { void insertUser(User user); User getUser(Long id); }
3 .建立配置文件mybatis-config.xml
配置文件主要用于配置程序中可变性高的设置,一个偏大的程序一定会存在经常会变的变量,如果每次变化都需要改变源码那会是非常糟糕的设计,所以,我们看到的各种各样的框架或者应用时都免不了要配置配置文件,MyBatis 中的配置文件主要封装在configuration 中,配置文件的基本结构如下图所示。
这里我们配置一个简易的 XML,包含了获取数据库连接实例的数据源(DataSource),决定事务范围和控制方式的事务管理器(TransactionManager) 和映射器(SQL Mapper),XML 配置文件的内容后面会详细的探讨,这里先给一个简单的示例,如下代码清单所示。
对上面的配置做一下说明
映射器是由 Java 接口和 XML 文件(或注解)共同组成,它的作用如下
一个映射器的实现方式有两种,一种是通过 XML 文件方式实现,读者应该刻我们在 mybatis-config.xml文件中己经描述了一个 XML 文件,它是用来生成 Mapper 的,另外一种就是通过代码方式实现,在 Configuration里面注册 Mapper接口(当然里面还需要我们写入 Java注解),当然它也是 MyBatis 的核心内容,同时也是最为复杂的,这两种方式都可以实现我们的需求,不过笔者强烈建义使用 XML 文件配置方式,理由如下:
XML 文件配置方式实现 Mapper
使用 XML文件配置是 MyBatis 实现 Mapper 的首先方式,它由一个 Java 接口和一个 XML 文件构成,让我们看看它是如何实现的。
第一步,给出 Java 接口,如下代码
public interface UserMapper { void insertUser(User user); User getUser(Long id); }
这里我们定义一个接口,它有一个方法 getUser(),通过用户 id 查找用户对象。
第二步:给出一个映射XML 文件,代码如下:
INSERT INTO lz_user (username, password, real_name, manager_id) VALUES (#{username},#{password},#{realName},#{managerId})
描述一下上面 XML 文件是做了什么?
5.建立测试类MyBatisUtil.java
至此,我们己经完成了 MyBatis 的建立过程,接下来的工作就是对之前的所有工作进行测试,以方便直接查看 MyBatis 为我们提供的效果。
public class MyBatisUtil { private final static SqlSessionFactory sqlSEssionFactory; static { String resource = "spring_1_100/config_61_70/mybatis-config.xml"; Reader reader = null; try { reader = Resources.getResourceAsReader(resource); } catch (IOException e) { e.printStackTrace(); } sqlSEssionFactory = new SqlSessionFactoryBuilder().build(reader); } public static SqlSessionFactory getSqlSEssionFactory(){ return sqlSEssionFactory; } }
解释一下代码的含义,正如生命周期一样,我们希望 SqlSessionFactory 对于一个数据库而言只有一个实例,我们希望它是单例,现在我们学习一下代码如何实现的。
某个对象在应用中承担唯一责任的时候 使用单例模式,而本例中 SqlSessionFactory 的唯一责任就是为我们创建 SqlSession,所以采用单例模式的好处在于可以重复使用这唯一对象,而对象在内存中读取和运行速度都比较快,同时节约内存,我们的办法往往是把构造方法私有化,并给一个静态方法,让其返回唯一单例,而在多线程环境初始化单例,往往需要加线程锁以避免类对象被多次初始化,正如本例一样。
开始测试:
static SqlSessionFactory sqlSessionFactory = null; static { sqlSessionFactory = MyBatisUtil.getSqlSEssionFactory(); }
创建 SqlSession
SqlSession是一个接口类,它类似于你们公司前台的美女客服,它扮演着门面的作用,而真正干活的是 Executor接口,你可以认为它是公司的工程师,假设我是客户找你们公司干活,我只需要告诉前台的美女客服(SqlSession)我要什么信息(参数),要做什么东西,过段时间,她会将结果给我,在这个过程中,作为用户的我所关心的信息。
而我不关心工程师(Executor) 是怎样为我工作的,只要前台告诉工程师(Executor) 工程师就知道如何为我工作了,这个步骤对我而言是个黑箱操作。
在 MyBatis中 SqlSession 接口的实现类有两个,分别是 DefaultSqlSession和 SqlSessionManager,这里我们暂时不讨论Executor 接口及其涉及的其他类,只关心 SqlSession的用法就好了,我们构建了 SqlSessionFactory,然后生成了 MyBatis 的门面接口 SqlSession,SqlSession 接口类似于一个 JDBC 中的 Connection接口对象,我们需要保证每次用完正常关闭它,所以正确的做法是关闭 SqlSession接口的代码写在 finally 语句中保证每次都会关闭 SqlSession,让连接资源归还给数据库,如果我们不及时关闭资源,数据库连接资源将会被耗尽,系统很快会因为数据库资源的匮乏而瘫痪,让我们来看看下面的代码。
@Test public void testGetUser() throws Exception { //定义 SqlSession SqlSession sqlSession =null; try{ //打开 SqlSession会话 sqlSession= sqlSessionFactory.openSession(); //执行相关代码 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.getUser(456l); System.out.println(JSON.toJSONString(user)); }cacth(Exception ex){ System.err.println(ex.getMessage()); }finally{ //在 finally 语句中确保资源被删除顺利并关闭 if(sqlSession !=null){ sqlSession.close(); } } }
测试结果:
这样 SqlSession被我们创建出来,在 finally 语句中我们保证了它的合理关闭,让连接资源归还给数据库连接池,以便后续使用。
SqlSession的用途主要有两种。
这里在数据库设定的 id 自增策略,所以插入的数据会直接在数据库中赋值,当执行测试如果数据库中的表不为空,那么在表中会出现一条我们插入的数据,并会在查询时将此数据查出 。
除了使用 XML 配置方式创建代码外,也可以使用 Java编码来实现,不过并不推荐这种方式。因为修改环境的时候,我们不得不重新编译代码。这样不利于维护。
不过,我们还是想研究一下其是如何实现的,和上面的 XML 方式一样,我们也需要配置别名,数据库环境和映射器,MyBatis 己经提供好了对象的类和方法,我们只要熟悉它们的使用即可,首先构建 Configuration 类对象,然后,往对象里注册我们构建的 SqlSessionFactory 所需要的信息便可。
public static SqlSessionFactory getSqlSessionFactory() { //构建数据库连接池 PooledDataSource dataSource = new PooledDataSource(); dataSource.setDriver("com.mysql.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://172.16.157.238:3306/pple_test?characterEncoding=utf-8"); dataSource.setUsername("ldd_biz"); dataSource.setPassword("Hello1234"); //构建数据库事务处理方式 TransactionFactory txFactory = new JdbcTransactionFactory(); //创建数据库运行环境 Environment environment = new Environment("development", txFactory, dataSource); //构建Configuration 对象 Configuration configuration = new Configuration(environment); //注册一个 MyBatis上下文别名 configuration.getTypeAliasRegistry().registerAlias("User", User.class); //加入一个映射器 configuration.addMapper(UserMapper.class); //使用 SqlSessionFactoryBuilder 构建SqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); return sqlSessionFactory; } @Test public void test1() { SqlSession sqlSession = getSqlSessionFactory().openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); System.out.println("================================="); User user = userMapper.getUser(456l); System.out.println(JSON.toJSONString(user)); }
让我们说明一下上面的代码做了什么?
上面需要注意的是 Mapper 和 Mapper.xml需要放到同一个包下,不然报错,显然用代码的方式和使用 XML只是换了个方法实现而已,其本质并无不同,采用代码方式一般是在需要加入自己的特性的时候才会用到,例如:数据源配置的信息要求是加密的,我们需要把它们转化出来,在大部分的情况下,不建义你使用这种方式来创建 MyBatis 的 SqlSessionFactory。
这样 SqlSession 就被我们创建出来了,在 finally 语句中我们保证了它的合理关闭,让连接资源归还给数据库连接池,以便后续使用。
SqlSession的用途主要有两种。
关于这两种用途我们会在映射器里进行讨论,这两种用途优劣我们也会进行讨论研究。
Java 注解方式实现映射方法不难,只需要在接口中使用 Java注解,注入 SQL 即可,我们不妨看看这个接口,代码如下:
public interface UserMapper {
@Select("select * from lz_user where id=#{id}
")
User getUser(Long id);
}
这里我们需要解析一下,我们使用@Select 注解,注入了和 XML一样的select元素,这样的 MyBatis 就会读取这条 SQL,并将参数的 id 递进 SQL,同样使用了别名,这样MyBatis 会为我们自动映射,得到我们需要的 User 对象,这里看起来比 XML 要简单,但是现实中我们遇到了 SQL 远比这个例子更加复杂,如果多个表的关联,多个查询条件,级联,条件分支等,显然这条 SQL 就会复杂得多,所以不建义使用注解方式,代码如下:
如果写到代码中,可读性太差。
在 MyBatis 中保留着 iBatis,通过命名空间+SQL id 的方式发送 SQL并返回数据的形式,而不需要去获取映射器,以下代码。
SqlSession sqlSession = sqlSessionFactory.openSession(); Listaa = sqlSession.selectList("com.spring_101_200.test_131_140.test_136_mybatis_saferowboundsenabled.UserMapper.selectUserBill", null, new RowBounds(0, 5)); System.out.println(JSON.toJSONString(aa));
如果 MyBatis 上下文只有一个 SQL的 getUser,那我们代码还可以简写:
SqlSession sqlSession = sqlSessionFactory.openSession(); Listaa = sqlSession.selectList("selectUserBill",null, new RowBounds(0, 5)); System.out.println(JSON.toJSONString(aa));
注意,当 SQL 的 id 有两个以上的 getUser的时候,第二种省略法办就会失败,系统异常就会提示你写出“命名空间+SQL id”的全路径模式可以,其实它们大同小异,都是发送 SQL 并返回需要的结果,而 MyBatis 一样会根据"com.spring_101_200.test_131_140.test_136_mybatis_saferowboundsenabled.UserMapper.selectUserBill"找到需要执行的接口和方法,进而找到对应的 SQL,传递参数到 SQL 中,返回数据,完成一次查询 。
那么困惑我们需要 Mapper吗?答案是肯定的,Mapper 是一个接口,相对而言它可以进一步屏蔽 SqlSession 对象,使得他具有更强的业务可读性,因此笔者强烈建义采用映射器方式编写代码,其好处主要有以下两点。
我们使用仅仅是接口和一个 XML 文件(或者注解)去实现 Mapper,Java 接口不是实现类,对于 Java 语言不熟悉的读者肯定会感到十分疑惑,一个没有实现类的接口怎么能够运行呢?其实它需要运用到 Java 语言的动态代理去实现,而实现 Java 语言动态代理的方式有多种,这里我们还需要集中于它的用法,所以可以这样理解,我们会在 MyBatis上下文描述这个接口,而 MyBatis 会为这个接口生成代理对象,代理对象会根据"接口全路径+方法名" 去匹配,找到对应的 XML 文件或者注解去完成所需要的任务,返回给我们需要的结果。
关于SqlSession和 Mapper 是 MyBatis 的核心内容和难点,它内部远远没有我们目前看到的那样简单,只是在入门阶段我们暂时不需要去讨论它的实现方式,知道它的作用和用法就可以了。
我们只讨论了 MyBatis主要组件和它的基本用法。而现实中想要写出高效的程序只掌握MyBatis的基本用法是远远不够的,我们还需要掌握它们的生命周期,这是十分重要的,尤其是在 Web 应用,Socket 连接池等多线程场景中,如果我们不了解MyBatis 组件的生命周期可能带来的并发问题,这节的任务是正常理解 SqlSessionFactoryBuilder,SqlSessionFactory,SqlSession和 Mapper 的生命周期,并且重构上面的代码,使 MyBatis 能够高效的工作,这对于 MyBatis应用的正确性和高性能极其重要,我们必需要掌握它们。
上面的示例非常简单,但是源码是如何实现的呢?注意
:因为我想创建一套MyBatis源码解析博客,因此今天的博客只是对 mybatis源码解析的开篇,源码解析部分较少。MyBatis使用特性居多。希望对mybatis使用的基本特性有一定了解以后,再来对源码深度分析。
SqlSessionFactoryBuilder 是利用 XML 或者 Java 编码获得资源来构建SqlSessionFactory的,通过它可以构建多个 SessionFactory,它的作用是构建器,一旦我们构建了 SqlSessionFactory,它的作用就完结,失去了存在的意义,这时我们就应该毫不犹豫的废弃它,将它回收,所以他的生命周期只在于方法的局部,它的作用就是生成 SqlSessionFactory 对象。
public SqlSessionFactory build(Reader reader) { return build(reader, null, null); }
public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); //构建DefaultSqlSessionFactory对象,将创建好的 configuration 对象传入 return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { } } }
SqlSessionFactory 的作用是创建 SqlSession,而 SqlSession 就是一个会话,相当于 JDBC中的 Connection对象,每次应用程序需要访问数据库,我们就需要通过 SqlSessionFactory 创建 SqlSession,所以 SqlSessionFactory 应该在 MyBatis 应用的整个生命周期中,而如果我们多次创建同一个数据库的 SqlSessionFactory,则每次创建 SqlSessionFactory 会打开更多的数据库连接(connection) 资源,那么连接资源就会很快被耗尽,因此 SqlSessionFactory的责任是唯一的,它责任就是创建 SqlSession,所以我们果断采用单例模式,如果我们采用多例,那就它对数据库的连接消耗是很大的,不利于我们统一管理。所以,正确的做法应该是使得每一个数据库只对应一个 SqlSessionFactory,管理好数据库资源的分配,避免过多的 Connection被消耗。
public SqlSessionFactory build(Configuration config) { //构建DefaultSqlSessionFactory对象,保存 configuration return new DefaultSqlSessionFactory(config); }
SqlSession 是一个会话,相当于 JDBC 的一个Connection对象,它的生命周期应该是在请求数据库处理事务的过程中,它是一个线程不安全的对象,在涉及多线程的时候我们就需要特别小心,操作数据库需要注意隔离级别,数据库锁等高级特性,此外,每次创建的 SqlSession 都必需及时关闭它,它长期存在会便数据库连接池的活动资源减少,对系统的影响很大,正如前面的代码一样,我们往往通过 finally 语句块保证我们正确的关闭 SqlSession,它存活于一个应用请求和操作中,可以执行多条 SQL 语句,保证事务的一致性。
Mapper 是一个接口,而没有任何实现类,它的作用是发送 SQL,然后返回我们需要的结果,或者执行 SQL 从而修改数据库的数据,因此,它应该是一个 SqlSession事务方法之内,是一个方法级别的东西,它就如同 JDBC 中的和条 SQL 语句的执行,它最大范围和 SqlSession是相同的,尽管我们想一直保存着 Mapper,但是你会发现它很难控制,所以尽量在一个 SqlSession事务的方法中使用它们。然后废弃掉。
有了上面的叙述,我们己经清楚了 MyBatis组件的生命周期,如下图所示
解析configuration
public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; //解析mybatis-config.xml的根元素configuration parseConfiguration(parser.evalNode("/configuration")); return configuration; }
下面将进入到真正的解析,将对mybatis-config.xml的根元素configuration进行解析。
private void parseConfiguration(XNode root) { try { //定义配置外在化 propertiesElement(root.evalNode("properties")); //issue #117 read properties first //为一些类定义别名 typeAliasesElement(root.evalNode("typeAliases")); //对某种方法进行拦截调用的机制,被称为plugin插件,下面对插件解析 pluginElement(root.evalNode("plugins")); //解析注册对象工厂 objectFactoryElement(root.evalNode("objectFactory")); //解析objectWrapperFactory标签 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); //MyBatis settings 标签相关配置 settingsElement(root.evalNode("settings")); //根据配置取线上线下环境 environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631 //设置数据库 mysql 或Oracle 等数据配置 databaseIdProviderElement(root.evalNode("databaseIdProvider")); //mybatis对对象数据设置或取出作类型转换 typeHandlerElement(root.evalNode("typeHandlers")); //注册 MyBatis 下的所有 mapper mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
在实际工作中,我们常常遇到这样的问题:系统是由运维人员去配置的,生产数据库的用户名密码对开发者而言是保密的,而且为了安全,运维人员要求对配置文件中的数据库用户名和密码进行加密,这样,我们的配置文件中往往配置的是加密过后的数据库信息,而且无法通过加密字符串去连接数据库,这个时候可以通过编码的形式来满足我们的场景。
下面假设 db.properies 文件中的用户名和密码两个属性都是加密字符串,这个时候我们需要生成 SqlSessionFactory 之前将它转化为明文,而系统己经提供了解密方法 decode(str),让我们来看看如何使用代码的方式完成 SqlSessionFactory 的创建。
public class MyBatisUtil1 { private final static SqlSessionFactory sqlSEssionFactory ; static { String resource = "spring_101_200/config_121_130/spring125_mybatis_properties/mybatis-config.xml"; InputStream cfgStream = null; Reader cfgReader = null; InputStream proStream = null; Reader proReader = null; Properties properties = null; try { //读取配置文件流 cfgStream = Resources.getResourceAsStream(resource); cfgReader = new InputStreamReader(cfgStream); //读取属性文件 proStream = Resources.getResourceAsStream("spring_101_200/config_121_130/spring125_mybatis_properties/db.properties"); proReader = new InputStreamReader(proStream); properties = new Properties(); properties.load(proReader); //解密为明文 properties.setProperty("username",decode(properties.getProperty("db.username"))); properties.setProperty("password",decode(properties.getProperty("db.pwd"))); } catch (Exception e) { e.printStackTrace(); } sqlSEssionFactory = new SqlSessionFactoryBuilder().build(cfgReader,properties); } //在这里实现解密操作 public static String decode(String value){ return value; } public static SqlSessionFactory getSqlSEssionFactory(){ return sqlSEssionFactory; } }
这样我们就完全可以在 db.properties 配置密文,满足对系统安全的要求。
MyBatis支持3种配置方式可能同时出现,并且属性还会重复配置,这3种方式是存在优先级,MyBatis 将按照下面的顺序来加载。
首先,我们来对properties进行解析,在解析 properties之前,我们先来了解一下,properties 是怎样用的。
我们先添加db.properties属性文件,在文件中添加内容如下:
db.driver=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/pple_test?characterEncoding=utf-8
db.username=username
db.pwd=password
修改mybatis-config.xml配置文件,如下:
${db.driver}"/> ${db.url}"> ${db.username}"> ${db.pwd}">
其中,红色部分就是与之前的mybatis-config.xml不相同的部分,从表现,我们可以看出,我们不再需要在 xml配置文件中添加具体各项配置信息,而将配置信息提取到db.properties中,具体的配置项不再是零散的分布在各个 xml 文件中,只需要是统一的配置信息放到db.properties,方便开发人员维护与管理。
我们来看看源码是如何解析db.properties配置文件的。
private void propertiesElement(XNode context) throws Exception { if (context != null) { //将所有子节点的 name,value 转化为Properties Properties defaults = context.getChildrenAsProperties(); //从节点中获取String 属性的resource String resource = context.getStringAttribute("resource"); //从节点中获取String属性的 url String url = context.getStringAttribute("url"); //如果 resource 属性不为空并且 url 属性也不为空,抛出异常 if (resource != null && url != null) { throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other."); } if (resource != null) { //将所有的resource资源文件所配置的内容转化为Properties并存入defaults中 defaults.putAll(Resources.getResourceAsProperties(resource)); } else if (url != null) { //将所有url的资源转化为Properties并存入defaults中 defaults.putAll(Resources.getUrlAsProperties(url)); } Properties vars = configuration.getVariables(); if (vars != null) { defaults.putAll(vars); } //解析器中存入defaults parser.setVariables(defaults); //configuration设置变量defaults configuration.setVariables(defaults); } }
别名typeAliases是一个指代名称,因为我们遇到类全限定名过长,所以希望用一个简短的名称去指代它,而这个名称可以是在 MyBatis上下文 中使用,别名 MyBatis 里面分为系统定义别名和自定义别名两类,注意在 MyBatis 中别名是不分大小写的,一个 typeAliases 的实例是在解析配置文件时生成的,然后长期保存在 Configuration对象中,当我们使用它时,再把它拿出来,这样就没有必要运行的时候再次生成它的实例了。
MyBatis系统定义一些经常使用的别名,例如,数值,字符串,日期和集合等,我们可以在 MyBatis 中使用别名(支持数组类型的只要加"[]"即可以使用,比如 Date 数组别名可以 date[]代替),MyBatis 己经在系统定义了 type Aliases
别名 | 映射的类型 | 支持数组 |
---|---|---|
_byte | byte | 是 |
_long | long | 是 |
_short | short | 是 |
_int | int | 是 |
_integer | int | 是 |
_double | double | 是 |
_float | float | 是 |
_boolean | boolean | 是 |
string | String | 否 |
byte | Byte | 是 |
long | Long | 是 |
short | Short | 是 |
int | Integer | 是 |
integer | Integer | 是 |
double | Double | 是 |
float | Float | 是 |
boolean | Boolean | 是 |
date | Date | 是 |
decimal | BigDecimal | 是 |
bigdecimal | BigDecimal | 是 |
object | Object | 是 |
map | Map | 否 |
hashMap | HashMap | 否 |
list | List | 否 |
arrayList | ArrayList | 否 |
collection | Collection | 否 |
iterator | Iterator | 否 |
ResultSet | ResultSet | 否 |
系统所定义的别名往往是不够用的,因为不同的应用有着不同的需要,所以 MyBatis 允许自定义别名
我们看到,我们没有返回具体的类型,这里具体的类型是指包名+类名,User 对象,可以是 com.aaa.User,也可以是 com.bbb.User,因此,我们可以定义typeAliases,来确定 User 对象具体代表的是哪个类,达到一次配置,到处使用。接下来,我们分析typeAliases的解析。
private void typeAliasesElement(XNode parent) { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { //如果子节点是 package,则获取 name 的值 String typeAliasPackage = child.getStringAttribute("name"); //注册当前包下的所有的类 configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); } else { String alias = child.getStringAttribute("alias"); String type = child.getStringAttribute("type"); //配置如下:try { Class> clazz = Resources.classForName(type); if (alias == null) { //注册所配置的类,别名或者简单类名或者是类所配置注解Alias的值 typeAliasRegistry.registerAlias(clazz); } else { //如果别名和类型都不为空,注册别名和类型 typeAliasRegistry.registerAlias(alias, clazz); } } catch (ClassNotFoundException e) { throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e); } } } } }
public void registerAlias(Class> type) { //获取类的简单名称 String alias = type.getSimpleName(); //获取Alias注解 Alias aliasAnnotation = type.getAnnotation(Alias.class); if (aliasAnnotation != null) { //如果配置了@Alias注解,则取其值作为别名 alias = aliasAnnotation.value(); } registerAlias(alias, type); }
从上面代码分析得知,在注册别名过程中,最不能少的就是类信息,当前类配置分两种情况,如下:
如果 POJO 过多时,我们可以使用自动扫描的形式自定义别名。如需要配置多个包,则像如下配置即可,多个包不能在 name 中以逗号分号隔开,只能写多个 package 标签
接下来,我们继续来分析plugins的解析,在分析之前,我们来看一个业务需求,我们想在第次执行 sql 时,打印sql 语句。这就需要用到我们的plugin的使用了
1.我们在mybatis-config.xml中加入配置文件
2.新建DataScopeInterceptor类
@Slf4j @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})}) public class DataScopeInterceptor extends SqlParserHandler implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget()); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); this.sqlParser(metaObject); BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql"); String originalSql = boundSql.getSql(); //打印出原始sql System.out.println(originalSql); Object result = invocation.proceed(); return result; } /** * 生成拦截对象的代理 */ @Override public Object plugin(Object target) { if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } return target; } /** * @param properties mybatis配置的属性 */ @Override public void setProperties(Properties properties) { } }
运行结果:
下面开始对plugin标签进行解析
private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { //获取plugin的interceptor属性 String interceptor = child.getStringAttribute("interceptor"); //获取所有的子节点中的 name,value 属性,封装成properties Properties properties = child.getChildrenAsProperties(); //创建拦截器 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); //调用拦截器的setProperties方法 interceptorInstance.setProperties(properties); //将拦截器加到拦截器链interceptorChain中 configuration.addInterceptor(interceptorInstance); } } }
在 MyBatis 中,当其 sql 映射配置文件中的 sql 语句所得到的查询结果,被动态映射到 resultType 或其他处理结果集的参数配置对应的 Java 类型,其中就有 JavaBean 等封装类。而 objectFactory 对象工厂就是用来创建实体对象的类。
在 MyBatis 中,默认的 objectFactory 要做的就是实例化查询结果对应的目标类,有两种方式可以将查询结果的值映射到对应的目标类,一种是通过目标类的默认构造方法,另外一种就是通过目标类的有参构造方法。
有时候在 new 一个新对象(构造方法或者有参构造方法),在得到对象之前需要处理一些逻辑,或者在执行该类的有参构造方法时,在传入参数之前,要对参数进行一些处理,这时就可以创建自己的 objectFactory 来加载该类型的对象。
当 MyBatis在构建一个结果返回的时候,都会用 ObjectFactory(对象工厂) 去构建 POJO,在 MyBatis 中可以定制自己的对象工厂,一般来说我们使用默认的 ObjectFactory 即可,MyBatis 的默认 ObjectFactory 是由 org.apache.ibatis.reflection.factory.DefaultObjectFactory 来提供服务的,在大部分场景下,我们都不用修改,如果要定制特定的工厂则需要进行配置。
首先在mybatis-config.xml配置文件中加入
在User 对象中加入 init 方法
public void init(){ //测试对象创建时,是否被工厂方法调用 System.out.println("user init "); }
我们这里配置了一个对象工厂UserObjectFactory,对它的要求是实现 ObjectFactory接口,实际上 DefaultObjectFactory 己经实现了 ObjectFactory 的接口,我们可以通过继承 DefaultObjectFactory 来编程 。
public class UserObjectFactory extends DefaultObjectFactory { publicT create(Class type) { return super.create(type); } //DefaultObjectFactory的create(Class type)方法也会调用此方法,所以只需要在此方法中添加逻辑即可 public T create(Class type, List > constructorArgTypes, List
结果输出:
从运行的结果中可以看出,首先,setProperties 方法可以使得我们如何去处理设置进去的属性,而 create方法分别可以处理单个对象和列表对象,其次,我们配置的 ObjectFactory 己经生效,注意,大部分情况下,我们不需要使用自己配置的 ObjectFactory,使用系统默认的即可。结果正如我们所料,在工厂方法中调用了 User 对象的 init 方法,下面我们来看看对象工厂标签是如何解析的。
private void objectFactoryElement(XNode context) throws Exception { if (context != null) { String type = context.getStringAttribute("type"); //解析objectFactory标签下的所有property元素 Properties properties = context.getChildrenAsProperties(); ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance(); //调用工厂类setProperties方法 factory.setProperties(properties); //设置对象工厂objectFactory configuration.setObjectFactory(factory); } }
MyBatis 提供在构造返回对象的时候,对于指定的对象进行特殊的加工。测试如下,下面将对返回值为 Map 对象进行加工。测试如下:
1.在mybatis-config.xml中添加如下配置
2.在UserMapper.xml中添加
3.添加工厂类
public class MyMapWrapperFactory implements ObjectWrapperFactory { @Override public boolean hasWrapperFor(Object object) { //如果 mybatis返回值类型是Map return object instanceof Map; } @Override public ObjectWrapper getWrapperFor(MetaObject metaObject, Object object) { return new MyMapWrapper(metaObject, (Map) object); } }
public class MyMapWrapper extends MapWrapper { public MyMapWrapper(MetaObject metaObject, Mapmap) { super(metaObject, map); } @Override public String findProperty(String name, boolean useCamelCaseMapping) { //对返回值属性名修改,在前面添加 my_ return "my_"+name; } }
4.测试
在返回值 map 的每个属性前都添加 my_,下面我们来看看, 关于objectWrapperFactory是如何解析的。
private void objectWrapperFactoryElement(XNode context) throws Exception { if (context != null) { //获取objectWrapperFactory的 type 属性 String type = context.getStringAttribute("type"); ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).newInstance(); //保存到configuration的objectWrapperFactory属性中 configuration.setObjectWrapperFactory(factory); } }
大家不要误解,这些标签的实现代码就我分析的这些,目前,只对标签的解析做分析,而 Mybatis 代码是如何实现这些功能,我们放到后面的博客中再来详解。
关于settings标签元素是如何使用的,这里只对一部分做测试了。我们来分析一下settings属性的解析吧。
private void settingsElement(XNode context) throws Exception { if (context != null) { //获取到默认子标签属性 Properties props = context.getChildrenAsProperties(); MetaClass metaConfig = MetaClass.forClass(Configuration.class); for (Object key : props.keySet()) { if (!metaConfig.hasSetter(String.valueOf(key))) { throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)."); } } //指定 MyBatis是否以及如何自动映射指定的列到字段或属性,NONE 表示取消自动映射,PARTIAL只会自动映射没有定义嵌套结果集映射的结果集 FULL会自动映射任意复杂的结果集(包括嵌套和其他情况) configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL"))); //cacheEnabled:false 关闭二级缓存(一级缓存依然可用的),true,开启二级缓存 configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true)); //指定 Mybatis 创建具有延迟加载能力的对象所用到的代理工具,JAVASSIST (MyBatis 3.3 以上) configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory"))); //延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态 configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false)); //当开启时,任何方法的调用都会加载该对象的所有属性。 否则,每个属性会按需加载(参考 lazyLoadTriggerMethods) configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), true)); //是否允许单一语句返回多结果集(需要驱动支持) configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true)); //使用列标签代替列名。不同的驱动在这方面会有不同的表现,具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果 configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true)); //允许 JDBC 支持自动生成主键,需要驱动支持。 如果设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能支持但仍可正常工作(比如 Derby) configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false)); //配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新 configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE"))); //设置超时时间,它决定驱动等待数据库响应的秒数 configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null)); //是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射 configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false)); //允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为 false configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false)); //MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION, //这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据 configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION"))); //当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER"))); //指定哪个对象的方法触发一次延迟加载 configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString")); //允许在嵌套语句中使用分页(ResultHandler)。如果允许使用则设置为 false configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true)); //指定动态 SQL 生成的默认语言,一个类型别名或完全限定类名,目前默认是XMLLanguageDriver. configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage"))); //指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这在依赖于 Map.keySet() 或 null //值初始化的时候比较有用。注意基本类型(int、boolean 等)是不能设置成 null 的 //设置在Mybatis中当结果集中的某个值为null时,是否依然调用所属JAVA对象的属性对应的Setter方法,默认值为false。 configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false)); //指定 MyBatis 增加到日志名称的前缀 configuration.setLogPrefix(props.getProperty("logPrefix")); //指定 MyBatis 所用日志的具体实现,未指定时将自动查找 configuration.setLogImpl(resolveClass(props.getProperty("logImpl"))); //指定一个提供 Configuration 实例的类。 这个被返回的 Configuration 实例用来加载被反序列化对象的延迟加载属性值。 //这个类必须包含一个签名为static Configuration getConfiguration() 的方法。(新增于 3.2.3) configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory"))); } }
关于autoMappingBehavior,网上说的是:默认是PARTIAL,只会自动映射没有定义嵌套结果集映射的结果集。这句话有点拗口,意思就是映射文件中,对于resultMap标签,如果没有显式定义result标签,mybatis不会帮你把结果映射到model(pojo)上,那这句话是什么意思呢?我们来举个例子看看。
1.创建表lz_user
CREATE TABLE lz_user
(
id
bigint(20) unsigned NOT NULL AUTO_INCREMENT,
is_delete
tinyint(2) DEFAULT ‘0’,
gmt_create
datetime DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’,
gmt_modified
datetime DEFAULT CURRENT_TIMESTAMP,
username
varchar(32) DEFAULT NULL COMMENT ‘用户名’,
password
varchar(64) DEFAULT NULL COMMENT ‘密码’,
real_name
varchar(64) DEFAULT NULL,
manager_id
int(11) DEFAULT NULL COMMENT ‘管理员id’,
PRIMARY KEY (id
)
) ENGINE=InnoDB AUTO_INCREMENT=462 DEFAULT CHARSET=utf8mb4 COMMENT=‘公益口罩’;
表中数据如下:
2.创建表lz_user_bill
CREATE TABLE lz_user_bill
(
id
bigint(20) unsigned NOT NULL AUTO_INCREMENT,
is_delete
tinyint(2) DEFAULT ‘0’,
gmt_create
datetime DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’,
gmt_modified
datetime DEFAULT CURRENT_TIMESTAMP,
type
varchar(32) DEFAULT ‘-’ COMMENT ‘收支类型’,
user_id
int(11) DEFAULT NULL COMMENT ‘用户id’,
manager_id
int(11) DEFAULT NULL COMMENT ‘管理员id’,
amount
decimal(12,2) DEFAULT NULL,
remark
text COMMENT ‘备注’,
bill_type
varchar(256) DEFAULT NULL COMMENT ‘账单类型’,
pay_type
varchar(255) DEFAULT NULL COMMENT ‘支付方式’,
status
int(11) DEFAULT ‘0’ COMMENT ‘-1表示作费,0表示提交,1表示已经报销’,
self_look
int(11) DEFAULT ‘0’ COMMENT ‘0表示公开,1表示仅仅自己可见’,
PRIMARY KEY (id
)
) ENGINE=InnoDB AUTO_INCREMENT=61 DEFAULT CHARSET=utf8mb4 COMMENT=‘公益口罩’;
表中数据如下
3.在mybatis-config.xml中加入配置文件
这里的 value 默认是PARTIAL,只会自动映射没有定义嵌套结果集映射的结果集,这句话有点拗口,意思就是映射文件中,对于resultMap标签,如果没有显式定义result标签,mybatis不会帮你把结果映射到model(pojo)上.还有NONE和 FULL配置,后面看效果。
4.新增 POJO
@Data public class UserBillInfo { private Long id; private ListbillList; }
@Data public class Bill { private Long id; private String type; private Long userId; private BigDecimal amount; }
5.在UserMapper.xml添加如下配置
从表结构中,我们可以看出,lz_user 和lz_user_bill 表是一对多的关系,而本次测试的目就是通过user 的 id 查询出该用户的所有账单。
a) 修改mybatis-config.xml中 value 值为
b) 修改UserMapper.xml为
c) 结果输出
{“billList”:[{“id”:456}],“id”:456}
a) 修改mybatis-config.xml中 value 值为
b) 修改UserMapper.xml为
c) 结果输出:
{“billList”:[{“amount”:360.00,“id”:456,“type”:"-",“userId”:456}],“id”:456}
通过上面测试结果,我们可以看到,如果autoMappingBehavior我们配置的是PARTIAL,那么在 collection 标签下面,我们配置 result 标签,将会映射出数据库中的结果集。如果没有配置,将不会映射出数据库中结果集。
下面我们再来测试autoMappingBehavior的FULL配置。
通过上面的配置,我们终于理解了autoMappingBehavior的PARTIAL和FULL属性。
在了解lazyLoadingEnabled和aggressiveLazyLoading使用之前,我们先来了解一下延迟加载。
resultMap可以实现高级映射(使用association、collection实现一对一及一对多映射),association、collection具备延迟加载功能。
需求
:如果查询订单并且关联查询用户信息。如果先查询订单信息即可满足要求,当我们需要查询用户信息时再查询用户信息。把对用户信息的按需去查询就是延迟加载。
延迟加载
:先从单表查询、需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。
数据准备
创建lz_user表
CREATE TABLE lz_user
(
id
bigint(20) unsigned NOT NULL AUTO_INCREMENT,
is_delete
tinyint(2) DEFAULT ‘0’,
gmt_create
datetime DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’,
gmt_modified
datetime DEFAULT CURRENT_TIMESTAMP,
username
varchar(32) DEFAULT NULL COMMENT ‘用户名’,
password
varchar(64) DEFAULT NULL COMMENT ‘密码’,
real_name
varchar(64) DEFAULT NULL,
manager_id
int(11) DEFAULT NULL COMMENT ‘管理员id’,
PRIMARY KEY (id
)
) ENGINE=InnoDB AUTO_INCREMENT=462 DEFAULT CHARSET=utf8mb4 COMMENT=‘公益口罩’;
创建lz_user_bill表
CREATE TABLE lz_user_bill
(
id
bigint(20) unsigned NOT NULL AUTO_INCREMENT,
is_delete
tinyint(2) DEFAULT ‘0’,
gmt_create
datetime DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’,
gmt_modified
datetime DEFAULT CURRENT_TIMESTAMP,
type
varchar(32) DEFAULT ‘-’ COMMENT ‘收支类型’,
user_id
int(11) DEFAULT NULL COMMENT ‘用户id’,
manager_id
int(11) DEFAULT NULL COMMENT ‘管理员id’,
amount
decimal(12,2) DEFAULT NULL,
remark
text COMMENT ‘备注’,
bill_type
varchar(256) DEFAULT NULL COMMENT ‘账单类型’,
pay_type
varchar(255) DEFAULT NULL COMMENT ‘支付方式’,
status
int(11) DEFAULT ‘0’ COMMENT ‘-1表示作费,0表示提交,1表示已经报销’,
self_look
int(11) DEFAULT ‘0’ COMMENT ‘0表示公开,1表示仅仅自己可见’,
PRIMARY KEY (id
)
) ENGINE=InnoDB AUTO_INCREMENT=61 DEFAULT CHARSET=utf8mb4 COMMENT=‘公益口罩’;
增加插件DataScopeInterceptor打印查询sql
@Slf4j @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})}) public class DataScopeInterceptor extends SqlParserHandler implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget()); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); this.sqlParser(metaObject); // 先判断是不是SELECT操作 BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql"); String originalSql = boundSql.getSql(); System.out.println(originalSql); Object result = invocation.proceed(); return result; } @Override public Object plugin(Object target) { if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties properties) { } }
@Data public class UserBill { private Long id; private String type; private Long userId; private BigDecimal amount; }
@Data public class User { private Long id; private String realName; private ListbillList; }
7.测试对于一对一和一对多两种情况,原理都一样,今天只对一对一的情况做分析
@Test public void findUserBillLazyLoading() throws Exception { SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); UserBill userBill = userMapper.findUserBillLazyLoading(60l); System.out.println("----------------------"); System.out.println("realName:" + userBill.getUser().getRealName()); System.out.println("----------------------"); System.out.println("userBill:" + userBill.getUser().getBillList()); }
@Data public class User { private Long id; private String realName; }
@Test public void findUserBillLazyLoading() throws Exception { SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.findUserById(456l); System.out.println(JSON.toJSONString(user)); }
useColumnLabel为true: 当数据库表字段与实体属性就是不一致的,如user表中,表示的用户名的字段叫real_name,而在User类中,只有realName字段和对应的setRealName方法。那么只给
useColumnLabel 为 false :我们在 sql (select id ,real_name as realName
from lz_user )中想通过 as 获取别名与实体相对应,发现数据库字段值不能映射为实体属性。
首先,最底层的接口是Executor,有两个实现类:BaseExecutor和CachingExecutor,CachingExecutor用于缓存,而BaseExecutor则用于基础的操作。并且由于BaseExecutor是一个抽象类,提供了三个实现:SimpleExecutor,BatchExecutor,ReuseExecutor,而具体使用哪一个Executor则是可以在mybatis-config.xml中进行配置的。配置如下:
如果没有配置Executor,默认情况下是SimpleExecutor。今天,不对缓存这一块分析。只对defaultExecutorType三个执行器SimpleExecutor,BatchExecutor,ReuseExecutor做简单分析。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; //默认是SIMPLE执行器 executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { //如果defaultExecutorType配置的是BATCH,使用BatchExecutor executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { //如果defaultExecutorType配置的是REUSE,使用ReuseExecutor executor = new ReuseExecutor(this, transaction); } else { //如果defaultExecutorType配置的是SIMPLE,使用SimpleExecutor executor = new SimpleExecutor(this, transaction); } //如果cacheEnabled为 true,使用CachingExecutor包装执行器 if (cacheEnabled) { executor = new CachingExecutor(executor); } //这里是通过责任链模式来生成代理对象 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
对于三个执行器,网上是这么说的
看了这个结论以后,我云里雾里,感觉很抽象。那就对这三个执行器逐一进行分析吧。对于表的创建,数据准备,配置文件的创建,我们己经写了很多次了。就不再赘述。
4. 新增UserMapper.xml Mapper 文件
select id ,real_name as realName from lz_user where id = #{id} update lz_user set real_name = '张三' where id = #{id} update lz_user set real_name = #{realName} where id = #{id}
提供了一个查询两个更新方法。
@Test public void findUserBillLazyLoading() throws Exception { SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); //连续两条一模一样查询语句,只有参数不同 User user = userMapper.findUserById(456l); user = userMapper.findUserById(457l); System.out.println(JSON.toJSONString(user)); } @Test public void testBatch() throws Exception { SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); //连续两条一模一样更新语句语句,只有参数不同 userMapper.updateRealName(456l,"zhangsan"); userMapper.updateRealName(457l,"lisi"); } @Test public void test3() throws Exception { SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); //三条更新语句,中间一条和前后两条原始语句不一样 userMapper.updateRealName(456l,"zhangsan"); userMapper.update(458l); userMapper.updateRealName(457l,"lisi"); }
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.update(stmt); } finally { closeStatement(stmt); } } publicList 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. query(stmt, resultHandler); } finally { closeStatement(stmt); } } private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; Connection connection = getConnection(statementLog); //无论是更新还是添加,每一次创建新的Statement stmt = handler.prepare(connection); handler.parameterize(stmt); return stmt; }
从上面源码中我们可以看出,对于SimpleExecutor执行器,无论更新还是查询,每次都是创建新的Statement。
执行 test3()连续3条更新语句,其中两条语句相同,参数不同,从下面结果中得知,创建了2个 Statement。
我们来看看源码
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null); Statement stmt = prepareStatement(handler, ms.getStatementLog()); return handler.update(stmt); } publicList doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); Statement stmt = prepareStatement(handler, ms.getStatementLog()); return handler. query(stmt, resultHandler); } private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; BoundSql boundSql = handler.getBoundSql(); String sql = boundSql.getSql(); //statementMap是否存在statement if (hasStatementFor(sql)) { stmt = getStatement(sql); } else { Connection connection = getConnection(statementLog); //以 sql 作为 key ,存储 statement 于Map statementMap中 stmt = handler.prepare(connection); putStatement(sql, stmt); } handler.parameterize(stmt); return stmt; }
从源码中得知,只要执行过的sql,就会存储于statementMap,更新三条语句,只创建2个Statement。
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException { final Configuration configuration = ms.getConfiguration(); final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null); final BoundSql boundSql = handler.getBoundSql(); final String sql = boundSql.getSql(); final Statement stmt; //每次比较当前 SQL和上一次执行的 sql是否相等,并且比较MappedStatement和上一次是否相等。 //MappedStatement则通过MappedStatement ms = configuration.getMappedStatement(statement);得到的 //statement是UserMapper.xml中命名空间和 id 组合,如: //com.spring_101_200.test_131_140.test_135_mybatis_executor.UserMapper.updateRealName //因此,只要执行的Mapper中的方法是一样的,并且相邻执行,就共用 Statement if (sql.equals(currentSql) && ms.equals(currentStatement)) { int last = statementList.size() - 1; stmt = statementList.get(last); BatchResult batchResult = batchResultList.get(last); batchResult.addParameterObject(parameterObject); } else { Connection connection = getConnection(ms.getStatementLog()); stmt = handler.prepare(connection); currentSql = sql; currentStatement = ms; statementList.add(stmt); batchResultList.add(new BatchResult(ms, sql, parameterObject)); } handler.parameterize(stmt); handler.batch(stmt); return BATCH_UPDATE_RETURN_VALUE; } publicList doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { flushStatements(); Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql); //对于查询,每次都创建新的 Statement Connection connection = getConnection(ms.getStatementLog()); stmt = handler.prepare(connection); handler.parameterize(stmt); return handler. query(stmt, resultHandler); } finally { closeStatement(stmt); } }
通过上面分析,我们大概了解了执行器在性能优化上的原理,但想更加深层次的分析,我们留给后面的博客吧。
safeRowBoundsEnabled:允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为 false,不允许使用设置为 true,如果要测试这个属性要满足两个条件,一,嵌套语句,二,分页
select * from lz_user lu left join lz_user_bill lub on lu.id =lub.user_id where lu.id =456
@Test public void testGetUser() throws Exception { SqlSession sqlSession = sqlSessionFactory.openSession(); Listaa = sqlSession.selectList("com.spring_101_200.test_131_140.test_136_mybatis_saferowboundsenabled.UserMapper.selectUserBill", null, new RowBounds(0, 5)); System.out.println(JSON.toJSONString(aa)); }
调用SqlSession默认的方法selectList()方法,传入 statement,方法参数,以及 RowBounds,MyBatis判断分页条件是是否包含RowBounds参数
//如果想执行ensureNoRowBounds方法,查询返回结果集是 resultMap,并且 resultMap 中需要有association,collection,case //中节点中一个,并且节点中不能有 select 属性 //如: //// ="findUserById" column="user_id"/> // //这样写是不行的,因为包含了select属性,MyBatis 认定不是嵌套查询 private void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { if (resultMap.hasNestedResultMaps()) { ensureNoRowBounds(); checkResultHandler(); handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping); } else { handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping); } } private void ensureNoRowBounds() { //提供分页参数 if (configuration.isSafeRowBoundsEnabled() && rowBounds != null && (rowBounds.getLimit() < RowBounds.NO_ROW_LIMIT || rowBounds.getOffset() > RowBounds.NO_ROW_OFFSET)) { throw new ExecutorException("Mapped Statements with nested result mappings cannot be safely constrained by RowBounds. " + "Use safeRowBoundsEnabled=false setting to bypass this check."); } }// select
通过上述源码的简单分析,我们得知,MyBatis 是通过返回类型是 ResultMap并且有association,collection,case节点中的一个,且节点中没有 select 属性认为是嵌套查询,并且传入的查询参数中有 RowBounds,满足以上两个条件时,MyBatis将较验safeRowBoundsEnabled属性。源码,我只列出了一部分,还一有部分比较繁琐,感兴趣的同学可以到 github 上下载测试用例,进行测试一下吧。
注意1:直接在 sql 中写 limit,safeRowBoundsEnabled无效
select * from lz_user lu left join lz_user_bill lub on lu.id =lub.user_id where lu.id =456 limit 1
注意2:直接调用 UserMapper,传入RowBounds,safeRowBoundsEnabled无效
@Test public void test3() throws Exception { SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class) UserBillInfo userBillInfo = userMapper.selectUserBill(456l,new RowBounds(0,5)); System.out.println(JSON.toJSONString(userBillInfo)); }
通过上面分析,发现safeRowBoundsEnabled属性的使用还是有很多限制的,而且加了这个限制给我们开发也带来不便,还是建义正常情况下,不要去设置该属性,使用默认配置好了。
lazyLoadTriggerMethods属性使用
指定哪个对象的方法触发一次延迟加载,用逗号分隔的方法列表,默认值equals,clone,hashCode,toString。
select * from lz_user_bill where user_id = #{id} select * from lz_user lu where lu.id =#{id}
为了使得测试有效果,lazyLoadingEnabled=true表示开启缓存,如果设置为 false,数据每次查询都是全部加载,看不到效果。aggressiveLazyLoading为 true时,只要触发到对象任何的方法,就会立即加载所有属性的加载,因此aggressiveLazyLoading为 false才能看出效果。本次测试,对于对象equals方法不执行加载,hashCode方法执行,执行延迟加载。
@Test public void findUserBillLazyLoading() throws Exception { SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.findUserById(456l); System.out.println("-------------equals方法执行---------"); System.out.println("equals result : " + user.equals(new User())); System.out.println("-------------hashCode方法执行---------"); System.out.println("hashCode: " + user.hashCode()); }
localCacheScope介绍:MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据,这句话是什么意思呢?我们来测试一下吧。
每次查询都调用DataScopeInterceptor方法打印 SQL。
准备UserMapper.xml
select * from lz_user where id=#{id}
测试
@Test public void testGetUser() throws Exception { SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.getUser(456l); System.out.println("============================"); user = userMapper.getUser(456l); System.out.println(JSON.toJSONString(user)); }
上面测试中,我们执行了两条一模一样查询。
4.结果
publicList query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) throw new ExecutorException("Executor was closed."); if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List list; try { queryStack++; //从本地缓存中查找数据 list = resultHandler == null ? (List ) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { //从数据库中获取数据 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } deferredLoads.clear(); // issue #601 //如果localCacheScope为STATEMENT,每次清理localCache缓存 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { clearLocalCache(); // issue #482 } } return list; }
通过结果和源码分析,我们很容易理解本地缓存机制了。
对于safeResultHandlerEnabled的使用和safeRowBoundsEnabled使用非常的相似,如果此属性设置为 true,表示不允许用户自定义 ResultHandler.
下面来看示例:
select * from lz_user lu left join lz_user_bill lub on lu.id =lub.user_id where lu.id =456
public class MyDefaultResultSetHandler implements ResultHandler { @Override public void handleResult(ResultContext context) { System.out.println("============================="); } }
@Test public void testGetUser1() throws Exception { SqlSession sqlSession = sqlSessionFactory.openSession(); sqlSession.select("com.spring_101_200.test_131_140.test_139_mybatis_saferesulthandlerenabled.UserMapper.selectUserBill", new MyDefaultResultSetHandler()); }
protected void checkResultHandler() { //如果有用户自定义resultHandler并且safeResultHandlerEnabled为 true,结果集非排序,抛出异常 if (resultHandler != null && configuration.isSafeResultHandlerEnabled() && !mappedStatement.isResultOrdered()) { throw new ExecutorException("Mapped Statements with nested result mappings cannot be safely used with a custom ResultHandler. " + "Use safeResultHandlerEnabled=false setting to bypass this check " + "or ensure your statement returns ordered data and set resultOrdered=true on it."); } }
我们在解析environments之前,先来看看业务需求,我们开发的时候,肯定开发环境的数据库和线上环境的数据库是不一样的,而我们不可能每次发布线上时,都修改数据库的具体配置,那怎么办呢?MyBatis给我们提供了解决方案。
配置环境可以注册多个数据源(datasource),每一个数据源分成两大部分,一个是数据源配置,另外一个是数据库事务(TransactionManager) 的配置,我们来看看一个连接池的数据源的配置。
online">
我们分析一下上面的配置。
根据不同的开发环境,我们配置不同的environment标签,每次切换环境时,只需要修改environments的default属性,就可以达到不同环境的切换,这样能给开发人员带来方便,同时降低修改配置错误风险,下面来看看,MyBatis 是如何解析environments的。
private void environmentsElement(XNode context) throws Exception { if (context != null) { //获取environments标签中的default值 if (environment == null) { environment = context.getStringAttribute("default"); } //遍历所有的子节点environment for (XNode child : context.getChildren()) { String id = child.getStringAttribute("id"); //如果子节点environment的 id 等于environment 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设置环境 configuration.setEnvironment(environmentBuilder.build()); } } } }
数据库事务
数据库事务 MyBatis 是由 SqlSession去控制的,我们可以通过 SqlSession提交 commit 或者回滚 rollback,我们插入一个解析对象,如果成功提交,否则就回滚。
try{
sqlSession = SqlSessionFactoryUtil.openSqlSession();
UserMapper roleMapper = sqlSession.getMapper(UserMapper.class);
int count = roleMapper.inserUser();
sqlSession.commit();
return count ;
}
数据源:
MyBatis 内部为我们提供了3种数据源的方式。
我们只需要将数据源属性 type 定义为 UNPOOLED,POOLED,JNDI 即可。
这3种实现方式比较简单,只需要配置参数即可,但是有的时候需要使用其他的数据源,如果使用自定义数据源,它必需实现 org.apache.ibatis.datasource.DataSourceFactory接口,比如说我们要实现 DBCP 数据源。
public class DbcpDataSourceFactory extends BasicDataSource implements DataSourceFactory { private Properties props = null; @Override public Logger getParentLogger() throws SQLFeatureNotSupportedException { return null; } @Override public void setProperties(Properties props) { this.props = props; } @Override public DataSource getDataSource() { DataSource dataSource = null; try { dataSource = BasicDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); } return null; } }
使用 DBCP数据源需要我们提供一个类去配置它。我们按照下面的方法配置就可以使用 DBCP 数据源了
在相同的数据库厂商的环境下,数据库厂商标识没有什么意义,在实际的应用中,使用的比较少,因为使用不同的厂商数据库系统还是比较少的,MyBatis 可能会运行在不同的厂商的数据库中,它为此提供了一个数据库标识,并提供自定义,它的作用是指定 SQL到对应的数据库厂商提供的数据库中运行。
mybatis 不能像hibernate一样,写一套HQL就实现不同数据库的任意切换。 mybatis 需要根据不同的环境写不同的sql,因此DatabaseIdProvider是区分不同的数据库环境的。
在mybatis-config.xml添加如下
type = "DB_VENDOR"是启动 MyBatis 内部注册的策略器,首先 MyBatis会将你配置读入 Configuration类里面,在连接数据库后调用 getDatabaseProductName()方法去获取数据库信息,然后用我们配置的 name值去匹配来得到 DatabaseId,我们把这些配置到我们的例子里,而我们的例子使用正是 MySQL 数据库,这个时候,我们可以用下面的代码来获得数据库的 ID,显然结果就是 MySQL。
sqlSessionFactory.getConfiguration().getDatabaseId();
我们也可以指定 SQL在哪个数据库厂商执行,我们把 Mapper 的 XML 配置修改一下,如下所示:
select * from lz_user where id=#{id}
在多了一个 datasourceId 属性的情况下,MyBatis将提供如下规则 。
MyBatis 也提供了规则允许自定义,我们只要实现了 databaseIdProvider接口,并且实现配置即可,下面我们来看看这个例子。
public class MydatabaseIdProvider implements DatabaseIdProvider { private Properties properties; @Override public void setProperties(Properties properties) { this.properties = properties; } @Override public String getDatabaseId(DataSource dataSource) throws SQLException { String dbName = dataSource.getConnection().getMetaData().getDatabaseProductName(); String dbId = (String)this.properties.get(dbName); return dbId; } }
其次,注册这个类到 MyBatis上下文环境中,我们这样配置 DatabaseIdProvide标签。
我们将 type修改为我们自己实现的类,类里面 stProperties方法参数传递进去的将会是我们在 XML 里配置的信息,我们保存在类的变量 properties 里,方便以后读出,在方法 getDatabaseId()中,传递的参数是数据库数据源,我们获取其名称,然后通过 properties的键值找到对应的 databaseId。
如果有特殊的要求,我们可以根据自己需要的规则来编写 databaseIdProvider,配置 Mapper,使用规则和默认的规则是一致的。
下面我们来看看databaseIdProvider标签是如何解析的
private void databaseIdProviderElement(XNode context) throws Exception { DatabaseIdProvider databaseIdProvider = null; if (context != null) { //如果 type = "VENDOR",设置了DB_VENDOR String type = context.getStringAttribute("type"); if ("VENDOR".equals(type)) type = "DB_VENDOR"; Properties properties = context.getChildrenAsProperties(); databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance(); //获取databaseIdProvider标签下的property标签下属性 databaseIdProvider.setProperties(properties); } Environment environment = configuration.getEnvironment(); if (environment != null && databaseIdProvider != null) { String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource()); configuration.setDatabaseId(databaseId); } }
public String getDatabaseId(DataSource dataSource) { if (dataSource == null) throw new NullPointerException("dataSource cannot be null"); try { //获取数据库名 return getDatabaseName(dataSource); } catch (Exception e) { log.error("Could not get a databaseId from dataSource", e); } return null; }
MyBatis在预处理语句(PreparedStatement)中设置一个参数时,或者从结果集(ResultSet) 中取出一个值时,都会用注册了 typeHandeler进行处理。
由于数据库可能来自不同的厂商,不同的厂商设置的参数可能有所不同,同时数据库也可以自定义数据类型,TypeHandler允许根据项目的需要自定义设置 java 传递到数据库的参数中,或者从数据库中读取数据 ,我们也需要进行特殊的处理,这些都可以在自定义的 typeHandler 中处理,尤其是在使用枚举的时候我们常常需要使用 typeHandler进行转换。
typeHandler和别名一样,分别为 MyBatis 系统定义和用户自定义两种,一般来说,使用 MyBatis系统定义就可以实现大部分的功能,如果使用用户自定义的 typeHandler,我们处理的时候务必小心谨慎,以避免出现不必要的错误。
typeHandler常用的配置为 Java 类型(javaType),JDBC类型( jdbcType),typeHandler的使用就是将参数 javaType 转化为 jdbcType,或者从数据库中取出结果时将 jdbcType转化为 javaType。
MyBatis
public TypeHandlerRegistry() { register(Boolean.class, new BooleanTypeHandler()); register(boolean.class, new BooleanTypeHandler()); register(JdbcType.BOOLEAN, new BooleanTypeHandler()); register(JdbcType.BIT, new BooleanTypeHandler()); ... register(java.sql.Date.class, new SqlDateTypeHandler()); register(java.sql.Time.class, new SqlTimeTypeHandler()); register(java.sql.Timestamp.class, new SqlTimestampTypeHandler()); // issue #273 register(Character.class, new CharacterTypeHandler()); register(char.class, new CharacterTypeHandler()); }
这便是系统为我们注册的 typeHandler,目前 MyBatis为我们注册了多个 TypeHandler,让我们来看看下面的表格,从而理解typeHandler 对应的 Java 类型的 jdbc 类型
类型处理器 | Java 类型 | JDBC 类型 |
---|---|---|
BooleanTypeHandler | java.lang.Boolean,boolean | 数据库兼容的 BOOLEAN |
ByteTypeHandler | java.lang.Byte,byte | 数据库兼容的 NUMERIC或BYTE |
ShortTypeHandler | java.lang.Short,short | 数据库兼容类型 NUMERIC 或 SHORT INTEGER |
IntegerTypeHandler | java.lang.Integer,int | 数据库兼容的NUMERIC 或 INTEGER |
LongTypeHandler | java.lang.Long,long | 数据库兼容的 NUMERIC 或 LONG INTEGER |
FloatTypeHandler | java.lang.Float,float | 数据库兼容类型 NUMERIC或 FLOAT |
DoubleTypeHandler | java.lang.Double,double | 数据库兼容类型 NUMBERIC 或 DOUBLE |
BigDecimalTypeHandler | java.math.BigDecimal | 数据库兼容类型NUMERIC 或 DECIMAL |
StringTypeHandler | java.lang.String | CHAR,VARCHAR |
ClobTypeHandler | java.lang.String | CLOB,LONGVARCHAR |
NStringTypeHandler | java.lang.String | NVARCHAR,NCHAR |
NClobTypeHandler | java.lang.String | NCLOB |
ByteArrayTypeHandler | byte[] | 数据库兼容的字节流类型 |
BlobTypeHandler | byte[] | BLOB,LONGVARBINARY |
DateTypeHandler | java.util.Date | TIMESTAMP |
DateOnlyTypeHandlller | java.util.Date | DATE |
TimeOnlyTypeHandler | java.util.Date | TIME |
SqlTimestampTypeHandler | java.sql.Timestamp | TIMESTAMP |
SqlDateTypeHandler | java.sql.Date | DATE |
SqlTimeTypeHandler | java.sql.Date | DATE |
SqlTimeTypeHandler | java.sql.Time | TIME |
ObjectTypeHandler | Any | OTHER 或未指定的类型 |
EnumTypeHandler | Enumeration Type | VARCHAR或任何兼容的字符串类型,存储枚举的名称(而不是索引 ) |
EnumOrdinalTypeHandler | Enumeration Type | 任何兼容的 NUMERIC 或 DOUBLE 类型,存储枚举的索引(而不是名称) |
我们需要注意下面的几点。
对于typeHandlers标签解析之前,我们先来看看关于typeHandlers标签的使用。
1.添加 POJO
@Data public class User { private Long id; private Integer isDelete; private Date gmtCreate; private Date gmtModified; private String username; private String password; private PhoneNumber realName; private Long managerId; }
@Data public class PhoneNumber { private String phone ; public PhoneNumber() { } public PhoneNumber(String phone) { this.phone = phone; } }
public class PhoneTypeHandler extends BaseTypeHandler{ //使用列名进行封装 @Override public PhoneNumber getNullableResult(ResultSet rs, String columnName) throws SQLException { return new PhoneNumber(rs.getString(columnName)); } //使用列的下标进行封装 @Override public PhoneNumber getNullableResult(ResultSet rs, int i) throws SQLException { return new PhoneNumber(rs.getString(i)); } //CallableStatement遇到PhoneNumber,如何设置参数 @Override public PhoneNumber getNullableResult(CallableStatement cs, int i) throws SQLException { return null; } //PreparedStatement遇到PhoneNumber,如何设置参数 @Override public void setNonNullParameter(PreparedStatement ps, int i, PhoneNumber phoneNumber, JdbcType type) throws SQLException { ps.setString(i, phoneNumber.toString()); } }
PhoneTypeHandler继承了 BaseTypeHandler,而 BaseTypeHandler实现了接口 typeHandler,并且自定义了4个抽象方法,所以在继承它的时候,正如本例一样需要实现其定义的4个抽象方法,这些方法己经在PhoneTypeHandler中用@Override 注解注明了。
setParameter 是 PreparedStatement对象设置的参数,它允许我们自己填写变换规则 。
getResult则分别为 ResultSet 则列名(ColumnName)或者使用列下标(colunmnIndex) 来获取结果数据,其中还包括用 CallableStatement(存储过程) 获取结果及数据方法。
2.在mybatis-config.xml添加
3.测试:
我们看到,realName字段转化成了PhoneNumber对象。了解了typeHandler使用之后,我们再来看看 mybatis 源码中是如何使用的。
MyBatis 内部提供了两种转化枚举类型的 typeHandler 给我们使用。
其中EnumTypeHandler是使用枚举字符串名称作为参数传递的,EnumOrdinalTypeHandler是使用整数下标作为参数传递的,如果枚举和数据库字典项保持一致,我们使用它们就可以了,然而这两个枚举类型的应用不是那么广泛,更多的时候我们希望使用自定义的 typeHandler处理他们。所以这里我们也会谈及自定义的 typeHandler实现枚举映射 。
下面以性别为例,讲述如何实现枚举类,现在我们有一个性别枚举,它定义了字典男(male),女(female),那么我们就可以轻易得到一个枚举类,如下面代码。
public enum Sex { MALE(1, "男"), FEMALE(2, "女"); private int id; private String name; private Sex(int id, String name) { this.id = id; this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Sex{" + "id=" + id + ", name='" + name + '\'' + '}'; } } @Data public class User { private Long id; private Integer isDelete; private Date gmtCreate; private Date gmtModified; private String username; private String password; private PhoneNumber realName; private Long managerId; private Sex sex; }
在没有配置的时候,EnumOrdinalTypeHandler是 MyBatis默认的枚举类型处理器,为了让 EnumOridinalTypeHandler能够处理它,我们在 MyBatis做如下配置,代码如下:
select * from lz_user where id=#{id}
结果正如我们所料,如下图所示
EnumTypeHandler是使用枚举名称去处理 Java枚举类型,EnumTypeHandler对应的是一个字符串,让我们来看看他的用法
在数据库中增加一个VARCHAR 类型的字段 sex_str,然后修改映射XML文件,这时我们在映射文件里做了全部的限定描述(javaType,jdbcType,typeHandler全配置,这样就不需要在 MyBatis 配置文件里再进行配置了)
准备POJO
@Data public class User { private Long id; private Integer isDelete; private Date gmtCreate; private Date gmtModified; private String username; private String password; private PhoneNumber realName; private Long managerId; private Sex sex; private Sex sexStr; }
@Test public void test1() throws Exception { SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.getUserByMap(456l); System.out.println("sex:"+user.getSex()); System.out.println(user.getSexStr()); }
然后 POJO 的属性 sex 从整形修改为 String型就可以了,EnumTypeHandler是通过 Enum.name方法将转化为字符串,通过 Enum.valueOf 方法将字符串转化为枚举的。
最后执行结果如下:
private void typeHandler(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { //如果配置了 package 标签,取其 name String typeHandlerPackage = child.getStringAttribute("name"); typeHandlerRegistry.register(typeHandlerPackage); } else { String javaTypeName = child.getStringAttribute("javaType"); String jdbcTypeName = child.getStringAttribute("jdbcType"); //如果没有,获取 handlerName String handlerTypeName = child.getStringAttribute("handler"); Class> javaTypeClass = resolveClass(javaTypeName); JdbcType jdbcType = resolveJdbcType(jdbcTypeName); Class> typeHandlerClass = resolveClass(handlerTypeName); //注册类型处理器 if (javaTypeClass != null) { if (jdbcType == null) { typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); } else { typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); } } else { typeHandlerRegistry.register(typeHandlerClass); } } } } }
映射器是 MyBatis最复杂的,最核心的组件,本节着重讨论如何引入映射器,而它的参数类型,动态 SQL,定义 SQL,缓存信息等功能我们会在以后的博客中再讨论。
我们再加回顾一下映射器的使用。
public interface UserMapper { void insertUser(User user); User getUser(Long id); @Select("select * from lz_user where id=#{id}") User getUserInfo(Long id); }
xml 文件
INSERT INTO lz_user (username, password, real_name, manager_id) VALUES (#{username},#{password},#{realName},#{managerId}) select * from lz_user where id=#{id}
引入映射器的方式有很多种,一般分成以下几种
下面是关于映射器的解析
private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { //如果子标签是 package if ("package".equals(child.getName())) { //遍历包名下的所有类并注册 ////注意:要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中。 String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); //如果resource不为空,url,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(); //如果 url 不为空,其他的为空 // } 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(); //如果mapperClass不为空,其他的为空 // //注意:要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中。 } else if (resource == null && url == null && mapperClass != null) { Class> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { //如果resource,url,class 都为空 throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }
文章到这里,己经将 mybatis 的基本配置的使用及解析一一做了介绍了,也让我们对 mybatis 的使用有了一个大概的了解,mybatis 是如何根据这些配置来实现相应的功能的呢?这个问题,我们留给后面的博客。看看解析的流程。
1) getResourceAsReader():根据resource获取Reader 1) new XMLConfigBuilder(): 创建config 解析器 2) build():创建DefaultSqlSessionFactory工厂 1)parse():解析mybatis-config.xml 1)parseConfiguration():解析configuration根元素 1)propertiesElement(): 解析properties标签,定义配置外在化 2)typeAliasesElement():解析typeAliases标签,为一些类定义别名 3)pluginElement():解析plugin元素,增加一些拦截器 4)objectFactoryElement():解析objectFactory标签,用于指定结果集对象的实例是如何创建的。 5)objectWrapperFactoryElement():解析objectWrapperFactory标签,构造返回对象的时候,对于指定的对象进行特殊的加工 6)settingsElement():解析settings标签,一些全局的配置 7)environmentsElement():解析environments标签,环境 8)databaseIdProviderElement():解析databaseIdProvider标签,数据源区分 9)typeHandlerElement():解析typeHandler标签,定义类型处理,也就是定义 Java类型也数据库中数据类型之间的转换关系。 10)mapperElement():解析mapper标签,指定映射文件或映射类
关于 mybatis 的配置和使用,我在网上看到 MyBatis 的配置文件设置 文章,有兴趣的同学可以去研究一下。
本文的 github地址是https://github.com/quyixiao/spring_tiny/tree/master/src/main/java/com/spring_1_100/test_61_70/test70_mybatis_alone