JDBC中我们调用数据库获得的数据都是结果集,需要运用java底层来运作。
XML语言
XML长什么样
Mybatis使用简单的XML或者注解来进行数据库操作。XML语言最早用于数据的存储和传输,跟HTML长得几乎差不多,但意义不同。
1 | <? xml version="1.0"encoding="utf-8"?> |
XML语言规范
规范如下:
- 必须存在一个根节点,子标签全部包含
<outer> - 可以但不必须包含一个头部声明
<xml> - 标签成对出现
- 区分大小写
- 标签可以存在属性,这里跟HTML有点相似
- 可以使用注释:`
- 还有如下转义字符,防止某些字符成为关键字无法使用

那么我们现在需要解析一个XML文件到Java程序当中。而JDK为我们内置了一个叫做org.w3c的XML解析库,而解析原理如下:
1 | // 创建DocumentBuilderFactory对象 |
了解这个原理倒是不必,因为我也看不懂。但是我们需要知道有这个东西,看一下就好。后期用到的Spring等框架也会用到XML来作为框架的配置文件。
Mybatis体验一下
如果在Idea中使用Mybatis
首先分为以下几步↓
- 创建mybatis-config.xml文件在根目录下
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${驱动类(含包名)}"/>
<property name="url" value="${数据库连接URL}"/>
<property name="username" value="${用户名}"/>
<property name="password" value="${密码}"/>
</dataSource>
</environment>
</environments>
</configuration> - 然后填入:
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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/study"/>
<property name="username" value="root"/>
<property name="password" value="mima"/>
</dataSource>
</environment>
</environments>
</configuration>
SqlSessionFactory对象
首先我们需要用SqlSessionFactoryBuilder中build一个SqlSessionFactory对象,然后记得参数为咱们上面的配置文件。
接着我们就得到了一个工厂,这个工厂就可以用来获得一堆跟数据库有关地对象。
1 | public static void main(String[] args) throws FileNotFoundException { |
接着我们可以得到多个会话SqlSession,每个会话就相当于我不同的地方登陆同一个账号去访问数据库,也就是我们JDBC中的Statement对象。会话之间相互隔离,没有任何关联。
试着把数据库学生表当中的数据全部给Student类
我们需要在根目录下重新创建一个mapper文件夹,新建一个名为TestMapper.xml的文件作为我们的映射器,并填写以下内容:
1 |
|
这里的resultType就是要把数据映射为哪个类。
MyBatis 用的是反射机制:
- 根据
resultType,用Class.forName("test.Student")获取类的信息 - 调用
newInstance()创建对象(需要Student类有一个无参构造方法) - 找到每个属性对应的
setter方法,比如setId()、setName() - 把数据库列的值,通过
setter方法赋值给对象属性
接着在配置文件中添加一个Mapper映射器:
1 | <mappers> |
最后在main方法中写入:
1 | public static void main(String[] args) throws FileNotFoundException { |
打印:
1 | Student(sid=1940618802, name=小强, sex=男) |
配置Mybatis
如果我想每次获得一个会话,都得sqlSessionFactory.openSession岂不是很麻烦。所以我们可以试着用一个类来实现这样的方法:
1 | public class MybatisUtil { |
这样我们只需要一段这样代码就可以查询:
1 | try (SqlSession sqlSession = MybatisUtil.getSession(true)){ |
接着我们来用一个接口实现SQL语句的返回特定类:
1 | package mapper; |
写好接口后,我们还需要写一下匹配器:
1 |
|
namespace这里需要变成我们的接口,并且这里的id需要和我们接口的方法名称一模一样,这样当我们调用接口对象的selectStudent方法时会运行mysql语句并匹配类然后返回对象。
1 | try (SqlSession sqlSession = MybatisUtil.getSession(true)){ |
我们回来继续讲解一下原配置文件xml中的标签:
- environments:里面有一个属性叫做环境变量,一个环境变量对应了一个数据库。我们在自己电脑上调试时一般是自己的开发环境
development,而到别人电脑上可能就不一样。 - transactionManager:事务管理器,一般是
JDBC - dataSource:一般用
POOLED就行 - typeAliases:起一个别名,当然还有很多用法,看Mybatis文档就好了。
1 | <typeAlias type="test.Student" alias="qqq"; |
还有很多,后面再说。
增删改查操作
resultMap方法与类匹配标签
我们在使用映射操作时,我们会发现,哪怕Student类的第一个变量名字不叫sid,也可以映射查询成果。那么我们可以在映射器xml配置文件中,使用resultMap标签来操控映射结果:
1 |
|
当我们类的构造方法超过1个时,除非拥有一个对的上数据库属性的有参构造方法,否则将会映射失败,这个时候就需要使用resultMap来选定构造方法:
1 | <resultMap id="Test" type="Student"> |
但是这里如果给构造方法的话,那么会默认在构造方法中赋值,如果构造方法中没有赋值,将会导致值缺失。
1 | 构造方法2 xxx:1940618802 |
settings设置标签
如果数据库中存在一个带下划线的字段,我们可以用settings标签中的mapUnderscoreToCamelCase来让其映射为驼峰命名:
1 | <settings> |
开启后我们数据库属性是my_test,如果想映射到类的int属性的话,该类属性字段应为myTest,否则映射失败。
查询单个
1 | public interface TestSelect { |
我们需要再给接口写一个方法,然后在映射器配置文件中添加新的<select>:
1 | <select id="selectStudentOneSid" resultType="Student"> |
注意这里的 #{sid}和${sid}的区别,一个是类似于prepareStatement的预编译,一个不是。#可以有效防止sql攻击,而$不行,所以一般用#。
insert插入一条数据
1 | public interface TestSelect { |
新写方法addStudent,然后在映射器配置文件中修改:
1 | <insert id="addStudent"> |
这里#里面的是我们类的字段,如果插入成功将会返回操作行数。
delete删除一条数据
和插入差不多:
1 | public interface TestSelect { |
1 | <delete id="deleteStudent"> |
复杂查询
一对多查询
一个老师可以教多个学生,我们可以直接把数据库中的学生全部映射给老师对象:
1 |
|
接着我们在映射器中写入以下:
1 | <select id="getTeacherByTid" resultMap="asTeacher"> |
这里我要讲解一下<id column="tid" property="tid"/>是什么,Mybatis是根据查询出来的每一行进行映射的,每一行都会走一遍映射,所以id作用就是告诉Mybatis,如果tid相同的话,那么这是一个对象。而collection就是告诉Mybatis这里面的东西是add加进去的,而不是直接赋值。
多对一查询
每个学生都有一个对应的老师,查询老师为张三的所有学生:
1 | public class Student { |
然后我们的映射器这样写:
1 | <resultMap id="test2" type="Student"> |
这里需要讲解下<association>标签,这个标签会导致只映射一个对象,也就是说在一个Student对象中只会拥有一个teacher。
我们在接口新加一个这样的方法:
1 | public interface TestSelect { |
事务操作
我们可以在Sqlsession关闭自动提交来开启事务模式,与JDBC差不多:
1 | public static void main(String[] args) { |
我们会发现代码运行完了,但是小王并没有被插入进去,因为没有提交。
如果想要提交,只需要在最后面写sqlSession.commit()就好了。
动态SQL
动态SQL就是可以在XML配置文件中的sql语句进行条件判断,比如这里就使用了if标签来判断:
- 如果
sid是偶数,那么只输出男生
1 | <delete id="deleteStudent"> |
当然还有choose等类似于JAVA中switch作用的标签,不过具体是什么就要自己看文档去了。
缓存机制
Mybatis内置了缓存机制,分为一级缓存和二级缓存。缓存的主要目的类似于Java本身的IO缓冲区,避免频繁从数据库中调用先前已经调用过的数据。比如我们来查询一串相同的Student数据从数据库中:
1 | public static void main(String[] args) throws InterruptedException { |
这个时候换回结果
true
说明student1和student2是同个对象,也就是说Mybatis会自己判断是否为同对象,而停止调用。那么这个东西叫做一级缓存。一级缓存是无法停止的,只能更改。
但这个东西是有限制的,比如我们以下代码:
1 | public static void main(String[] args) throws InterruptedException { |
false
这里为什么会变成false?因为中间出现了一次插入操作,导致数据库更新了,那么就会清空缓存。
还有其他操作也会清空缓存,比如该会话session停止,也会清空缓存。
而我们上面所说的是一级缓存,多个会话之间的一级缓存是不共享的。

而且一个DML操作只会清空该会话的缓存,这里也要注意。
那么我们现在想一下,一个会话结束会导致一级缓存内东西失效,那么我们能不能用这种跨会话的二级缓存?当然可以,不过二级缓存默认是关闭状态,我们需要在映射器xml中手动开启,而之后查找会优先查找二级缓存,才会是一级缓存。我们只需要添加一行就好了:
1 | <cache/> |
但是需要某个会话结束,才会把这个会话的东西写入二级缓存。当然了我们还可以给某个特定方法写二级缓存useCache不过这个的话还是自己到时候查文档吧。
但是缓存会暴露出一个问题,就是缓存一致性问题,假如两个线程访问数据库,第一个线程已经把小明的信息录入了缓存,可是线程二修改了小明数据。那么线程一在短时间内搜索到的一直还是缓存中的旧数据。而这种问题目前解决方法就是停止二级缓存,清空一级缓存;或者实现缓存共用。
使用注解的方式操纵Mybatis
简单查询
我们只前是编写了一个mapperXML文件来进行映射,你不觉得这个还是很麻烦吗?那么有没有什么办法不用写这东西来进行操纵数据库,有的兄弟,有的。
在主配置文件的映射器列表写你的接口包,使得这里面每个接口都是映射器:
1 | <mappers> |
而我们的接口这样写:
1 | package mapper; |
复杂查询
我们先看下一对多的关系,查询一个老师所教的所有学生。我们会使用到@Many来进行:
1 |
|
同样的,@Result也提供了实现元素对应的表示的方法,同时其中的@One注解可以表示是否为同一个对象,类似于之前的assocation标签:
1 |
|
如果我们还想使用XML中的ResultMap的话,可以用@ResultMap注解来使用,不过还是得要把XML文件写下:
1 |
|
我们发现,当参数列表中出现两个以上的参数时,会出现错误:
1 |
|
原因是Mybatis不明确到底哪个参数是什么,因此我们可以添加@Param来指定参数名称:
1 |
|
那么我们还会遇到以下问题,就是该类的构造方法中我们参数没有设置好,比如这样:
1 |
|
结果和constructor标签效果一致。当然得还可以这样↓:
1 |
|
那么如何通过注解控制缓存机制呢?
1 |
|
使用@CacheNamespace注解直接定义在接口上即可,然后我们可以通过使用@Options来控制单个操作的缓存启用。
探究Mapper的动态代理机制
动态代理机制非常非常复杂,我个人是这样觉得,不过已经弄明白了。
- 接口
- 接口实现类(可有可没有)
- 动态类(实现了InvocationHandler接口的类)
然后我们的接口可以直接作为一个动态生成的对象的引用,接着这个对象当中会保存着我们动态类对象,该动态生成的对象中实现了我们的接口。当我们调用接口方法时,会将 - 方法名
- 参数
传入到我们的动态类对象的invoke方法当中,在invoke方法当中有我们接口的方法对象,并且动态类对象中有我们的接口实现类,所以可以直接在这里面调用我们的接口实现类方法。神奇吧😋,这一切的一切都是动态生成的,我们并没有明确应该怎样做,只是传入了该传入的东西,而类接口实现类(动态生成的对象)就自然而然地形成了。
仔细想来也是:
1 | TestSelect testSelect = sqlSession.getMapper(TestSelect.class); |
仅凭这一行代码,Mybatis不可能一开始就有我们接口的实现类对象,所以他也是在程序运行中根据接口类型创建的,这种就叫动态代理。而像我们直接在本地文件中进行实现类对象,这种操作为静态代理,也就是我们学习到现在一直在用的。
说些什么吧!