1、基本概念
MyBatis是⼀款优秀的基于ORM的半⾃动轻量级持久层框架,它⽀持定制化SQL、存储过程以及⾼级映射。MyBatis避免了⼏乎所有的JDBC代码和⼿动设置参数以及获取结果集。MyBatis可以使⽤简单的XML或注解来配置和映射原⽣类型、接⼝和Java的POJO (Plain Old Java Objects,普通⽼式Java对 象)为数据库中的记录。
官网地址 : 官网地址
2、常用配置介绍使用
为了方面后续学习使用,先搭建一个maven工程,引入mybatis 依赖,另外因为要连接数据库,还需要引入jdbcdriver,为方便测试引入junit, 具体pom 配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>mybatis_quickstart</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
</dependencies>
</project>
搭建项目结构如下,其中jdbc.properties 存储数据库连接配置信息(可以配置在sqlmapconfig中,但因为数据库信息每个环境都不一样,常见方式是单独放在与环境相关的文件中,为此单独放在jdbc.properties),sqlmapConfig.xml 是mybatis 核心配置文件,usermapper.xml是 mybatis sql映射配置文件存储信息
文件内容如下:
sqlmapConfig.xml中
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--1、加载外部配置文件 -->
<properties resource="jdbc.properties"></properties>
<!--2、给实体类的全限定名起别名 -->
<typeAliases>
<!-- 给单独实体起别名 -->
<!--
<typeAlias type="com.example.pojo.User" alias="user"></typeAlias>
-->
<!-- 多个起别名 该包下所有类本身的类名 (不区分大小写)-->
<package name="com.example.pojo"/>
</typeAliases>
<!--3、 指定当前默认的环境名称 -->
<environments default="development">
<!-- 3.1 可以配置不同的环境信息 -->
<environment id="development">
<!--3.1.1 当前事务交由jdbc 进行管理 -->
<transactionManager type="JDBC"></transactionManager>
<!--3.1.2 当前使用mybatis 提供的连接池 -->
<dataSource type="POOLED">
<!--<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/de?useUnicode=true&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>-->
<!-- 数据源配置信息 -->
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!-- 方式1 -->
<!--<mappers>
<mapper resource="IOrderMapper.xml"></mapper>
</mappers>-->
<!-- 方式2 -->
<!-- package 方式 引入需要配置文件与mapper 文件同包同名 -->
<!--<mappers>
<package name="com.example.mapper"/>
</mappers>-->
<!-- 方式3 -->
<mappers>
<mapper class="com.example.mapper.IUserMapper"></mapper>
</mappers>
</configuration>
jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/de?useUnicode=true&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=root
usermapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.dao.IUserDao">
<!--抽取sql 片段 -->
<sql id="selectuser">
select * from user
</sql>
<!-- namespace 命名空间,与id组成唯一标识 -->
<select id="findAll" resultType="uSeR">
<include refid="selectuser"></include>
</select>
<insert id="insertUser" parameterType="com.example.pojo.User">
insert into user(id,username) values (#{id},#{username})
</insert>
<update id="updateUser" parameterType="com.example.pojo.User">
update user set username=#{username} where id = #{id}
</update>
<delete id="deleteUser" parameterType="com.example.pojo.User">
delete from user where id = #{id}
</delete>
<select id="findUserByCondition" parameterType="user" resultType="user">
select * from user where 1=1
<if test="id !=null">
and id = #{id}
</if>
<if test="username !=null">
and username = #{username}
</if>
</select>
<!-- mybatis where 关键字会自动添加where关键字,并去掉第一个if条件里面的and -->
<select id="findUserByCondition2" parameterType="user" resultType="user">
select * from user
<where>
<if test="id !=null">
and id = #{id}
</if>
<if test="username !=null">
and username = #{username}
</if>
</where>
</select>
<select id="findUserByIds" parameterType="list" resultType="user">
select * from user
<where>
<foreach collection="list" open=" id in (" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select>
</mapper>
User 类
package com.example.pojo;
public class User {
private Integer id;
private String username;
//省略 get set 方法
}
至此,项目核心配置搭建完成
编写测试类
import java.util.List;
public class Mybayis_test {
@Test
public void test() throws Exception{
//RESOURCE 工具类,配置文件加载,把配置文件加载到字节输入流
InputStream is= Resources.getResourceAsStream("SqlMapConfig.xml");
// 2 \解析配置文件,并创建sql session工厂
SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(is);
//3、 生产sqlsession
SqlSession sqlSession = sqlSessionFactory.openSession(); // 默认开启事务,但是 该事务不会自动提交,所以增删改 需要提交事务
//设置为 true 自动提交事务
//SqlSession sqlSession = sqlSessionFactory.openSession(true);
//4 sqlsession 调用方法,查询所有 selectlist 查询一个 selectone 更新 update 删除 delete 添加 insert
List<User> uSers=sqlSession.selectList("User.findAll");
for (User user : uSers) {
System.out.println(user);
}
sqlSession.close();
}
}
如上面代码所示,通过mybatis实现单表的增删改查可以通过不同的标签,再通过mapper文件的namespace 以及sql id,指定到具体的一个sql ,mybatis帮我们实现参数转换和结果集封装。
有几个注意事项:
1、mappee.xml 中的paramtertype 指的是参数类型,retulttype 是返回值类型,两个都要写类的全限定名(别名例外,typealias )
2、sql 语句中,使用#{实体属性名}引用实体中的属性值
3、增删改涉及数据库变化,要使用sqlsession 提交事务,即sqlsession.commit()
4、通过sqlsession 提供的相关api完成对数据库的操作
5、映射mapper文件要有通用的文件头
按照经典三层模型数据结构,在我们企业实际开发中,通常会将和数据的交互放在dao层实现
这样,我们可以调整代码如下:
新建dao 层接口
public interface IUserDao {
public List<User> findAll() throws Exception;
}
新建实现类,实现dao层接口
public class UserDaoImpl implements IUserDao {
//传统开发方式
@Override
public List<User> findAll() throws Exception{
//RESOURCE 工具类,配置文件加载,把配置文件加载到字节输入流
InputStream is= Resources.getResourceAsStream("SqlMapConfig.xml");
// 2 \解析配置文件,并创建sql session工厂
SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(is);
//3、 生产sqlsession
SqlSession sqlSession = sqlSessionFactory.openSession(); // 默认开启事务,但是 该事务不会自动提交,所以增删改 需要提交事务
//设置为 true 自动提交事务
//SqlSession sqlSession = sqlSessionFactory.openSession(true);
//4 sqlsession 调用方法,查询所有 selectlist 查询一个 selectone 更新 update 删除 delete 添加 insert
List<User> uSers=sqlSession.selectList("User.findAll");
sqlSession.close();
return uSers;
}
}
测试
@Test
public void testdao() throws Exception{
IUserDao userDao=new UserDaoImpl();
List<User> all = userDao.findAll();
for (User user : all) {
System.out.println(user);
}
}
以上代码可以完成和数据库的正常交互,但是当有成百上千个交互要实现的时候,就会比较痛苦,每个sql 都要有自己的实现类,直观上看我们实现类的代码交互都类似,初始化相关解析配置信息,获取sqlsession, 获取对应的statementid,找到对应的脚本并执行,有事务的时候提交事务 为此,mybatis 提供了第二种方式:代理方式
采⽤ 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的类型相同
我们可以先注释上面的实现类
@Test
@Test
public void testdao2() throws Exception{
InputStream is= Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();
IUserDao mapper = sqlSession.getMapper(IUserDao.class);
List<User> all = mapper.findAll();
for (User user : all) {
System.out.println(user);
}
}
关于mybatis 动态sql 如if ,where ,foreach,sql 等标签的使用,可以参考上面的demo。
3、复杂映射
上面提到,当我们对单表操作时,通过sql 属性,resulttype指定对应的返回结果类型,这样mybatis 会自动帮我们做封装,那么问题来了,如果是对多个表操作,那么我们如果封装结果集,这里引入resultMap
这里我们以用户和订单的关系为例:
通常在我们电商系统中,一个用户有多笔订单,每个订单对应一个用户,如果这样,那么
新建订单类
package com.example.pojo;
public class Order {
private Integer id;
private String orderTime;
private Double total;
//表明该订单属于哪个用户
private User user;
//省略 get set方法
}
订单表维护如下
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ordertime` varchar(255) DEFAULT NULL,
`total` double DEFAULT NULL,
`uid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uid` (`uid`),
CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
3.1 一对一查询结果映射
如果我们查询订单对应的用户:
<?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="com.example.mapper.IOrderMapper">
<resultMap id="userOrderMap" type="com.example.pojo.Order">
<result property="id" column="id"></result>
<result property="orderTime" column="orderTime"></result>
<result property="total" column="total"></result>
<association property="user" javaType="com.example.pojo.User" >
<result property="id" column="uid"></result>
<result property="username" column="username"></result>
</association>
</resultMap>
<select id="findOrderAndUSer" resultMap="userOrderMap">
select * from user u,orders o where o.uid=u.id;
</select>
</mapper>
public interface IOrderMapper {
//查询订单的同时,查询所属的用户 一对一
public List<Order> findOrderAndUSer();
}
3.2 一对多查询结果映射
一个人对应多个订单,为此:用户表新增订单列表集合
public class User implements Serializable {
private Integer id;
private String username;
private List<Order> orderList;
private List<Role> roleList;
// 省略 set get 方法
}
核心代码如下
<resultMap id="userOrderMap" type="com.example.pojo.User">
<result property="id" column="id"></result>
<result property="username" column="username"></result>
<collection property="orderList" ofType="com.example.pojo.Order">
<result property="id" column="oid"></result>
<result property="orderTime" column="orderTime"></result>
<result property="total" column="total"></result>
</collection>
</resultMap>
<select id="findUserAndOrders" resultMap="userOrderMap">
select u.*,o.id oid,o.ordertime,o.total,o.uid from user u left join orders o on u.id = o.uid
</select>
3.3 多对多查询结果映射
在一个系统中有用户和角色两个模块,一个用户可以担任多个角色,一个角色可以有多个用户担任,为此,用户和角色是一对一的关系。如果要查询一个用户有哪些角色,为此
用户表新增角色list
package com.example.pojo;
import com.sun.org.apache.xpath.internal.operations.Or;
import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
public class User implements Serializable {
private Integer id;
private String username;
private List<Order> orderList;
private List<Role> roleList;
//省略get set方法
}
<resultMap id="userRoleMap" type="com.example.pojo.User">
<result property="id" column="userid"></result>
<result property="username" column="username"></result>
<collection property="roleList" ofType="com.example.pojo.Role">
<result property="id" column="roleid"></result>
<result property="rolename" column="rolename"></result>
<result property="roleDesc" column="roleDesc"></result>
</collection>
</resultMap>
<select id="findUserAndRole" resultMap="userRoleMap">
select * from user left join sys_user_role sur on user.id = sur.userid left join sys_role sr on sur.roleid = sr.id
</select>
以上可以发现,resultmap维护的就是sql 字段和 实体类的映射关系,这里要注意,因为常规开发中,我们经常列名是下划线命名,字段名是驼峰命名,为此,resultMap 在我们实际开发中实际上是一个很常用的标签。
4、注解开发
近年来随着注解开发越来越流行,mybatis也可以使用注解开发,不过这个只是个趋势,笔者自己的公司 实际上还是用的mapper 配置文件比较多,当时这种方式还是值得 学习了解。
4.1 使用注解完成简单增删改查
因为注解就不需要配置文件了,为此,调整mybatis sqlmap.xml mapper引入方式,调整后就能扫描到包下所有mapper
<!--<mappers>
<mapper resource="IOrderMapper.xml"></mapper>
</mappers>-->
<!-- package 方式 引入需要配置文件与mapper 文件同包同名 -->
<mappers>
<package name="com.example.mapper"/>
</mappers>
Iusermapper 新增注解方法
@CacheNamespace (implementation = RedisCache.class)//开启二级缓存
public interface IUserMapper {
//添加用户
@Insert("insert into user value(#{id},#{username})")
public void addUser(User user);
//更新用户
@Update("update user set username=#{username} where id=#{id}")
public void updateUser(User user);
@Select("select * from user")
public List<User> selectUser();
@Delete("delete from user where id=#{id}")
public void deleteUser(User user);
@Select("select * from user where id =#{id}")
public User findUserById(Integer id);
}
测试类测试
@Before
public void befo() throws Exception{
InputStream is= Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
userMapper = sqlSession.getMapper(IUserMapper.class);
orderMapper=sqlSession.getMapper(IOrderMapper.class);
}
@Test
public void addUSer(){
User user=new User();
user.setId(6);
user.setUsername("testadd");
userMapper.addUser(user);
}
@Test
public void updateUSer(){
User user=new User();
user.setId(6);
user.setUsername("testudpate");
userMapper.updateUser(user);
}
@Test
public void deleteUSer(){
User user=new User();
user.setId(6);
user.setUsername("testudpate");
userMapper.deleteUser(user);
}
@Test
public void selectUSer(){
User user=new User();
user.setId(1);
user.setUsername("testudpate");
userMapper.selectUser();
}
4.2 使用注解实现复杂映射开发
实现复杂关系映射之前我们可以在映射⽂件中通过配置来实现,使⽤注解开发后,我们可以使⽤
@Results注解,@Result注解,@One注解,@Many注解组合完成复杂关系的配置
- 一对一映射开发
usermapper 新增注解查询方法
package com.example.mapper;
import com.example.pojo.Order;
import com.example.pojo.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
public interface IOrderMapper {
//查询订单的同时,查询所属的用户 一对一
public List<Order> findOrderAndUSer();
@Results({
@Result(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.example.mapper.IUserMapper.findUserById"))
})
@Select("select * from orders")
public List<Order> findOrderAndUSerbyAnnotation();
@Options(useCache = true) //使用二级缓存
@Select("select * from orders where uid = #{id}")
public List<Order> findOrderByUId(Integer id);
}
- 一对多,多对多
public interface IUserMapper {
//查询所有用户信息,同时查询所有用户所属订单信息
@Select("select * from user")
@Results({
@Result(property = "id",column = "id"),
@Result(property = "username",column = "username"),
@Result(property = "orderList",column = "id",javaType = List.class,many = @Many(
select = "com.example.mapper.IOrderMapper.findOrderByUId"
))
})
public List<User> findUserAndOrdersByAnnotation();
//查询所有用户,同时查询所有用户关联角色信息
@Select("select * from user")
@Results({
@Result(property = "id",column = "id"),
@Result(property = "username",column = "username"),
@Result(property = "roleList",column = "id",javaType = List.class,many = @Many(
select = "com.example.mapper.IRoleMapper.findRoleByid"
))
})
public List<User> findUserAndRoleByAnnotation();
}
public interface IRoleMapper {
@Select("select sys_role.* from sys_role,sys_user_role where sys_role.id=sys_user_role.roleid and sys_user_role.userid=#{id}")
public List<Role> findRoleByid(Integer id);
}
5、缓存原理
5.1 缓存概念
mybatis 提供了缓存机制,以便部分不会频繁更新的热点数据能更高效的查询,减少与数据库的交互次数,针对不同的数据有不同的缓存级别
5.2 一级缓存
一级缓存是 会话级别的,一级缓存又叫查询缓存,因为对同一个sqlSession,查询完执行 commit操作,会将缓存中的数据清空。一级缓存底层是一个map, value 是对象地址,一级缓存默认开启
Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds
rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
//创建缓存
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds
rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
...
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);
}
...
// queryFromDatabase ⽅法
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; }
}
private Map<Object, Object> cache = new HashMap<Object, Object>();
@Override
public void putObject(Object key, Object value) { cache.put(key, value);
}
5.3 二级缓存
二级缓存是namespace 级别的,多个sqlSession可以共享⼀个mapper中的⼆级缓存区域
开启二级缓存,需要在sqlmapConfig.xml中加入如下配置:
<!--开启⼆级缓存-->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
对应mapper文件中加入下面配置
<!--开启⼆级缓存-->
<cache></cache>
针对每次查询都需要最新的数据sql,要设置成useCache=false,禁⽤⼆级缓存,直接从数据库中获取。在mapper的同⼀个namespace中,如果有其它insert、update, delete操作数据后需要刷新缓 存,如果不执⾏刷新缓存会出现脏读。设置statement配置中的flushCache="true”属性,默认情况下为true,即刷新缓存,如果改成false则不会刷新。使⽤缓存时如果⼿动修改数据库表中的查询数据会出现脏读。
mybatis除了默认实现,也对redis有个实现,pom 添加依赖
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
resource 新增配置文件 redis.properties
host=localhost
port=6379
connectionTimeout=5000
password=
database=0
缓存注解调整,指定使用redis实现
@CacheNamespace (implementation = RedisCache.class)//开启二级缓存
mybatis-redis在存储数据的时候,是使⽤的hash结构,把cache的id作为这个hash
的key (cache的id在mybatis中就是mapper的namespace);这个mapper中的查询缓存数据作为 hash
的field,需要缓存的内容直接使⽤SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责 对象
的序列化和反序列化;
底层结构 | 默认开启 | 级别 | 存储 | 额外处理 | |
---|---|---|---|---|---|
一级缓存 | hashmap | 是 | 会话级别 | 对象地址(可以用==比较) | |
二级缓存 | hashmap | 否 | namespace | 对象拷贝 | 存储对象实现序列号接口,sqlmapconfig.xml mapper文件要加额外配置,缓存才能生效,可以通过实现Cache 接口自定义缓存处理类(mybatis 有默认实现,也针对redis有个包) |
6、插件机制
我们可基于MyBati s插件机制实现分⻚、分表,监控等功能。由于插件和业务⽆关,业务也⽆法感知插件的存在。因此可以⽆感植⼊插件,在⽆形中增强功能
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等⽅法);
6.1 插件原理
在四⼤对象创建的时候
1、每个创建出来的对象不是直接返回的,⽽是interceptorChain.pluginAll(parameterHandler);
2、获取到所有的Interceptor (拦截器)(插件需要实现的接⼝);调⽤ interceptor.plugin(target);返 回 target 包装后的对象
3、插件机制,我们可以使⽤插件为⽬标对象创建⼀个代理对象;AOP (⾯向切⾯)我们的插件可 以
为四⼤对象创建出代理对象,代理对象就可以拦截到四⼤对象的每⼀个执⾏;
public ParameterHandler newParameterHandler(MappedStatement mappedStatement,
Object object, BoundSql sql, InterceptorChain interceptorChain) {
ParameterHandler parameterHandler =
mappedStatement.getLang().createParameterHandler(mappedStatement, object, sql);
parameterHandler = (ParameterHandler)
interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调⽤拦截器链中的拦截器依次的对⽬标进⾏拦截或增强。interceptor.plugin(target)中的target就可以理解为mybatis中的四⼤对象。返回的target是被重重代理后的对象,如果我们想要拦截StatementHandler的prepare⽅法,那么可以这样定义插件:
package com.example.plugin;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;
@Intercepts({//注意看这个⼤花括号,也就这说这⾥可以定义多个@Signature对多个地⽅拦截,都⽤
这个拦截器
@Signature(type = StatementHandler.class,//这是指拦截哪个接⼝
method = "prepare",//接⼝内的哪个⽅法名
args = {Connection.class,Integer.class})//这是拦截的⽅法的⼊参,按
顺序写到这,不要多也不要少,如果⽅法重载,可是要通过⽅法名和⼊参来确定唯⼀的
})
public class MyPlugin implements Interceptor {
/**
* 只要被拦截的目标对象的目标方法 被执行时,每次都会执行intercept 方法
**/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("对方法进行了增强");
return invocation.proceed(); //原方法执行
}
/**
* 主要为了 把当前的拦截器 生成代理 存到拦截器链中
* 包装⽬标对象 为⽬标对象创建代理对象
* @Param target为要拦截的对象
* @Return代理对象
**/
@Override
public Object plugin(Object o) {
Object wrap = Plugin.wrap(o, this);
return wrap;
}
/**
获取配置文件参数
插件初始化的时候调⽤,也只调⽤⼀次,插件配置的属性从这⾥设置进来
**/
@Override
public void setProperties(Properties properties) {
System.out.println("获取到的配置文件到的参数"+properties);
}
}
在sqlmapConfig.xml 中配置插件
<plugins>
<plugin interceptor="com.example.plugin.MyPlugin">
<property name="name" value="tom"/>
</plugin>
</plugins>
这样MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。
待准备⼯作做完后,MyBatis处于就绪状态。我们在执⾏SQL时,需要先通过DefaultSqlSessionFactory
创建 SqlSession。StatementHandler实例会在创建 SqlSession 的过程中被创建, StatementHandler实例创建完毕后,MyBatis会通过JDK动态代理为实例⽣成代理类。这样,插件逻辑即可在StatementHandler相关⽅法被调⽤前执⾏。
6.2 自定义插件
Mybatis 插件接⼝-Interceptor
- Intercept⽅法,插件的核⼼⽅法
- plugin⽅法,⽣成target的代理对象
- setProperties⽅法,传递插件所需参数
示例代码如上
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
private final Map<Class<?>, Set<Method>> signatureMap;
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
} catch (Exception var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
从mybatis 源码可以看到,Plugin实现了invocationHandler接口,通过warp 接口返回代理对象,在我们调用目标对象的invoke 方法时,会判断方法有没有被代理,如果被代理,会调用对应拦截器的intercept方法,否则调用原方法
6.3 常用插件
6.3.1 pagehelper插件
MyBati s可以使⽤第三⽅的插件来对功能进⾏扩展,分⻚助⼿PageHelper是将分⻚的复杂操作进⾏封
装,使⽤简单的⽅式即可获得分⻚的相关数据
开发步骤:
- 导⼊通⽤PageHelper的坐标
- 在mybatis核⼼配置⽂件中配置PageHelper插件
- 测试分⻚数据获取
1、 导入坐标
<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>
2、配置pagehelper插件
<!--注意:分⻚助⼿的插件 配置在通⽤馆mapper之前*-->*
<plugin interceptor="com.github.pagehelper.PageHelper">
<!—指定⽅⾔ —>
<property name="dialect" value="mysql"/>
</plugin>
@Test
public void pageHelperTest(){
PageHelper.startPage(1,1);
List<User> users = userMapper.selectUser();
for (User user : users) {
System.out.println(user);
}
PageInfo<User> pageInfo=new PageInfo<>(users);
System.out.println("总条数"+pageInfo.getTotal());
System.out.println("总页数"+pageInfo.getPages());
System.out.println("当前页"+pageInfo.getPageNum());
System.out.println("每页显示条数"+pageInfo.getPageSize());
}
6.3.2 通用mapper 插件
通⽤Mapper就是为了解决单表增删改查,基于Mybatis的插件机制。开发⼈员不需要编写SQL,不需要
在DAO中增加⽅法,只要写好实体类,就能⽀持相应的增删改查⽅法
1、导入坐标
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>3.1.2</version>
</dependency>
2、配置插件
<plugins>
<!--分⻚插件:如果有分⻚插件,要排在通⽤mapper之前-->
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql"/>
</plugin>
<plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
<!-- 通⽤Mapper接⼝,多个通⽤接⼝⽤逗号隔开 -->
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
</plugin>
</plugins>
3、实体类设置主键
@Table(name = "user")
public class User implements Serializable {
//public long serialVersionUID =1;
@Id //对应主键 id
@GeneratedValue(strategy = GenerationType.IDENTITY) //设置主键生成策略
private Integer id;
@Column // 列名和实体类名字 不一样时候,使用
private String username;
}
这里演示个别框架封装的方法,其他方法类似
@Test
public void mappperTest() throws Exception{
InputStream is= Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user=new User();
user.setId(1);
//(1)mapper基础接⼝
//select 接⼝
User user1 = userMapper.selectOne(user); //根据实体中的属性进⾏查询,只能有—个返回值
List<User> users = userMapper.select(null); //查询全部结果
userMapper.selectByPrimaryKey(1); //根据主键字段进⾏查询,⽅法参数必须包含完整的主键属性,查询条件使⽤等号
userMapper.selectCount(user); //根据实体中的属性查询总数,查询条件使⽤等号
// insert 接⼝
int insert = userMapper.insert(user); //保存⼀个实体,null值也会保存,不会使⽤数据库默认值
int i = userMapper.insertSelective(user); //保存实体,null的属性不会保存,会使⽤数据库默认值
// update 接⼝
int i1 = userMapper.updateByPrimaryKey(user);//根据主键更新实体全部字段,null值会被更新
// delete 接⼝
int delete = userMapper.delete(user); //根据实体属性作为条件进⾏删除,查询条件 使⽤等号
userMapper.deleteByPrimaryKey(1); //根据主键字段进⾏删除,⽅法参数必须包含完整的主键属性
//(2)example⽅法
Example example=new Example(User.class);
example.createCriteria().andEqualTo("id",1);
List<User> users = userMapper.selectByExample(example);
for (User user1 : users) {
System.out.println(user1);
}
}