Mybatis
Mybatis官网 https://mybatis.org/mybatis-3/zh/configuration.html
第⼀部分:⾃定义持久层框架
https://www.jianshu.com/p/32c28d82f268
第⼆部分:Mybatis相关概念
2.1 对象/关系数据库映射(ORM)
ORM全称Object/Relation Mapping:表示对象-关系映射的缩写
ORM完成⾯向对象的编程语⾔到关系数据库的映射。当ORM框架完成映射后,程序员既可以利⽤⾯向 对象程序设
计语⾔的简单易⽤性,⼜可以利⽤关系数据库的技术优势。ORM把关系数据库包装成⾯向对 象的模型。ORM框架
是⾯向对象设计语⾔与关系数据库发展不同步时的中间解决⽅案。采⽤ORM框架 后,应⽤程序不再直接访问底层
数据库,⽽是以⾯向对象的⽅式来操作持久化对象,⽽ORM框架则将这 些⾯向对象的操作转换成底层SQL操作。
ORM框架实现的效果:把对持久化对象的保存、修改、删除 等操作,转换为对数据库的操作
2.2 Mybatis简介
MyBatis是⼀款优秀的基于ORM的半⾃动轻量级持久层框架,它⽀持定制化SQL、存储过程以及⾼级映 射。
MyBatis避免了⼏乎所有的JDBC代码和⼿动设置参数以及获取结果集。MyBatis可以使⽤简单的 XML或注解来配置
和映射原⽣类型、接⼝和Java的POJO (Plain Old Java Objects,普通⽼式Java对 象)为数据库中的记录。
2.3 Mybatis历史
原是apache的⼀个开源项⽬iBatis, 2010年6⽉这个项⽬由apache software foundation 迁移到了google code,随
着开发团队转投Google Code旗下,ibatis3.x正式更名为Mybatis ,代码于2013年11⽉迁移到Github。
iBATIS⼀词来源于“internet”和“abatis”的组合,是⼀个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL
Maps和Data Access Objects(DAO)
2.4 Mybatis优势
Mybatis是⼀个半⾃动化的持久层框架,对开发⼈员开说,核⼼sql还是需要⾃⼰进⾏优化,sql和java编码进⾏分
离,功能边界清晰,⼀个专注业务,⼀个专注数据。
分析图示如下:
第三部分:Mybatis基本应⽤
3.1 快速⼊⻔
MyBatis官⽹地址:http://www.mybatis.org/mybatis-3/
3.1.1 开发步骤:
①添加MyBatis的坐标
②创建user数据表
③编写User实体类
④编写映射⽂件UserMapper.xml
⑤编写核⼼⽂件SqlMapConfig.xml
⑥编写测试类
3.1.1 环境搭建:
1)导⼊MyBatis的坐标和其他相关坐标
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--mybatis坐标-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<!--mysql驱动坐标-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
<scope>runtime</scope>
</dependency>
<!--单元测试坐标-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--⽇志坐标-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
</dependencies>
2)创建表
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
3)编写User实体
public class User {
private int id;
private String username;
private String password;
//省略get个set⽅法
}
4)编写UserMapper映射⽂件
<?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">
<mapper namespace="userMapper">
<select id="findAll" resultType="com.lagou.domain.User">
select * from User
</select>
</mapper>
5)编写MyBatis核⼼⽂件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///test"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/lagou/mapper/UserMapper.xml"/>
</mappers>
</configuration>
6) 编写测试代码
public static void main(String[] args) throws IOException {
//加载核⼼配置⽂件
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
//获得sqlSession⼯⼚对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
//获得sqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//执⾏sql语句
List<User> userList = sqlSession.selectList("userMapper.findAll");
//打印结果
System.out.println(userList);
//释放资源
sqlSession.close();
}
3.1.2 MyBatis的增删改查操作
MyBatis的插⼊数据操作
1)编写UserMapper映射⽂件
<insert id="add" parameterType="com.lagou.domain.User">
insert into user values(#{id},#{username},#{password})
</insert>
2)编写测试代码
public static void main(String[] args) throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
User u = new User();
u.setPassword("test1");
u.setUsername("test2");
int count = sqlSession.insert("userMapper.add", u);
sqlSession.commit();
System.out.println("受影响的行数" + count);
sqlSession.close();
}
3)插⼊操作注意问题
- 插⼊语句使⽤insert标签
- 在映射⽂件中使⽤parameterType属性指定要插⼊的数据类型
- Sql语句中使⽤#{实体属性名}⽅式引⽤实体中的属性值
- 插⼊操作使⽤的API是sqlSession.insert(“命名空间.id”,实体对象);
- 插⼊操作涉及数据库数据变化,所以要使⽤sqlSession对象显示的提交事务,即sqlSession.commit()
3.1.5 MyBatis的修改数据操作
1)编写UserMapper映射⽂件
<update id="update" parameterType="com.lagou.domain.User">
update user set username=#{username},password=#{password} where id=#{id}
</update>
2)编写测试代码
public static void main(String[] args) throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
User u = new User();
u.setPassword("root");
u.setUsername("root");
u.setId(4L);
int count = sqlSession.insert("userMapper.update", u);
sqlSession.commit();
System.out.println("受影响的行数" + count);
sqlSession.close();
}
3)修改操作注意问题
- 修改语句使⽤update标签
- 修改操作使⽤的API是sqlSession.update(“命名空间.id”,实体对象);
3.1.6 MyBatis的删除数据操作
1)编写UserMapper映射⽂件
<delete id="delete" parameterType="java.lang.Integer">
delete from user where id=#{id}
</delete>
2)编写测试代码
public static void main(String[] args) throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
int count = sqlSession.insert("userMapper.delete", 1);
sqlSession.commit();
System.out.println("受影响的行数" + count);
sqlSession.close();
}
3)删除操作注意问题
- 删除语句使⽤delete标签
- Sql语句中使⽤#{任意字符串}⽅式引⽤传递的单个参数
- 删除操作使⽤的API是sqlSession.delete(“命名空间.id”,Object);
3.1.5 MyBatis的映射⽂件概述
3.1.6 ⼊⻔核⼼配置⽂件分析:
MyBatis核⼼配置⽂件层级关系
MyBatis常⽤配置解析
1)environments标签
数据库环境的配置,⽀持多环境配置
其中,事务管理器(transactionManager)类型有两种:
- JDBC:这个配置就是直接使⽤了JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作⽤域。
- MANAGED:这个配置⼏乎没做什么。它从来不提交或回滚⼀个连接,⽽是让容器来管理事务的整个⽣命周期
(⽐如 JEE 应⽤服务器的上下⽂)。 默认情况下它会关闭连接,然⽽⼀些容器并不希望这样,因此需要将
closeConnection 属性设置为 false 来阻⽌它默认的关闭⾏为。
其中,数据源(dataSource)类型有三种:
- UNPOOLED:这个数据源的实现只是每次被请求时打开和关闭连接。
- POOLED:这种数据源的实现利⽤“池”的概念将 JDBC 连接对象组织起来。
- JNDI:这个数据源的实现是为了能在如 EJB 或应⽤服务器这类容器中使⽤,容器可以集中或在外部配置数据源,
然后放置⼀个 JNDI 上下⽂的引⽤。
2)mapper标签
该标签的作⽤是加载映射的,加载⽅式有如下⼏种:
// 使⽤相对于类路径的资源引⽤,例如:
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
// 使⽤完全限定资源定位符(URL),例如:
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
// 使⽤映射器接⼝实现类的完全限定类名,例如:
<mapper class="org.mybatis.builder.AuthorMapper"/>
// 将包内的映射器接⼝实现全部注册为映射器,例如:
<package name="org.mybatis.builder"/>
3.1.7 Mybatis相应API介绍
SqlSession⼯⼚构建器SqlSessionFactoryBuilder
常⽤API:SqlSessionFactory build(InputStream inputStream)
通过加载mybatis的核⼼⽂件的输⼊流的形式构建⼀个SqlSessionFactory对象
String resource = "org/mybatis/builder/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);
其中, Resources ⼯具类,这个类在 org.apache.ibatis.io 包中。Resources 类帮助你从类路径下、⽂件系统或⼀
个 web URL 中加载资源⽂件。
SqlSession⼯⼚对象SqlSessionFactory
SqlSessionFactory 有多个个⽅法创建SqlSession 实例。常⽤的有如下两个:
SqlSession会话对象
SqlSession 实例在 MyBatis 中是⾮常强⼤的⼀个类。在这⾥你会看到所有执⾏语句、提交或回滚事务和获取映射
器实例的⽅法。
执⾏语句的⽅法主要有:
<T> T selectOne(String statement, Object parameter) <E> List<E> selectList(String statement, Object parameter)
int insert(String statement, Object parameter)
int update(String statement, Object parameter)
int delete(String statement, Object parameter)
操作事务的⽅法主要有:
void commit()
void rollback()
3.2 Mybatis的Dao层实现
3.2.1 传统开发⽅式
编写UserDao接⼝
public interface UserDao {
List<User> findAll() throws IOException;
}
编写UserDaoImpl实现
public class UserDaoImpl implements UserDao {
public List<User> findAll() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
List<User> userList = sqlSession.selectList("userMapper.findAll");
sqlSession.close();
return userList;
}
}
测试传统⽅式
@Test
public void testTraditionDao() throws IOException {
UserDao userDao = new UserDaoImpl();
List<User> all = userDao.findAll();
System.out.println(all);
}
3.2.2 代理开发⽅式
代理开发⽅式介绍
采⽤ Mybatis 的代理开发⽅式实现 DAO 层的开发,这种⽅式是我们后⾯进⼊企业的主流。
Mapper 接⼝开发⽅法只需要程序员编写Mapper 接⼝(相当于Dao 接⼝),由Mybatis 框架根据接⼝定义创建接
⼝的动态代理对象,代理对象的⽅法体同上边Dao接⼝实现类⽅法。
Mapper 接⼝开发需要遵循以下规范:
- Mapper.xml⽂件中的namespace与mapper接⼝的全限定名相同
- Mapper接⼝⽅法名和Mapper.xml中定义的每个statement的id相同
- Mapper接⼝⽅法的输⼊参数类型和mapper.xml中定义的每个sql的parameterType的类型相同
-
Mapper接⼝⽅法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同
编写UserMapper接⼝
image.png
测试代理⽅式
@Test
public void testProxyDao() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
//获得MyBatis框架⽣成的UserMapper接⼝的实现类
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.findById(1);
System.out.println(user);
sqlSession.close();
}
第四部分:Mybatis配置⽂件深⼊
4.1 核⼼配置⽂件SqlMapConfig.xml
4.1.1 MyBatis核⼼配置⽂件层级关系
4.2 MyBatis常⽤配置解析
1)environments标签
数据库环境的配置,⽀持多环境配置
其中,事务管理器(transactionManager)类型有两种:
•JDBC:这个配置就是直接使⽤了JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作⽤域。
•MANAGED:这个配置⼏乎没做什么。它从来不提交或回滚⼀个连接,⽽是让容器来管理事务的整个⽣命周期
(⽐如 JEE 应⽤服务器的上下⽂)。 默认情况下它会关闭连接,然⽽⼀些容器并不希望这样,因此需要将
closeConnection 属性设置为 false 来阻⽌它默认的关闭⾏为。
其中,数据源(dataSource)类型有三种:
•UNPOOLED:这个数据源的实现只是每次被请求时打开和关闭连接。
•POOLED:这种数据源的实现利⽤“池”的概念将 JDBC 连接对象组织起来。
•JNDI:这个数据源的实现是为了能在如 EJB 或应⽤服务器这类容器中使⽤,容器可以集中或在外部配置数据源,
然后放置⼀个 JNDI 上下⽂的引⽤。
2)mapper标签
该标签的作⽤是加载映射的,加载⽅式有如下⼏种:
// 使⽤相对于类路径的资源引⽤,例如:
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
// 使⽤完全限定资源定位符(URL),例如:
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
// 使⽤映射器接⼝实现类的完全限定类名,例如:
<mapper class="org.mybatis.builder.AuthorMapper"/>
// 将包内的映射器接⼝实现全部注册为映射器,例如:
<package name="org.mybatis.builder"/>
3)Properties标签
实际开发中,习惯将数据源的配置信息单独抽取成⼀个properties⽂件,该标签可以加载额外配置的properties⽂件
4)typeAliases标签
类型别名是为Java 类型设置⼀个短的名字。原来的类型名称配置如下
配置typeAliases,为com.lagou.domain.User定义别名为user
上⾯我们是⾃定义的别名,mybatis框架已经为我们设置好的⼀些常⽤的类型的别名
4.2 映射配置⽂件mapper.xml
动态sql语句
动态sql语句概述
Mybatis 的映射⽂件中,前⾯我们的 SQL 都是⽐较简单的,有些时候业务逻辑复杂时,我们的 SQL是动态变化
的,此时在前⾯的学习中我们的 SQL 就不能满⾜要求了。
参考的官⽅⽂档,描述如下:
我们根据实体类的不同取值,使⽤不同的 SQL语句来进⾏查询。⽐如在 id如果不为空时可以根据id查询,如果
username 不同空时还要加⼊⽤户名作为条件。这种情况在我们的多条件组合查询中经常会碰到。
<select id="findByCondition" parameterType="user" resultType="user">
select * from User
<where>
<if test="id!=0">
and id=#{id}
</if>
<if test="username!=null">
and username=#{username}
</if>
</where>
</select>
<select id="findByIds" parameterType="list" resultType="user">
select * from User
<where>
<foreach collection="list" open="id in(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select>
foreach标签的属性含义如下:
- collection:代表要遍历的集合元素,注意编写时不要写#{}
- open:代表语句的开始部分
- close:代表结束部分
- item:代表遍历集合的每个元素,⽣成的变量名
- sperator:代表分隔符
SQL⽚段抽取
Sql 中可将重复的 sql 提取出来,使⽤时⽤ include 引⽤即可,最终达到 sql 重⽤的⽬的
<!--抽取sql⽚段简化编写-->
<sql id="selectUser">
SELECT * FROM User
</sql>
<select id="findById" parameterType="int" resultType="user">
<include refid="selectUser"></include>
where id=#{id}
</select>
<select id="findByIds" parameterType="list" resultType="user">
<include refid="selectUser"></include>
<where>
<foreach collection="array" open="id in(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select>
第五部分:Mybatis复杂映射开发
5.1 ⼀对⼀查询
5.1.1 ⼀对⼀查询的模型
⽤户表和订单表的关系为,⼀个⽤户有多个订单,⼀个订单只从属于⼀个⽤户
⼀对⼀查询的需求:查询⼀个订单,与此同时查询出该订单所属的⽤户
5.1.2⼀对⼀查询的语句
对应的sql语句:select * from orders o,user u where o.uid=u.id;
查询的结果如下:
5.1.3 创建Order和User实体
public class Order {
private int id;
private Date ordertime;
private double total;
//代表当前订单从属于哪⼀个客户
private User user;
}
public class User {
private Long id;
private String username;
private String password;
private Date birthday;
}
5.1.4 创建OrderMapper接⼝
public interface OrderMapper {
List<Order> findAll();
}
5.1.5 配置OrderMapper.xml
<mapper namespace="com.lagou.mapper.OrderMapper">
<resultMap id="orderMap" type="com.lagou.domain.Order">
<result column="uid" property="user.id"></result>
<result column="username" property="user.username"></result>
<result column="password" property="user.password"></result>
<result column="birthday" property="user.birthday"></result>
</resultMap>
<select id="findAll" resultMap="orderMap">
select * from orders o,user u where o.uid=u.id
</select>
</mapper>
其中还可以配置如下
<resultMap id="orderMap" type="com.lagou.domain.Order">
<result property="id" column="id"></result>
<result property="ordertime" column="ordertime"></result>
<result property="total" column="total"></result>
<association property="user" javaType="com.lagou.domain.User">
<result column="uid" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
</association>
</resultMap>
5.2 ⼀对多查询
5.2.1 ⼀对多查询的模型
⽤户表和订单表的关系为,⼀个⽤户有多个订单,⼀个订单只从属于⼀个⽤户
⼀对多查询的需求:查询⼀个⽤户,与此同时查询出该⽤户具有的订单
5.2.2 ⼀对多查询的语句
对应的sql语句:select *,o.id oid from user u left join orders o on u.id=o.uid;
查询的结果如下:
5.2.3 修改实体
public class Order {
private int id;
private Date ordertime;
private double total;
}
public class User {
private Long id;
private String username;
private String password;
private Date birthday;
private List<Order> orderList;
}
5.2.4 创建UserMapper接⼝
public interface UserMapper {
List<User> findAll();
}
5.2.5 配置UserMapper.xml
<resultMap id="userMap" type="com.lagou.domain.User">
<result column="id" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<collection property="orderList" ofType="com.lagou.domain.Order">
<result column="oid" property="id"></result>
<result column="ordertime" property="ordertime"></result>
<result column="total" property="total"></result>
</collection>
</resultMap>
<select id="findAll" resultMap="userMap">
select *,o.id oid from user u left join orders o on u.id=o.uid
</select>
5.3 多对多查询
5.3.1 多对多查询的模型
⽤户表和⻆⾊表的关系为,⼀个⽤户有多个⻆⾊,⼀个⻆⾊被多个⽤户使⽤
多对多查询的需求:查询⽤户同时查询出该⽤户的所有⻆⾊
5.3.2 多对多查询的语句
对应的sql语句:select u.,r.,r.id rid from user u left join user_role ur on u.id=ur.user_id
inner join role r on ur.role_id=r.id;
查询的结果如下:
5.3.3 修改实体
public class Role {
private int id;
private String rolename;
}
public class User {
private Long id;
private String username;
private String password;
private Date birthday;
private List<Role> roleList;
}
5.2.4 创建UserMapper接⼝
public interface UserMapper {
List<User> findAllUserAndRole();
}
5.3.5 配置UserMapper.xml
<resultMap id="userRoleMap" type="com.lagou.domain.User">
<result column="id" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<collection property="roleList" ofType="com.lagou.domain.Role">
<result column="rid" property="id"></result>
<result column="rolename" property="rolename"></result>
</collection>
</resultMap>
<select id="findAllUserAndRole" resultMap="userRoleMap">
SELECT u.*, r.*, r.id rid
FROM user u
LEFT JOIN user_role ur ON u.id = ur.user_id
INNER JOIN role r ON ur.role_id = r.id
</select>
第六部分:Mybatis注解开发
这⼏年来注解开发越来越流⾏,Mybatis也可以使⽤注解开发⽅式,这样我们就可以减少编写Mapper
映射⽂件了。我们先围绕⼀些基本的CRUD来学习,再学习复杂映射多表操作。
@Insert:实现新增
@Update:实现更新
@Delete:实现删除
@Select:实现查询
@Result:实现结果集封装
@Results:可以与@Result ⼀起使⽤,封装多个结果集
@One:实现⼀对⼀结果集封装
@Many:实现⼀对多结果集封装
6.2 MyBatis的增删改查
我们完成简单的user表的增删改查的操作
修改MyBatis的核⼼配置⽂件,我们使⽤了注解替代的映射⽂件,所以我们只需要加载使⽤了注解的Mapper接⼝
即可
<mappers>
<!--扫描使⽤注解的类-->
<mapper class="com.lagou.mapper.UserMapper"></mapper>
</mappers>
或者指定扫描包含映射关系的接⼝所在的包也可以
<mappers>
<!--扫描使⽤注解的类所在的包-->
<package name="com.lagou.mapper"></package>
</mappers>
修改mapper接口类
public interface UserMapper {
@Insert("INSERT INTO user VALUES (#{id}, #{username}, #{password})")
void add(User user);
@Update("UPDATE user SET username=#{username}, password=#{password} WHERE id = #{id}")
void update(User user);
@Delete("DELETE FROM user WHERE id = #{id}")
void delete(User user);
@Select(" SELEC *FROM User")
List<User> findAll();
}
测试
public static void main(String[] args) throws IOException {
//加载核⼼配置⽂件
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
//获得sqlSession⼯⼚对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
//获得sqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//执⾏sql语句
User user = new User();
user.setUsername("mumashi");
user.setPassword("123456");
sqlSession.getMapper(UserMapper.class).add(user);
sqlSession.commit();
List<User> userList = sqlSession.selectList("userMapper.findAll");
//打印结果
System.out.println(userList);
//释放资源
sqlSession.close();
}
6.3 MyBatis的注解实现复杂映射开发
实现复杂关系映射之前我们可以在映射⽂件中通过配置来实现,使⽤注解开发后,我们可以使⽤@Results注解,
@Result注解,@One注解,@Many注解组合完成复杂关系的配置
6.4 ⼀对⼀查询
6.4.1 ⼀对⼀查询的模型
⽤户表和订单表的关系为,⼀个⽤户有多个订单,⼀个订单只从属于⼀个⽤户
⼀对⼀查询的需求:查询⼀个订单,与此同时查询出该订单所属的⽤户
6.4.2 ⼀对⼀查询使⽤注解配置Mapper
public interface OrderMapper {
@Select("select * from orders")
@Results({
@Result(id = true, property = "id", column = "id"),
@Result(property = "ordertime", column = "ordertime"),
@Result(property = "total", column = "total"),
@Result(property = "user", column = "uid",
javaType = User.class,
one = @One(select = "com.lagou.mapper.UserMapper.findById"))})
List<Order> findAll();
}
public interface UserMapper {
@Select("select * from user where id=#{id}")
User findById(int id);
}
6.5 ⼀对多查询
6.5.1 ⼀对多查询的模型
⽤户表和订单表的关系为,⼀个⽤户有多个订单,⼀个订单只从属于⼀个⽤户
⼀对多查询的需求:查询⼀个⽤户,与此同时查询出该⽤户具有的订单
6.5.2 ⼀对多查询使⽤注解配置Mapper
@Select("select * from user")
@Results({
@Result(id = true,property = "id",column = "id"),
@Result(property = "username",column = "username"),
@Result(property = "password",column = "password"),
@Result(property = "birthday",column = "birthday"),
@Result(property = "orderList",column = "id",
javaType = List.class,
many = @Many(select = "com.lagou.mapper.OrderMapper.findByUid"))
})
List<User> findAllUserAndOrder();
@Select("select * from orders where uid=#{uid}")
List<Order> findByUid(int uid);
6.6 多对多查询
6.6.1 多对多查询的模型
⽤户表和⻆⾊表的关系为,⼀个⽤户有多个⻆⾊,⼀个⻆⾊被多个⽤户使⽤
多对多查询的需求:查询⽤户同时查询出该⽤户的所有⻆⾊
6.6.2 多对多查询使⽤注解配置Mapper
public interface UserMapper {
@Select("select * from user")
@Results({
@Result(id = true,property = "id",column = "id"),
@Result(property = "username",column = "username"),
@Result(property = "password",column = "password"),
@Result(property = "birthday",column = "birthday"),
@Result(property = "roleList",column = "id",
javaType = List.class,
many = @Many(select = "com.lagou.mapper.RoleMapper.findByUid"))
})
List<User> findAllUserAndRole();
}
public interface RoleMapper {
@Select("select * from role r,user_role ur where r.id=ur.role_id and ur.user_id=# {uid}")
List<Role>findByUid(int uid);
}
第七部分:Mybatis缓存
7.1 ⼀级缓存
①、在⼀个sqlSession中,对User表根据id进⾏两次查询,查看他们发出sql语句的情况
@Test
public void test1(){
//根据 sqlSessionFactory 产⽣ session
SqlSession sqlSession = sessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//第⼀次查询,发出sql语句,并将查询出来的结果放进缓存中
User u1 = userMapper.selectUserByUserId(1);
System.out.println(u1);
//第⼆次查询,由于是同⼀个sqlSession,会在缓存中查询结果
//如果有,则直接从缓存中取出来,不和数据库进⾏交互
User u2 = userMapper.selectUserByUserId(1);
System.out.println(u2);
sqlSession.close();
}
查看控制台打印情况:
② 、同样是对user表进⾏两次查询,只不过两次查询之间进⾏了⼀次update操作。
@Test
public void test2(){
//根据 sqlSessionFactory 产⽣ session
SqlSession sqlSession = sessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中
User u1 = userMapper.selectUserByUserId( 1 );
System.out.println(u1);
//第⼆步进⾏了⼀次更新操作,sqlSession.commit()
u1.setSex("⼥");
userMapper.updateUserByUserId(u1);
sqlSession.commit();
//第⼆次查询,由于是同⼀个sqlSession.commit(),会清空缓存信息
//则此次查询也会发出sql语句
User u2 = userMapper.selectUserByUserId(1);
System.out.println(u2);
sqlSession.close();
}
查看控制台打印情况:
③、总结
1、第⼀次发起查询⽤户id为1的⽤户信息,先去找缓存中是否有id为1的⽤户信息,如果没有,从 数据库查询⽤户
信息。得到⽤户信息,将⽤户信息存储到⼀级缓存中。
2、 如果中间sqlSession去执⾏commit操作(执⾏插⼊、更新、删除),则会清空SqlSession中的 ⼀级缓存,这
样做的⽬的为了让缓存中存储的是最新的信息,避免脏读。
3、 第⼆次发起查询⽤户id为1的⽤户信息,先去找缓存中是否有id为1的⽤户信息,缓存中有,直 接从缓存中获取
⽤户信息
⼀级缓存原理探究与源码分析
⼀级缓存到底是什么?⼀级缓存什么时候被创建、⼀级缓存的⼯作流程是怎样的?相信你现在应该会有 这⼏个疑
问,那么我们本节就来研究⼀下⼀级缓存的本质
⼤家可以这样想,上⾯我们⼀直提到⼀级缓存,那么提到⼀级缓存就绕不开SqlSession,所以索性我们 就直接从
SqlSession,看看有没有创建缓存或者与缓存有关的属性或者⽅法
调研了⼀圈,发现上述所有⽅法中,好像只有clearCache()和缓存沾点关系,那么就直接从这个⽅ 法⼊⼿吧,分析
源码时,我们要看它(此类)是谁,它的⽗类和⼦类分别⼜是谁,对如上关系了解了,你才 会对这个类有更深的认
识,分析了⼀圈,你可能会得到如下这个流程图
再深⼊分析,流程⾛到Perpetualcache中的clear()⽅法之后,会调⽤其cache.clear()⽅法,那 么这个cache是什
么东⻄呢?点进去发现,cache其实就是private Map cache = new HashMap();也就是⼀个Map,所以说cache.clear()其实就是map.clear(),也就是说,缓存其实就是 本地存放⼀个map对象,每⼀个SqISession都会存放⼀个map对象的引⽤,那么这个cache是何 时创建的呢?
你觉得最有可能创建缓存的地⽅是哪⾥呢?我觉得是Executor,为什么这么认为?因为Executor是 执⾏器,⽤来
执⾏SQL请求,⽽且清除缓存的⽅法也在Executor中执⾏,所以很可能缓存的创建也很 有可能在Executor中,看了
⼀圈发现Executor中有⼀个createCacheKey⽅法,这个⽅法很像是创 建缓存的⽅法啊,跟进去看看,你发现
createCacheKey⽅法是由BaseExecutor执⾏的,代码如下
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
创建缓存key会经过⼀系列的update⽅法,udate⽅法由⼀个CacheKey这个对象来执⾏的,这个 update⽅法最终
由updateList的list来把五个值存进去,对照上⾯的代码和下⾯的图示,你应该能 理解这五个值都是什么了
这⾥需要注意⼀下最后⼀个值,configuration.getEnvironment().getId()这是什么,这其实就是 定义在mybatis�config.xml中的标签,⻅如下。
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://172.28.34.186:3306/test?useUnicode=true&characterEncoding=utf8"></property>
<property name="username" value="root"></property>
<property name="password" value="pywm@40orgsystem"></property>
</dataSource>
</environment>
</environments>
那么我们回归正题,那么创建完缓存之后该⽤在何处呢?总不会凭空创建⼀个缓存不使⽤吧?绝对不会 的,经过我
们对⼀级缓存的探究之后,我们发现⼀级缓存更多是⽤于查询操作,毕竟⼀级缓存也叫做查询缓存吧,为什么叫查
询缓存我们⼀会⼉说。我们先来看⼀下这个缓存到底⽤在哪了,我们跟踪到 query⽅法如下:
@Override
public <E> List<E> 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<E> list;
try {
queryStack++;
// 从一级缓存中获取数据
list = resultHandler == null ? (List<E>) 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();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进⾏写⼊。 localcache对象的put⽅
法最终交给Map进⾏存放
private Map<Object, Object> cache = new HashMap<Object, Object>();
@Override
public void putObject(Object key, Object value) { cache.put(key, value);
}
7.2 ⼆级缓存
⼆级缓存的原理和⼀级缓存原理⼀样,第⼀次查询,会将数据放⼊缓存中,然后第⼆次查询则会直接去 缓存中取。
但是⼀级缓存是基于sqlSession的,⽽⼆级缓存是基于mapper⽂件的namespace的,也 就是说多个sqlSession可
以共享⼀个mapper中的⼆级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper,那么这两
个mapper中执⾏sql查询到的数据也将存在相同的⼆级缓存区域中
如何使⽤⼆级缓存
① 、开启⼆级缓存
和⼀级缓存默认开启不⼀样,⼆级缓存需要我们⼿动开启
⾸先在全局配置⽂件sqlMapConfig.xml⽂件中加⼊如下代码:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
其次在UserMapper.xml⽂件中开启缓存
<!--开启⼆级缓存-->
<cache></cache>
我们可以看到mapper.xml⽂件中就这么⼀个空标签,其实这⾥可以配置,PerpetualCache这个类是 mybatis默认实
现缓存功能的类。我们不写type就使⽤mybatis默认的缓存,也可以去实现Cache接⼝ 来⾃定义缓存。
我们可以看到⼆级缓存底层还是HashMap结构
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap();
public PerpetualCache(St ring id) { this.id = id; }
}
开启了⼆级缓存后,还需要将要缓存的pojo实现Serializable接⼝,为了将缓存数据取出执⾏反序列化操 作,因为
⼆级缓存数据存储介质多种多样,不⼀定只存在内存中,有可能存在硬盘中,如果我们要再取 这个缓存的话,就需
要反序列化了。所以mybatis中的pojo都去实现Serializable接⼝
public class User implements Serializable(
//⽤户ID
private int id;
//⽤户姓名
private String username;
//⽤户性别
private String sex;
}
③、测试
⼀、测试⼆级缓存和sqlSession⽆关
@Test
public void testTwoCache() {
//根据 sqlSessionFactory 产⽣ session
SqlSession sqlSession1 = sessionFactory.openSession();
SqlSession sqlSession2 = sessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
//第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中
User u1 = userMapper1.selectUserByUserId(1);
System.out.println(u1);
sqlSession1.close(); //第⼀次查询完后关闭 sqlSession
//第⼆次查询,即使sqlSession1已经关闭了,这次查询依然不发出sql语句
User u2 = userMapper2.selectUserByUserId(1);
System.out.println(u2);
sqlSession2.close();
}
可以看出上⾯两个不同的sqlSession,第⼀个关闭了,第⼆次查询依然不发出sql查询语句
⼆、测试执⾏commit()操作,⼆级缓存数据清空
@Test
public void testTwoCache(){
//根据 sqlSessionFactory 产⽣ session
SqlSession sqlSession1 = sessionFactory.openSession();
SqlSession sqlSession2 = sessionFactory.openSession();
SqlSession sqlSession3 = sessionFactory.openSession();
String statement = "com.lagou.pojo.UserMapper.selectUserByUserld" ;
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper. class );
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper. class );
UserMapper userMapper3 = sqlSession2.getMapper(UserMapper. class );
//第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中
User u1 = userMapperl.selectUserByUserId( 1 );
System.out.println(u1);
sqlSessionl .close(); //第⼀次查询完后关闭sqlSession
//执⾏更新操作,commit()
u1.setUsername( "aaa" );
userMapper3.updateUserByUserId(u1);
sqlSession3.commit();
//第⼆次查询,由于上次更新操作,缓存数据已经清空(防⽌数据脏读),这⾥必须再次发出sql语
User u2 = userMapper2.selectUserByUserId( 1 );
System.out.println(u2);
sqlSession2.close();
}
查看控制台情况:
④、useCache和flushCache
mybatis中还可以配置userCache和flushCache等配置项,userCache是⽤来设置是否禁⽤⼆级缓 存的,在 statement中设置useCache=false可以禁⽤当前select语句的⼆级缓存,即每次查询都会发出 sql去查询,默认情况是true,即该sql使⽤⼆级缓存
<select id="selectUserByUserId" useCache="false" resultType="com.lagou.pojo.User"
parameterType="int">
select * from user where id=#{id}
</select>
这种情况是针对每次查询都需要最新的数据sql,要设置成useCache=false,禁⽤⼆级缓存,直接从数 据库中获取。
在mapper的同⼀个namespace中,如果有其它insert、update, delete操作数据后需要刷新缓存,如果不执⾏刷
新缓存会出现脏读。
设置statement配置中的flushCache="true”属性,默认情况下为true,即刷新缓存,如果改成false则 不会刷新。使
⽤缓存时如果⼿动修改数据库表中的查询数据会出现脏读。
<select id="selectUserByUserId" flushCache="true" useCache="false"
resultType="com.lagou.pojo.User" parameterType="int">
select * from user where id=#{id}
</select>
⼀般下执⾏完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。所以我们
不⽤设置,默认即可。
源码分析
CachingExecutor查询
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 从MappedStatement获取缓存,MappedStatement中的cache是由MapperBuilderAssistant产生的,在同一个mapper文件中cache共享
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从TransactionalCacheManager中获取缓存,实际上是把cache作为可以key从transactionalCaches中拿到一个TransactionalCache对象,然后在TransactionalCache中从真正的二级缓存中拿数据
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 将缓存放入待提交缓存中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
TransactionalCacheManager
public class TransactionalCacheManager {
// 不同namespace的cache不同,所以这里的cache可以看做是命名空间。之所有用cache做缓存而不是用namespace是为了后面对cache进行操作
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
}
TransactionalCache
// 真正的二级缓存
private final Cache delegate;
private boolean clearOnCommit;
// 待提交的缓存数据
private final Map<Object, Object> entriesToAddOnCommit;
// 二级缓存中不存在的缓存key
private final Set<Object> entriesMissedInCache;
.......
@Override
public Object getObject(Object key) {
// issue #116
// 查询的时候是去真实的二级缓存中查询
Object object = delegate.getObject(key);
if (object == null) {
// 如果不存在就把key加入到不存在的key名单中。这里可以做一下优化,在getObject时判断一下是否在entriesMissedInCache中
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
// 事务提交时,将未提交的缓存提交到二级缓存中
flushPendingEntries();
reset();
}
总结:
- 二级缓存和事务有关系,当事务提交时才提交未提交缓存到二级缓存中
- 多个事务提交二级缓存,可能会出现脏数据的问题,没有很好地控制
- 这个东西应该是不能用的,代码有待优化
7.3 ⼆级缓存整合redis
上⾯我们介绍了 mybatis⾃带的⼆级缓存,但是这个缓存是单服务器⼯作,⽆法实现分布式缓存。 那么什么是分布
式缓存呢?假设现在有两个服务器1和2,⽤户访问的时候访问了 1服务器,查询后的缓 存就会放在1服务器上,假设
现在有个⽤户访问的是2服务器,那么他在2服务器上就⽆法获取刚刚那个 缓存,如下图所示:
为了解决这个问题,就得找⼀个分布式的缓存,专⻔⽤来存储缓存数据的,这样不同的服务器要缓存数 据都往它那
⾥存,取缓存数据也从它那⾥取,如下图所示:
如上图所示,在⼏个不同的服务器之间,我们使⽤第三⽅缓存框架,将缓存都放在这个第三⽅框架中, 然后⽆论有
多少台服务器,我们都能从缓存中获取数据。
这⾥我们介绍mybatis与redis的整合。
刚刚提到过,mybatis提供了⼀个eache接⼝,如果要实现⾃⼰的缓存逻辑,实现cache接⼝开发即可。
mybati s本身默认实现了⼀个,但是这个缓存的实现⽆法实现分布式缓存,所以我们要⾃⼰来实现。
redis分布式缓存就可以,mybatis提供了⼀个针对cache接⼝的redis实现类,该类存在mybatis-redis包 中
实现:
- pom⽂件
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
2.配置⽂件
Mapper.xml
<cache type="org.mybatis.caches.redis.RedisCache" />
3.redis.properties
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0
4.测试
@Test
public void SecondLevelCache(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);
lUserMapper mapper2 = sqlSession2.getMapper(lUserMapper.class);
lUserMapper mapper3 = sqlSession3.getMapper(IUserMapper.class);
User user1 = mapper1.findUserById(1);
sqlSession1.close(); //清空⼀级缓存
User user = new User();
user.setId(1);
user.setUsername("lisi");
mapper3.updateUser(user);
sqlSession3.commit();
User user2 = mapper2.findUserById(1);
System.out.println(user1==user2);
}
源码分析:
RedisCache和⼤家普遍实现Mybatis的缓存⽅案⼤同⼩异,⽆⾮是实现Cache接⼝,并使⽤jedis操作缓存;不过该
项⽬在设计细节上有⼀些区别;
public RedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
} else {
this.id = id;
RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(), redisConfig.getDatabase(), redisConfig.getClientName());
}
}
private Object execute(RedisCallback callback) {
Jedis jedis = pool.getResource();
Object var3;
try {
var3 = callback.doWithRedis(jedis);
} finally {
jedis.close();
}
return var3;
}
public void putObject(final Object key, final Object value) {
this.execute(new RedisCallback() {
public Object doWithRedis(Jedis jedis) {
jedis.hset(RedisCache.this.id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
return null;
}
});
}
public Object getObject(final Object key) {
return this.execute(new RedisCallback() {
public Object doWithRedis(Jedis jedis) {
return SerializeUtil.unserialize(jedis.hget(RedisCache.this.id.toString().getBytes(), key.toString().getBytes()));
}
});
}
RedisCache在mybatis启动的时候,由MyBatis的CacheBuilder创建,创建的⽅式很简单,就是调⽤ RedisCache
的带有String参数的构造⽅法,即RedisCache(String id);⽽在RedisCache的构造⽅法中, 调⽤了 RedisConfigu
rationBuilder 来创建 RedisConfig 对象,并使⽤ RedisConfig 来创建JedisPool。
RedisConfig类继承了 JedisPoolConfig,并提供了 host,port等属性的包装,简单看⼀下RedisConfig的 属性:
public class RedisConfig extends JedisPoolConfig {
private String host = "localhost";
private int port = 6379;
private int connectionTimeout = 2000;
private int soTimeout = 2000;
private String password;
private int database = 0;
private String clientName;
}
RedisConfig对象是由RedisConfigurationBuilder创建的,简单看下这个类的主要⽅法:
private static final String SYSTEM_PROPERTY_REDIS_PROPERTIES_FILENAME = "redis.properties.filename";
private static final String REDIS_RESOURCE = "redis.properties";
private final String redisPropertiesFilename = System.getProperty("redis.properties.filename", "redis.properties");
public RedisConfig parseConfiguration(ClassLoader classLoader) {
Properties config = new Properties();
InputStream input = classLoader.getResourceAsStream(this.redisPropertiesFilename);
if (input != null) {
try {
config.load(input);
} catch (IOException var12) {
throw new RuntimeException("An error occurred while reading classpath property '" + this.redisPropertiesFilename + "', see nested exceptions", var12);
} finally {
try {
input.close();
} catch (IOException var11) {
}
}
}
RedisConfig jedisConfig = new RedisConfig();
this.setConfigProperties(config, jedisConfig);
return jedisConfig;
}
核⼼的⽅法就是parseConfiguration⽅法,该⽅法从classpath中读取⼀个redis.properties⽂件
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0
并将该配置⽂件中的内容设置到RedisConfig对象中,并返回;接下来,就是RedisCache使⽤ RedisConfig类创建
完成edisPool;在RedisCache中实现了⼀个简单的模板⽅法,⽤来操作Redis:
private Object execute(RedisCallback callback) {
Jedis jedis = pool.getResource();
try {
return callback.doWithRedis(jedis);
} finally {
jedis.close();
}
}
模板接⼝为RedisCallback,这个接⼝中就只需要实现了⼀个doWithRedis⽅法⽽已:
public interface RedisCallback {
Object doWithRedis(Jedis jedis);
}
接下来看看Cache中最重要的两个⽅法:putObject和getObject,通过这两个⽅法来查看mybatis-redis 储存数据
的格式:
public void putObject(final Object key, final Object value) {
this.execute(new RedisCallback() {
public Object doWithRedis(Jedis jedis) {
jedis.hset(RedisCache.this.id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
return null;
}
});
}
可以很清楚的看到,mybatis-redis在存储数据的时候,是使⽤的hash结构,把cache的id作为这个hash 的key
(cache的id在mybatis中就是mapper的namespace);这个mapper中的查询缓存数据作为 hash的field,需要缓存
的内容直接使⽤SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责 对象的序列化和反序列化;
第⼋部分:Mybatis插件
8.1 插件简介
⼀般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者⾃⾏拓展。这样的好处是显⽽易⻅ 的,⼀是增
加了框架的灵活性。⼆是开发者可以结合实际需求,对框架进⾏拓展,使其能够更好的⼯ 作。以MyBatis为例,我
们可基于MyBati s插件机制实现分⻚、分表,监控等功能。由于插件和业务 ⽆关,业务也⽆法感知插件的存在。因
此可以⽆感植⼊插件,在⽆形中增强功能
8.2 Mybatis插件介绍
Mybati s作为⼀个应⽤⼴泛的优秀的ORM开源框架,这个框架具有强⼤的灵活性,在四⼤组件
(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易⽤的插 件扩展机制。
Mybatis对持久层的操作就是借助于四⼤核⼼对象。MyBatis⽀持⽤插件对四⼤核⼼对象进 ⾏拦截,对mybatis来说
插件就是拦截器,⽤来增强核⼼对象的功能,增强功能本质上是借助于底层的 动态代理实现的,换句话说,
MyBatis中的四⼤对象都是代理对象
MyBatis所允许拦截的⽅法如下:
- 执⾏器Executor (update、query、commit、rollback等⽅法);
- SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等⽅ 法);
- 参数处理器ParameterHandler (getParameterObject、setParameters⽅法);
- 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等⽅法);
8.3 Mybatis插件原理
在四⼤对象创建的时候
1、每个创建出来的对象不是直接返回的,⽽是interceptorChain.pluginAll(parameterHandler);
2、获取到所有的Interceptor (拦截器)(插件需要实现的接⼝);调⽤ interceptor.plugin(target);返回 target 包
装后的对象
3、插件机制,我们可以使⽤插件为⽬标对象创建⼀个代理对象;AOP (⾯向切⾯)我们的插件可 以为四⼤对象
创建出代理对象,代理对象就可以拦截到四⼤对象的每⼀个执⾏;
Executor注册拦截器链
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
InterceptorChain拦截器链
Plugin为拦截器设置代理
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 从集合中查询当前类被Interceptor标记拦截的方法集合
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
// 如果该方法被被标记了拦截
if (methods != null && methods.contains(method)) {
// interceptor实现类对方法进行代理处理
return interceptor.intercept(new Invocation(target, method, args));
}
// 直接调用原方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调⽤拦截器链 中的拦截器
依次的对⽬标进⾏拦截或增强。interceptor.plugin(target)中的target就可以理解为mybatis 中的四⼤对象。返回
的target是被重重代理后的对象
如果我们想要拦截Executor的query⽅法,那么可以这样定义插件:
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
)
})
public class ExeunplePlugin implements Interceptor {
//省略逻辑
}
除此之外,我们还需将插件配置到sqlMapConfig.xm l中。
<plugins>
<plugin interceptor="com.lagou.plugin.ExamplePlugin">
这里可以配置插件的属性
</plugin>
</plugins>
这样MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。待准备⼯作
做完后,MyBatis处于就绪状态。我们在执⾏SQL时,需要先通过DefaultSqlSessionFactory 创建 SqlSession。
Executor 实例会在创建 SqlSession 的过程中被创建, Executor实例创建完毕后,MyBatis会通过JDK动态代理为
实例⽣成代理类。这样,插件逻辑即可在 Executor相关⽅法被调⽤前执⾏。
以上就是MyBatis插件机制的基本原理
8.6 pageHelper分⻚插件
MyBati s可以使⽤第三⽅的插件来对功能进⾏扩展,分⻚助⼿PageHelper是将分⻚的复杂操作进⾏封装,使⽤简
单的⽅式即可获得分⻚的相关数据
开发步骤:
① 导⼊通⽤PageHelper的坐标
② 在mybatis核⼼配置⽂件中配置PageHelper插件
③ 测试分⻚数据获取
①导⼊通⽤PageHelper坐标
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>3.7.5</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>0.9.1</version>
</dependency>
② 在mybatis核⼼配置⽂件中配置PageHelper插件
<plugins>
<plugin interceptor="com.github.pagehelper.PageHelper">
<!-- 指定⽅⾔ -->
<property name="dialect" value="mysql"/>
</plugin>
</plugins>
③ 测试分⻚代码实现
@Test
public void testPageHelper() {
//设置分⻚参数
PageHelper.startPage(1, 2);
List<User> select = userMapper2.select(null);
for (User user : select) {
System.out.println(user);
}
}
获得分⻚相关的其他参数
//其他分⻚的数据
PageInfo<User> pageInfo = new PageInfo<User>(select);
System.out.println("总条数:"+pageInfo.getTotal());
System.out.println("总⻚数:"+pageInfo. getPages ());
System.out.println("当前⻚:"+pageInfo. getPageNum());
System.out.println("每⻚显万⻓度:"+pageInfo.getPageSize());
System.out.println("是否第⼀⻚:"+pageInfo.isIsFirstPage());
System.out.println("是否最后⼀⻚:"+pageInfo.isIsLastPage());
实现原理
PageHelper拦截MappedStatement的query方法
@Intercepts({@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)})
public class PageHelper implements Interceptor {
....
public Object intercept(Invocation invocation) throws Throwable {
return this.sqlUtil.processPage(invocation);
}
....
}
sqlUtil 执行查询list和count
private Object _processPage(Invocation invocation) throws Throwable {
final Object[] args = invocation.getArgs();
RowBounds rowBounds = (RowBounds) args[2];
if (SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT) {
return invocation.proceed();
} else {
//忽略RowBounds-否则会进行Mybatis自带的内存分页
args[2] = RowBounds.DEFAULT;
//分页信息
Page page = getPage(rowBounds);
//pageSizeZero的判断
if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
//执行正常(不分页)查询
Object result = invocation.proceed();
//得到处理结果
page.addAll((List) result);
//相当于查询第一页
page.setPageNum(1);
//这种情况相当于pageSize=total
page.setPageSize(page.size());
//仍然要设置total
page.setTotal(page.size());
//返回结果仍然为Page类型 - 便于后面对接收类型的统一处理
return page;
}
//获取原始的ms
MappedStatement ms = (MappedStatement) args[0];
SqlSource sqlSource = ms.getSqlSource();
//简单的通过total的值来判断是否进行count查询
if (page.isCount()) {
//将参数中的MappedStatement替换为新的qs
msUtils.processCountMappedStatement(ms, sqlSource, args);
//查询总数
Object result = invocation.proceed();
//设置总数
page.setTotal((Integer) ((List) result).get(0));
if (page.getTotal() == 0) {
return page;
}
}
//pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
if (page.getPageSize() > 0 &&
((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
|| rowBounds != RowBounds.DEFAULT)) {
//将参数中的MappedStatement替换为新的qs
msUtils.processPageMappedStatement(ms, sqlSource, page, args);
//执行分页查询
Object result = invocation.proceed();
//得到处理结果
page.addAll((List) result);
}
//返回结果
return page;
}
}
8.7 通⽤ mapper
什么是通⽤Mapper
通⽤Mapper就是为了解决单表增删改查,基于Mybatis的插件机制。开发⼈员不需要编写SQL,不需要 在DAO中增
加⽅法,只要写好实体类,就能⽀持相应的增删改查⽅法
如何使⽤
- ⾸先在maven项⽬,在pom.xml中引⼊mapper的依赖
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>3.1.2</version>
</dependency>
- Mybatis配置⽂件中完成配置
<plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
<!-- 通⽤Mapper接⼝,多个通⽤接⼝⽤逗号隔开 -->
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
</plugin>
- 实体类设置主键
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
}
- 定义通⽤mapper
import com.lagou.domain.User;
import tk.mybatis.mapper.common.Mapper;
public interface UserMapper extends Mapper<User> {
定义自己的通用mapper方法
}
第九部分:Mybatis架构原理
9.1架构设计
我们把Mybatis的功能架构分为三层:
(1) API接⼝层:提供给外部使⽤的接⼝ API,开发⼈员通过这些本地API来操纵数据库。接⼝层⼀接收到 调⽤请求
就会调⽤数据处理层来完成具体的数据处理。
MyBatis和数据库的交互有两种⽅式:
a. 使⽤传统的MyBatis提供的API ;
b. 使⽤Mapper代理的⽅式
(2) 数据处理层:负责具体的SQL查找、SQL解析、SQL执⾏和执⾏结果映射处理等。它主要的⽬的是根 据调⽤的
请求完成⼀次数据库操作。
(3) 基础⽀撑层:负责最基础的功能⽀撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共 ⽤的东
⻄,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的⽀撑
9.2主要构件及其相互关系
构件 | 描述 |
---|---|
SqlSession | 作为MyBatis⼯作的主要顶层API,表示和数据库交互的会话,完成必要数 据库增删改查功能 |
Executor | MyBatis执⾏器,是MyBatis调度的核⼼,负责SQL语句的⽣成和查询缓 存的维护 |
StatementHandler | 封装了JDBC Statement操作,负责对JDBC statement的操作,如设置参 数、将Statement 结果集转换成List集合。 |
ParameterHandler | 负责对⽤户传递的参数转换成JDBC Statement所需要的参数, |
ResultSetHandler | 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合; |
TypeHandler | 负责java数据类型和jdbc数据类型之间的映射和转换 |
MappedStatement | MappedStatement维护了⼀条<select、update 、 delete 、 insert>节点的封装 |
SqlSource | 负责根据⽤户传递的parameterObject,动态地⽣成SQL语句,将信息封 装到BoundSql 对象中,并返回 |
BoundSql | 表示动态⽣成的SQL语句以及相应的参数信息 |
9.3总体流程
(1) 加载配置并初始化
触发条件:加载配置⽂件
配置来源于两个地⽅,⼀个是配置⽂件(主配置⽂件conf.xml,mapper⽂件*.xml),—个是java代码中的 注解,将主
配置⽂件内容解析封装到Configuration,将sql的配置信息加载成为⼀个mappedstatement 对象,存储在内存之中
(2) 接收调⽤请求
触发条件:调⽤Mybatis提供的API
传⼊参数:为SQL的ID和传⼊参数对象
处理过程:将请求传递给下层的请求处理层进⾏处理。
(3) 处理操作请求
触发条件:API接⼝层传递请求过来
传⼊参数:为SQL的ID和传⼊参数对象
处理过程:
(A) 根据SQL的ID查找对应的MappedStatement对象。
(B) 根据传⼊参数对象解析MappedStatement对象,得到最终要执⾏的SQL和执⾏传⼊参数。
(C) 获取数据库连接,根据得到的最终SQL语句和执⾏传⼊参数到数据库执⾏,并得到执⾏结果。
(D) 根据MappedStatement对象中的结果映射配置对得到的执⾏结果进⾏转换处理,并得到最终的处理 结果。
(E) 释放连接资源。
(4) 返回处理结果
将最终的处理结果返回。