DataBase-MyBatis初级学习
MyBatis初级
目标
- 优点
- # 和 $
- 插件机制
- 缓存机制
- 连接池
- 动态SQL
- 分页
- MyBatisPlus
基本介绍
ORM(Object Relational Mapping): 对象关系映射,指的是持久化数据和实体对象的映射模式,解决面向对象与关系型数据库存在的互不匹配的现象
MyBatis:
-
MyBatis 是一个优秀的基于 Java 的持久层框架,它内部封装了 JDBC,使开发者只需关注 SQL 语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 Statement 等过程。
-
MyBatis 通过 XML 或注解的方式将要执行的各种 Statement 配置起来,并通过 Java 对象和 Statement 中 SQL 的动态参数进行映射生成最终执行的 SQL 语句。
-
MyBatis 框架执行 SQL 并将结果映射为 Java 对象并返回。采用 ORM 思想解决了实体和数据库映射的问题,对 JDBC 进行了封装,屏蔽了 JDBC 底层 API 的调用细节,使我们不用操作 JDBC API,就可以完成对数据库的持久化操作。
#{}和${}
#{}:占位符,传入的内容会作为字符串加上引号,以预编译的方式传入,将 sql 中的 #{} 替换为 ? 号,调用 PreparedStatement 的 set 方法来赋值,有效的防止 SQL 注入,提高系统安全性
${}:拼接符,传入的内容会直接替换拼接,不会加上引号,可能存在 sql 注入的安全隐患
-
能用 #{} 的地方就用 #{},不用或少用 ${}
-
必须使用 ${} 的情况:
- 表名作参数时,如:
SELECT * FROM ${tableName}
- order by 时,如:
SELECT * FROM t_user ORDER BY ${columnName}
- 表名作参数时,如:
-
sql 语句使用 #{},properties 文件内容获取使用 ${}
缓存机制
缓存概述
缓存:缓存就是一块内存空间,保存临时数据
作用:将数据源(数据库或者文件)中的数据读取出来存放到缓存中,再次获取时直接从缓存中获取,可以减少和数据库交互的次数,提升程序的性能
缓存适用:
- 适用于缓存的:经常查询但不经常修改的,数据的正确与否对最终结果影响不大的
- 不适用缓存的:经常改变的数据 , 敏感数据(例如:股市的牌价,银行的汇率,银行卡里面的钱)等等
缓存类别:
- 一级缓存:SqlSession 级别的缓存,又叫本地会话缓存,自带的(不需要配置),一级缓存的生命周期与 SqlSession 一致。在操作数据库时需要构造 SqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据,不同的 SqlSession 之间的缓存数据区域是互相不影响的
- 二级缓存:mapper(namespace)级别的缓存,二级缓存的使用,需要手动开启(需要配置)。多个 SqlSession 去操作同一个 Mapper 的 SQL 可以共用二级缓存,二级缓存是跨 SqlSession 的
开启缓存:配置核心配置文件中
- cacheEnabled:true 表示全局性地开启所有映射器配置文件中已配置的任何缓存,默认 true
参考文章:https://www.cnblogs.com/ysocean/p/7342498.html
一级缓存
一级缓存是 SqlSession 级别的缓存
工作流程:第一次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,如果没有,从数据库查询用户信息,得到用户信息,将用户信息存储到一级缓存中;第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存中获取用户信息。
一级缓存的失效:
- SqlSession 不同
- SqlSession 相同,查询条件不同时(还未缓存该数据)
- SqlSession 相同,手动清除了一级缓存,调用
sqlSession.clearCache()
- SqlSession 相同,执行 commit 操作或者执行插入、更新、删除,清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读
Spring 整合 MyBatis 后,一级缓存作用:
- 未开启事务的情况,每次查询 Spring 都会创建新的 SqlSession,因此一级缓存失效
- 开启事务的情况,Spring 使用 ThreadLocal 获取当前资源绑定同一个 SqlSession,因此此时一级缓存是有效的
测试一级缓存存在
1 | public void testFirstLevelCache(){ |
二级缓存
基本介绍
二级缓存是 mapper 的缓存,只要是同一个命名空间(namespace)的 SqlSession 就共享二级缓存的内容,并且可以操作二级缓存
作用:作用范围是整个应用,可以跨线程使用,适合缓存一些修改较少的数据
工作流程:一个会话查询数据,这个数据就会被放在当前会话的一级缓存中,如果会话关闭或提交一级缓存中的数据会保存到二级缓存
二级缓存的基本使用:
-
在 MyBatisConfig.xml 文件开启二级缓存,cacheEnabled 默认值为 true,所以这一步可以省略不配置
1
2
3
4<!--配置开启二级缓存-->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings> -
配置 Mapper 映射文件
<cache>
标签表示当前这个 mapper 映射将使用二级缓存,区分的标准就看 mapper 的 namespace 值1
2
3
4
5<mapper namespace="dao.UserDao">
<!--开启user支持二级缓存-->
<cache eviction="FIFO" flushInterval="6000" readOnly="" size="1024"/>
<cache></cache> <!--则表示所有属性使用默认值-->
</mapper>eviction(清除策略):
LRU
– 最近最少使用:移除最长时间不被使用的对象,默认FIFO
– 先进先出:按对象进入缓存的顺序来移除它们SOFT
– 软引用:基于垃圾回收器状态和软引用规则移除对象WEAK
– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象
flushInterval(刷新间隔):可以设置为任意的正整数, 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新
size(引用数目):缓存存放多少元素,默认值是 1024
readOnly(只读):可以被设置为 true 或 false
- 只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,促进了性能提升
- 可读写的缓存会(通过序列化)返回缓存对象的拷贝, 速度上会慢一些,但是更安全,因此默认值是 false
type:指定自定义缓存的全类名,实现 Cache 接口即可
-
要进行二级缓存的类必须实现 java.io.Serializable 接口,可以使用序列化方式来保存对象。
1
public class User implements Serializable{}
相关属性
-
select 标签的 useCache 属性
映射文件中的
<select>
标签中设置useCache="true"
代表当前 statement 要使用二级缓存(默认)注意:如果每次查询都需要最新的数据 sql,要设置成 useCache=false,禁用二级缓存
1
2
3<select id="findAll" resultType="user" useCache="true">
select * from user
</select> -
每个增删改标签都有 flushCache 属性,默认为 true,代表在执行增删改之后就会清除一、二级缓存,保证缓存的一致性;而查询标签默认值为 false,所以查询不会清空缓存
-
localCacheScope:本地缓存作用域,
中的配置项,默认值为 SESSION,当前会话的所有数据保存在会话缓存中,设置为 STATEMENT 禁用一级缓存
源码解析
事务提交二级缓存才生效:DefaultSqlSession 调用 commit() 时会回调 executor.commit()
-
CachingExecutor#query():执行查询方法,查询出的数据会先放入 entriesToAddOnCommit 集合暂存
1
2
3
4
5
6
7
8// 从二缓存中获取数据,获取不到去一级缓存获取
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 回调 BaseExecutor#query
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 将数据放入 entriesToAddOnCommit 集合暂存,此时还没放入二级缓存
tcm.putObject(cache, key, list);
} -
commit():事务提交,清空一级缓存,放入二级缓存,二级缓存使用 TransactionalCacheManager(tcm)管理
1
2
3
4
5public void commit(boolean required) throws SQLException {
// 首先调用 BaseExecutor#commit 方法,【清空一级缓存】
delegate.commit(required);
tcm.commit();
} -
TransactionalCacheManager#commit:查询出的数据放入二级缓存
1
2
3
4
5
6public void commit() {
// 获取所有的缓存事务,挨着进行提交
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}1
2
3
4
5
6
7
8
9public void commit() {
if (clearOnCommit) {
delegate.clear();
}
// 将 entriesToAddOnCommit 中的数据放入二级缓存
flushPendingEntries();
// 清空相关集合
reset();
}1
2
3
4
5
6private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
// 将数据放入二级缓存
delegate.putObject(entry.getKey(), entry.getValue());
}
}
增删改操作会清空缓存:
-
update():CachingExecutor 的更新操作
1
2
3
4
5public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
// 回调 BaseExecutor#update 方法,也会清空一级缓存
return delegate.update(ms, parameterObject);
} -
flushCacheIfRequired():判断是否需要清空二级缓存
1
2
3
4
5
6
7
8private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
// 判断二级缓存是否存在,然后判断标签的 flushCache 的值,增删改操作的 flushCache 属性默认为 true
if (cache != null && ms.isFlushCacheRequired()) {
// 清空二级缓存
tcm.clear(cache);
}
}
自定义缓存
1 | <cache type="com.domain.something.MyCustomCache"/> |
type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口,且提供一个接受 String 参数作为 id 的构造器
1 | public interface Cache { |
缓存的配置,只需要在缓存实现中添加公有的 JavaBean 属性,然后通过 cache 元素传递属性值,例如在缓存实现上调用一个名为 setCacheFile(String file)
的方法:
1 | <cache type="com.domain.something.MyCustomCache"> |
- 可以使用所有简单类型作为 JavaBean 属性的类型,MyBatis 会进行转换。
- 可以使用占位符(如
${cache.file}
),以便替换成在配置文件属性中定义的值
MyBatis 支持在所有属性设置完毕之后,调用一个初始化方法, 如果想要使用这个特性,可以在自定义缓存类里实现 org.apache.ibatis.builder.InitializingObject
接口
1 | public interface InitializingObject { |
注意:对缓存的配置(如清除策略、可读或可读写等),不能应用于自定义缓存
对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新,在多个命名空间中共享相同的缓存配置和实例,可以使用 cache-ref 元素来引用另一个缓存
1 | <cache-ref namespace="com.someone.application.data.SomeMapper"/> |
构造语句
动态 SQL
基本介绍
动态 SQL 是 MyBatis 强大特性之一,逻辑复杂时,MyBatis 映射配置文件中,SQL 是动态变化的,所以引入动态 SQL 简化拼装 SQL 的操作
DynamicSQL 包含的标签:
- if
- where
- set
- choose (when、otherwise)
- trim
- foreach
各个标签都可以进行灵活嵌套和组合
OGNL:Object Graphic Navigation Language(对象图导航语言),用于对数据进行访问
参考文章:https://www.cnblogs.com/ysocean/p/7289529.html
where
作用:如果标签返回的内容是以 AND 或 OR 开头的,标签内会剔除掉
表结构:
if
基本格式:
1 | <if test=“条件判断”> |
我们根据实体类的不同取值,使用不同的 SQL 语句来进行查询。比如在 id 如果不为空时可以根据 id 查询,如果username 不同空时还要加入用户名作为条件,这种情况在我们的多条件组合查询中经常会碰到。
-
UserMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<mapper namespace="mapper.UserMapper">
<select id="selectCondition" resultType="user" parameterType="user">
SELECT * FROM user
<where>
<if test="id != null ">
id = #{id}
</if>
<if test="username != null ">
AND username = #{username}
</if>
<if test="sex != null ">
AND sex = #{sex}
</if>
</where>
</select>
</mapper> -
MyBatisConfig.xml,引入映射配置文件
1
2
3
4<mappers>
<!--mapper引入指定的映射配置 resource属性执行的映射配置文件的名称-->
<mapper resource="UserMapper.xml"/>
</mappers> -
DAO 层 Mapper 接口
1
2
3
4public interface UserMapper {
//多条件查询
public abstract List<User> selectCondition(Student stu);
} -
实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public class DynamicTest {
public void selectCondition() throws Exception{
//1.加载核心配置文件
InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml");
//2.获取SqlSession工厂对象
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(is);
//3.通过工厂对象获取SqlSession对象
SqlSession sqlSession = ssf.openSession(true);
//4.获取StudentMapper接口的实现类对象
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setId(2);
user.setUsername("李四");
//user.setSex(男); AND 后会自动剔除
//5.调用实现类的方法,接收结果
List<Student> list = mapper.selectCondition(user);
//6.处理结果
for (User user : list) {
System.out.println(user);
}
//7.释放资源
sqlSession.close();
is.close();
}
}
set
1 | <!-- 根据 id 更新 user 表的数据 --> |
- 如果第一个条件 username 为空,那么 sql 语句为:update user u set u.sex=? where id=?
- 如果第一个条件不为空,那么 sql 语句为:update user u set u.username = ? ,u.sex = ? where id=?
choose
假如不想用到所有的查询条件,只要查询条件有一个满足即可,使用 choose 标签可以解决此类问题,类似于 Java 的 switch 语句
标签:
1 | <select id="selectUserByChoose" resultType="user" parameterType="user"> |
有三个条件,id、username、sex,只能选择一个作为查询条件
-
如果 id 不为空,那么查询语句为:select * from user where id=?
-
如果 id 为空,那么看 username 是否为空
- 如果不为空,那么语句为:select * from user where username=?
- 如果 username 为空,那么查询语句为 select * from user where sex=?
trim
trim 标记是一个格式化的标记,可以完成 set 或者是 where 标记的功能,自定义字符串截取
- prefix:给拼串后的整个字符串加一个前缀,trim 标签体中是整个字符串拼串后的结果
- prefixOverrides:去掉整个字符串前面多余的字符
- suffix:给拼串后的整个字符串加一个后缀
- suffixOverrides:去掉整个字符串后面多余的字符
改写 if + where 语句:
1 | <select id="selectUserByUsernameAndSex" resultType="user" parameterType="com.ys.po.User"> |
改写 if + set 语句:
1 | <!-- 根据 id 更新 user 表的数据 --> |
foreach
基本格式:
1 | <foreach>:循环遍历标签。适用于多个参数或者的关系。 |
属性:
- collection:参数容器类型, (list-集合, array-数组)
- open:开始的 SQL 语句
- close:结束的 SQL 语句
- item:参数变量名
- separator:分隔符
需求:循环执行 sql 的拼接操作,SELECT * FROM user WHERE id IN (1,2,5)
-
UserMapper.xml片段
1
2
3
4
5
6
7
8<select id="selectByIds" resultType="user" parameterType="list">
SELECT * FROM student
<where>
<foreach collection="list" open="id IN(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select> -
测试代码片段
1
2
3
4
5
6
7
8
9
10
11//4.获取StudentMapper接口的实现类对象
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<Integer> ids = new ArrayList<>();
Collections.addAll(list, 1, 2);
//5.调用实现类的方法,接收结果
List<User> list = mapper.selectByIds(ids);
for (User user : list) {
System.out.println(user);
}
SQL片段
将一些重复性的 SQL 语句进行抽取,以达到复用的效果
格式:
1 | <sql id=“片段唯一标识”>抽取的SQL语句</sql> <!--抽取标签--> |
使用:
1 | <sql id="select">SELECT * FROM user</sql> |
逆向工程
MyBatis 逆向工程,可以针对单表自动生成 MyBatis 执行所需要的代码(mapper.java、mapper.xml、pojo…)
generatorConfig.xml
1 |
|
生成代码:
1 | public void testGenerator() throws Exception{ |
参考文章:https://www.cnblogs.com/ysocean/p/7360409.html
构建 SQL
基础语法
MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL 语句
方法 | 说明 |
---|---|
SELECT(String… columns) | 根据字段拼接查询语句 |
FROM(String… tables) | 根据表名拼接语句 |
WHERE(String… conditions) | 根据条件拼接语句 |
INSERT_INTO(String tableName) | 根据表名拼接新增语句 |
INTO_VALUES(String… values) | 根据值拼接新增语句 |
UPDATE(String table) | 根据表名拼接修改语句 |
DELETE_FROM(String table) | 根据表名拼接删除语句 |
增删改查注解:
- @SelectProvider:生成查询用的 SQL 语句
- @InsertProvider:生成新增用的 SQL 语句
- @UpdateProvider:生成修改用的 SQL 语句注解
- @DeleteProvider:生成删除用的 SQL 语句注解。
- type 属性:生成 SQL 语句功能类对象
- method 属性:指定调用方法
基本操作
-
MyBatisConfig.xml 配置
1
2
3
4<!-- mappers引入映射配置文件 -->
<mappers>
<package name="mapper"/>
</mappers> -
Mapper 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public interface StudentMapper {
//查询全部
public abstract List<Student> selectAll();
//新增数据
public abstract Integer insert(Student student);
//修改操作
public abstract Integer update(Student student);
//删除操作
public abstract Integer delete(Integer id);
} -
ReturnSQL 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42public class ReturnSql {
//定义方法,返回查询的sql语句
public String getSelectAll() {
return new SQL() {
{
SELECT("*");
FROM("student");
}
}.toString();
}
//定义方法,返回新增的sql语句
public String getInsert(Student stu) {
return new SQL() {
{
INSERT_INTO("student");
INTO_VALUES("#{id},#{name},#{age}");
}
}.toString();
}
//定义方法,返回修改的sql语句
public String getUpdate(Student stu) {
return new SQL() {
{
UPDATE("student");
SET("name=#{name}","age=#{age}");
WHERE("id=#{id}");
}
}.toString();
}
//定义方法,返回删除的sql语句
public String getDelete(Integer id) {
return new SQL() {
{
DELETE_FROM("student");
WHERE("id=#{id}");
}
}.toString();
}
} -
功能实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50public class SqlTest {
//查询全部
public void selectAll() throws Exception{
//1.加载核心配置文件
InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml");
//2.获取SqlSession工厂对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
//3.通过工厂对象获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession(true);
//4.获取StudentMapper接口的实现类对象
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
//5.调用实现类对象中的方法,接收结果
List<Student> list = mapper.selectAll();
//6.处理结果
for (Student student : list) {
System.out.println(student);
}
//7.释放资源
sqlSession.close();
is.close();
}
//新增
public void insert() throws Exception{
//1 2 3 4获取StudentMapper接口的实现类对象
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
//5.调用实现类对象中的方法,接收结果 ->6 7
Student stu = new Student(4,"赵六",26);
Integer result = mapper.insert(stu);
}
//修改
public void update() throws Exception{
//1 2 3 4 5调用实现类对象中的方法,接收结果 ->6 7
Student stu = new Student(4,"赵六wq",36);
Integer result = mapper.update(stu);
}
//删除
public void delete() throws Exception{
//1 2 3 4 5 6 7
Integer result = mapper.delete(4);
}
}
插件使用
插件原理
实现原理:插件是按照插件配置顺序创建层层包装对象,执行目标方法的之后,按照逆向顺序执行(栈)
在四大对象创建时:
- 每个创建出来的对象不是直接返回的,而是
interceptorChain.pluginAll(parameterHandler)
- 获取到所有 Interceptor(插件需要实现的接口),调用
interceptor.plugin(target)
返回 target 包装后的对象 - 插件机制可以使用插件为目标对象创建一个代理对象,代理对象可以拦截到四大对象的每一个执行
1 |
|
核心配置文件:
1 | <!--plugins:注册插件 --> |
分页插件
- 分页可以将很多条结果进行分页显示。如果当前在第一页,则没有上一页。如果当前在最后一页,则没有下一页,需要明确当前是第几页,这一页中显示多少条结果。
- MyBatis 是不带分页功能的,如果想实现分页功能,需要手动编写 LIMIT 语句,不同的数据库实现分页的 SQL 语句也是不同,手写分页 成本较高。
- PageHelper:第三方分页助手,将复杂的分页操作进行封装,从而让分页功能变得非常简单
分页操作
开发步骤:
-
导入 PageHelper 的 Maven 坐标
-
在 MyBatis 核心配置文件中配置 PageHelper 插件
注意:分页助手的插件配置在通用 Mapper 之前
1
2
3
4
5
6
7<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 指定方言 -->
<property name="dialect" value="mysql"/>
</plugin>
</plugins>
<mappers>.........</mappers> -
与 MySQL 分页查询页数计算公式不同
static <E> Page<E> startPage(int pageNum, int pageSize)
:pageNum第几页,pageSize页面大小1
2
3
4
5
6
7
8
9
public void selectAll() {
//第一页:显示2条数据
PageHelper.startPage(1,2);
List<Student> students = sqlSession.selectList("StudentMapper.selectAll");
for (Student student : students) {
System.out.println(student);
}
}
参数获取
PageInfo构造方法:
PageInfo<Student> info = new PageInfo<>(list)
: list 是 SQL 执行返回的结果集合,参考上一节
PageInfo相关API:
- startPage():设置分页参数
- PageInfo:分页相关参数功能类。
- getTotal():获取总条数
- getPages():获取总页数
- getPageNum():获取当前页
- getPageSize():获取每页显示条数
- getPrePage():获取上一页
- getNextPage():获取下一页
- isIsFirstPage():获取是否是第一页
- isIsLastPage():获取是否是最后一页