mybatis学习笔记(二)
1.myBatis相关概念
ORM全称Object/Relation Mapping:表示对象-关系映射的缩写
Mybatis是⼀个半⾃动化的持久层框架,对开发⼈员开说,核⼼sql还是需要⾃⼰进⾏优化,sql和java编
码进⾏分离,功能边界清晰,⼀个专注业务,⼀个专注数据。
hibernate:全自动的 不能对sql优化
2.环境搭建
1).引入依赖
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</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).文件头
普通mapper
<?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">
sqlMapConfig
<!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>
3).启动时遇到问题
1.无效的 XML 字符
在复制pdf中的代码时,双引号转移成错误的字符 修改为正确的即可
2.Could not find resource
复制代码 配置环境不一致
3.mybatis的CRUD操作
不同的操作使用不同的标签
public void test4() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = build.openSession();
User user = new User();
user.setId("3");
user.setName("wangwu");
user.setSex("2");
user.setAge(11);
//sqlSession.selectList("user.findAll");
//sqlSession.insert("user.saveUser",user);
//sqlSession.update("user.updateUser",user);
sqlSession.delete("user.deleteUser",3);
sqlSession.commit();
sqlSession.close();
}
操作涉及数据库数据变化,要使⽤sqlSession对象显示的提交事务,即sqlSession.commit()
4.相关配置文件
标签层级示意图
sqlMapConfig.xml
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--加载外部的properties文件-->
<properties resource="jdbc.properties"></properties>
<!--typeAliases 给实体类的全限定类名起别名-->
<typeAliases>
<!--给单独的实体-->
<!-- <typeAlias type="com.zmx.pojo.User" alias="user"></typeAlias>-->
<!--批量起别名 别名不区分大小写-->
<package name="com.zmx.pojo"/>
</typeAliases>
<!--environments 运行环境-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<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>
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>
</configuration>
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⽂件
jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///zmx_mybatis
jdbc.username=root
jdbc.password=root
<!--加载外部的properties文件-->
<properties resource="jdbc.properties"></properties>
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
4). typeAliases标签
为Java 类型设置⼀个短的名字
<!--typeAliases 给实体类的全限定类名起别名-->
<typeAliases>
<!--给单独的实体-->
<!-- <typeAlias type="com.zmx.pojo.User" alias="user"></typeAlias>-->
<!--批量起别名 别名不区分大小写-->
<package name="com.zmx.pojo"/>
</typeAliases>
5.相关api
//1.Resources工具类 配置文件加载 将配置文件加载成字节输入流
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
//2.解析配置文件 创建SqlSessionFactory工厂
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
//3.生产SqlSession
SqlSession sqlSession = build.openSession();//默认开启一个事务 但是该事务不会自动提交
// SqlSession sqlSession = build.openSession(加入参数时会自定提交); //在进行增删改操作时 手动提交事务
//4. sqlSession调用方法 查询所有-selectList 查询单个-selectOne 添加-add 修改-update 删除-delete
List<User> objects = sqlSession.selectList("user.findAll");
SqlSession⼯⼚构建器 SqlSessionFactoryBuilder
常⽤API:SqlSessionFactory build(InputStream inputStream)
通过加载mybatis的核⼼⽂件的输⼊流的形式构建⼀个SqlSessionFactory对象
Resources ⼯具类,这个类在 org.apache.ibatis.io 包中。Resources 类帮助你从类路径下、⽂
件系统或⼀个 web URL 中加载资源⽂件。
SqlSession⼯⼚对象 SqlSessionFactory
SqlSessionFactory 有多个个⽅法创建SqlSession 实例
SqlSession sqlSession = build.openSession();//默认开启一个事务 但是该事务不会自动提交 手动提交事务
SqlSession sqlSession = build.openSession(true); //在进行增删改操作时 加入参数时会自定提交
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()
6-7 dao层开发方式
1).传统开发方式
编写dao层接口以及Impl实现类
public interface UserDao {
List<User> findAll() throws IOException;
}
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;
}
}
2).代理开发方式
采⽤ Mybatis 的代理开发⽅式实现 DAO 层的开发。
Mapper 接⼝开发⽅法只需要编写Mapper 接⼝(相当于Dao 接⼝),由Mybatis 框架根据接⼝
定义创建接⼝的动态代理对象,代理对象的⽅法体同上边Dao接⼝实现类⽅法。
Mapper 接⼝开发需要遵循以下规范:
1) Mapper.xml⽂件中的namespace与mapper接⼝的全限定名相同
2) Mapper接⼝⽅法名和Mapper.xml中定义的每个statement的id相同
3) Mapper接⼝⽅法的输⼊参数类型和mapper.xml中定义的每个sql的parameterType的类型相同
4) Mapper接⼝⽅法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同
@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();
}
8-9 TypeAliases标签-Properties标签
略 (详见 4 相关配置文件)
10-11.动态标签
1). if 标签
根据实体类的不同取值,使⽤不同的 SQL语句来进⾏查询
<select id="findByCondition" resultType="user" parameterType="user">
<include refid="selectUser"></include>
<where>
<if test="id != null">
and id=#{id}
</if>
<if test="name != null">
and name=#{name}
</if>
</where>
</select>
2). foreach标签
循环执⾏sql的拼接操作
foreach标签的属性含义如下:
标签⽤于遍历集合,它的属性:
•collection:代表要遍历的集合元素,注意编写时不要写#{}
•open:代表语句的开始部分
•close:代表结束部分
•item:代表遍历集合的每个元素,⽣成的变量名
•sperator:代表分隔
<select id="findByIds" resultType="user" parameterType="list">
<include refid="selectUser"></include>
<where>
<foreach collection="array" open="id in(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select>
3). 片段抽取
Sql 中可将重复的 sql 提取出来,使⽤时⽤ include 引⽤
<!--抽取sql片段-->
<sql id="selectUser">
select * from user
</sql>
<select id="findAll" resultType="user">
<include refid="selectUser"></include>
</select>
12-14.mybatis复杂映射
1).一对一查询:
创建实体类
public class Order {
private int id;
private Date ordertime;
private double total;
//代表当前订单从属于哪⼀个客户
private User user;
}
public class User {
private int id;
private String username;
private String password;
private Date birthday;
}
mapper配置文件(两种方式同样效果)
<resultMap id="orderMap" type="com.zmx.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.zmx.pojo.User">
<result property="id" column="uid"></result>
<result property="username" column="username"></result>
</association>
</resultMap>
<!--resultMap 手动配置实体属性与表字段的映射关系-->
<select id="findOrderUser" resultMap="orderMap">
select * from orders o,user u where o.uid = u.id
</select>
<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>
其他:引进mapper文件的方式
<mappers>
<package name="com.zmx.mapper"/>
</mappers>
需要在resource文件目录下边创建与对应实体类包结构相同的文件结构
2).一对多查询
创建实体类(单个用户对应多个账单)
public class Order {
private int id;
private Date ordertime;
private double total;
//代表当前订单从属于哪⼀个客户
private User user;
}
public class User {
private int id;
private String username;
private String password;
private Date birthday;
//代表当前⽤户具备哪些订单
private List<Order> orderList;
}
mapper配置文件
<resultMap id="userMap" type="com.zmx.pojo.User">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<collection property="orderList" ofType="com.zmx.pojo.Order">
<id property="id" column="oid"></id>
<result property="orderTime" column="orderTime"></result>
<result property="total" column="total"></result>
</collection>
</resultMap>
<!--resultMap 手动配置实体属性与表字段的映射关系-->
<select id="findAll" resultMap="userMap">
select *,o.id oid from user u left join orders o on u.id=o.uid
</select>
3). 多对多查询
创建实体类
public class User {
private int id;
private String username;
private String password;
private Date birthday;
//代表当前⽤户具备哪些订单
private List<Order> orderList;
//代表当前⽤户具备哪些⻆⾊
private List<Role> roleList;
}
public class Role {
private int id;
private String rolename;
}
mapper文件
<resultMap id="userRoleMap" type="com.zmx.pojo.User">
<result property="id" column="id"></result>
<result property="username" column="username"></result>
<collection property="roleList" ofType="com.zmx.pojo.Role">
<result property="id" column="rid"></result>
<result property="roleName" column="roleName"></result>
<result property="roleDesc" column="roleDesc"></result>
</collection>
</resultMap>
<select id="findAllUserAndRole" resultMap="userRoleMap">
select u.*,r.*,r.id rid from user u
left join sys_user_role ur on u.id=ur.userid
inner join sys_role r on ur.roleid=r.id
</select>
15-18. 注解开发
常用注解:
@Insert:实现新增
@Update:实现更新
@Delete:实现删除
@Select:实现查询
@Result:实现结果集封装
@Results:可以与@Result ⼀起使⽤,封装多个结果集
@One:实现⼀对⼀结果集封装
@Many:实现⼀对多结果集封装
1). 注解CRUD开发
直接在xxxMapper.java文件里边的方法加上注解,注解写对应sql
//添加用户
@Insert("INSERT INTO USER(id,username) VALUES (#{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(Integer id);
测试过程可以在测试类中加入@before注解进行对代码的提取,实现对代码的复用
2).注解一对一
创建实体类
public class Order {
private int id;
private Date ordertime;
private double total;
//代表当前订单从属于哪⼀个客户
private User user;
}
public class User {
private int id;
private String username;
private String password;
private Date birthday;
//代表当前⽤户具备哪些订单
private List<Order> orderList;
}
直接写配置文件或直接在方法加注解实现一样的效果
mapper.xml
<resultMap id="orderMap" type="com.zmx.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.zmx.pojo.User">
<result property="id" column="uid"></result>
<result property="username" column="username"></result>
</association>
</resultMap>
//查询订单的同时查询所属用户
@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.zmx.mapper.IUserMapper.findUserById"))
})
@Select("select * from orders")
public List<Order> findOrderUser();
3). 注解一对多
实体类
public class Order {
private int id;
private Date ordertime;
private double total;
//代表当前订单从属于哪⼀个客户
private User user;
}
public class User {
private int id;
private String username;
private String password;
private Date birthday;
//代表当前⽤户具备哪些订单
private List<Order> orderList;
}
注解方式
//查询所有用户信息 同时查询每个用户关联的订单信息
@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.zmx.mapper.IOrderMapper.findOrderByUid"))
})
public List<User> findAll();
@Select("select * from orders where id = #{id}")
public List<Order> findOrderByUid(Integer id);
4). 注解多对多
实体类
public class User {
private int id;
private String username;
private String password;
private Date birthday;
//代表当前⽤户具备哪些订单
private List<Order> orderList;
//代表当前⽤户具备哪些⻆⾊
private List<Role> roleList;
}
public class Role {
private int id;
private String rolename;
}
注解方式
//查询所有用户 同时查询每个用户关联的角色信息
@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.zmx.mapper.IRoleMapper.findRoleByUid"))
})
public List<User> findAllUserAndRole();
@Select("select * from sys_role r,sys_user_role ur where r.id=ur.roleid and ur.userid = #{uid}")
public List<Role> findRoleByUid(Integer uid);
理解:多对多 存在中间表将浪着关联起来,每一条数据都能够互相对应另一张表的多条数据
(例子:一个用户可以是多个角色;一个角色也可以有多个用户)
19-24. myBatis缓存
1). 缓存概念
2). 一级缓存
证明该缓存的存在:
//第一次查询id为1的用户
User userById = iUserMapper.findUserById(1);
//更新用户
User user = new User();
user.setId(1);
user.setUsername("tom");
iUserMapper.updateUser(user);
//第二次查询
User userById1 = iUserMapper.findUserById(1);
System.out.println(userById==userById1);
当第二次查询操作之前并没有进行更新操作时,两次的查询结果一致;如果进行了更新操作,会清空SqlSession中的 ⼀ 级缓存,⽬的为了让缓存中存储的是最新的信息,避免脏读。可以手动进行缓存刷新(sqlSession.clearCache())。
一级缓存源码分析:
查看clearCache()时,流程⾛到Perpetualcache中的clear()⽅法之后,会调⽤cache.clear()⽅法 ,cache就是private Map cache = new HashMap();所以说cache.clear()其实就是map.clear() 也就是说,缓存其实就是本地存放的⼀个map对象,每⼀个SqISession都会存放⼀个map对象的引⽤
Executor是 执⾏器,⽤来执⾏SQL请求,清除缓存的⽅法也在Executor,缓存的创建有可能在Executor 找到createCacheKey方法
缓存的创建:offset limit 分页参数
CacheKey cacheKey = new CacheKey();
//MappedStatement 的 id
// id就是Sql语句的所在位置包名+类名+ SQL名称
cacheKey.update(ms.getId());
// offset 就是 0
cacheKey.update(rowBounds.getOffset());
// limit 就是 Integer.MAXVALUE
cacheKey.update(rowBounds.getLimit());
//具体的SQL语句
cacheKey.update(boundSql.getSql());
//后⾯是update 了 sql中带的参数
cacheKey.update(value);
...
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
创建缓存key会经过⼀系列的update⽅法,udate⽅法由⼀个CacheKey这个对象来执⾏的,这个update⽅法最终由updateList的list来把五个值存进去
configuration.getEnvironment().getId() 定义在mybatis-config.xml中的标签
⼀级缓存更多是⽤于查询操作
@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 {
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;
}
// 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;
}
查不到的话,就从数据库查,在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);
}
3).二级缓存
⼆级缓存的原理和⼀级缓存原理⼀样,第⼀次查询,会将数据放⼊缓存中,然后第⼆次查询则会直接去缓存中取。但是⼀级缓存是基于sqlSession的,⽽⼆级缓存是基于mapper⽂件的namespace的,也 就是说多sqlSession可以共享⼀个mapper中的⼆级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper,那么这两个mapper中执⾏sql查询到的数据也将存在相同的⼆级缓存区域中
⼆级缓存底层还是HashMap结构
需要配置进行使用
sqlMapConfig.xml⽂件中加⼊如下代码:
<!--开启⼆级缓存-->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
其次在Mapper.xml⽂件中开启缓存
<!--开启⼆级缓存-->
<cache></cache>
mapper.xml⽂件中就这么⼀个空标签,其实这⾥可以配置,PerpetualCache这个类是 mybatis默认实现缓存功能的类。不写type可以使⽤mybatis默认的缓存,也可以实现Cache接⼝来⾃定义缓存
@CacheNamespace //开启二级缓存
public interface IUserMapper {
}
开启了⼆级缓存后,需要将要缓存的pojo实现Serializable接⼝,为了将缓存数据取出执⾏反序列化操
作,因为⼆级缓存数据存储介质多种多样,不⼀定只存在内存中,有可能存在硬盘中,如果我们要再取
这个缓存的话,就需要反序列化。所以mybatis中的pojo都去实现Serializable接⼝
测试二级缓存
@Test
public void SecondLevelCache(){
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
SqlSession sqlSession3 = sqlSessionFactory.openSession(true);
IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);
IUserMapper mapper2 = sqlSession2.getMapper(IUserMapper.class);
IUserMapper mapper3 = sqlSession3.getMapper(IUserMapper.class);
User userById = mapper1.findUserById(1);
sqlSession1.close();//清空一级缓存
User user = new User();
user.setId(1);
user.setUsername("33");
mapper3.updateUser(user);
sqlSession3.commit();
User userById1 = mapper2.findUserById(1);
System.out.println(userById == userById1);
}
其他:
mybatis中还可以配置userCache和flushCache等配置项,userCache是⽤来设置是否禁⽤⼆级缓 存
的,在statement中设置useCache=false可以禁⽤当前select语句的⼆级缓存,即每次查询都会发出 sql
去查询,默认情况是true,即该sql使⽤⼆级缓存
<select id="selectUserByUserId" useCache="false"resultType="com.zmx.pojo.User" parameterType="int">
select * from user where id=#{id}
</select>
//根据id查询用户
@Options(useCache = false)
@Select("select * from user where id=#{id}")
public User findUserById(Integer id);
这种情况是针对每次查询都需要最新的数据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表示刷新缓存,这样可以避免数据库脏
读。所以不⽤设置,默认即可
4). 二级缓存整合redis
需要redis的原因:是这个缓存是单服务器⼯作,⽆法实现分布式缓存,需要找⼀个分布式的缓存,专⻔⽤来存储缓存数据的,这样不同的服务器要缓存数据都往它那⾥存,取缓存数据也从它那⾥取。
mybatis提供了⼀个eache接⼝,如果要实现⾃⼰的缓存逻辑,实现cache接⼝开发即可。mybatis本身默认实现了⼀个,但是这个缓存的实现⽆法实现分布式缓存,所以要⾃⼰实现。
redis分布式缓存就可以,mybatis提供了⼀个针对cache接⼝的redis实现类,该类存在mybatis-redis包中
使用步骤
引入依赖
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
加入注解
@CacheNamespace(implementation = RedisCache.class) //开启二级缓存
public interface IUserMapper {}
启动redis 加入redis.properties
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0
测试
源码分析
RedisCache和普遍实现Mybatis的缓存⽅案⼤同⼩异,实现Cache接⼝,并使⽤jedis操作缓存
RedisCache在mybatis启动的时候,由MyBatis的CacheBuilder创建,创建的⽅式就是调⽤RedisCache的带有String参数的构造⽅法,即RedisCache(String id);⽽在RedisCache的构造⽅法中,调⽤了 RedisConfigu rationBuilder 来创建 RedisConfig 对象,并使⽤ RedisConfig 来创建JedisPool。
RedisConfig类继承了 JedisPoolConfig,并提供了 host,port等属性的包装
public class RedisConfig extends JedisPoolConfig {
private String host = Protocol.DEFAULT_HOST;
private int port = Protocol.DEFAULT_PORT;
private int connectionTimeout = Protocol.DEFAULT_TIMEOUT;
private int soTimeout = Protocol.DEFAULT_TIMEOUT;
private String password;
private int database = Protocol.DEFAULT_DATABASE;
private String clientName;
}
RedisConfig对象是由RedisConfigurationBuilder创建的
public RedisConfig parseConfiguration(ClassLoader classLoader) {
Properties config = new Properties();
InputStream input = classLoader.getResourceAsStream(redisPropertiesFilename);
if (input != null) {
try {
config.load(input);
} catch (IOException e) {
throw new RuntimeException( "An error occurred while reading classpath property '"
+ redisPropertiesFilename + "', see nested exceptions", e);
} finally {
try {
input.close();
} catch (IOException e) {
// close quietly
}
}
}
RedisConfig jedisConfig = new RedisConfig();
setConfigProperties(config, jedisConfig);
return jedisConfig;
}
核⼼的⽅法就是parseConfiguration⽅法,该⽅法从classpath中读取⼀个redis.properties⽂件并将该配置⽂件中的内容设置到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
储存数据的格式
@Override
public void putObject(final Object key, final Object value) {
execute(new RedisCallback() {
@Override
public Object doWithRedis(Jedis jedis) {
jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
return null;
}
});
}
@Override
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()));
}
});
}
@Override
public Object doWithRedis(Jedis jedis) {
return SerializeUtil.unserialize(jedis.hget(id.toString().getBytes(),
key.toString().getBytes()));
}
});
}
mybatis-redis在存储数据的时候,是使⽤的hash结构,把cache的id作为这个hash的key (cache的id在mybatis中就是mapper的namespace);这个mapper中的查询缓存数据作为 hash的field,需要缓存的内容直接使⽤SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责对象的序列化和反序列化
25.mybatis插件
四⼤组件 (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等⽅法);
插件原理:
在四⼤对象创建的时候
1、每个创建出来的对象不是直接返回的,⽽是interceptorChain.pluginAll(parameterHandler);
2、获取到所有的Interceptor (拦截器)(插件需要实现的接⼝);调⽤ interceptor.plugin(target);返回 target 包装后的对象
3、插件机制,我们可以使⽤插件为⽬标对象创建⼀个代理对象;AOP (⾯向切⾯)我们的插件可 以为四⼤对象创建出代理对象,代理对象就可以拦截到四⼤对象的每⼀个执⾏;
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.xml
<plugins>
<plugin interceptor="com.lagou.plugin.ExamplePlugin">
</plugin>
</plugins>
这样MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。待准备⼯作做完后,MyBatis处于就绪状态。我们在执⾏SQL时,需要先通过DefaultSqlSessionFactory创建 SqlSession。Executor 实例会在创建 SqlSession 的过程中被创建, Executor实例创建完毕后,MyBatis会通过JDK动态代理为实例⽣成代理类。这样,插件逻辑即可在 Executor相关⽅法被调⽤前执⾏。
自定义mybatis插件:
@Intercepts({
@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();//原方法执行
}
//为了把当前的拦截器生成代理存到拦截器链中
@Override
public Object plugin(Object target) {
Object wrap = Plugin.wrap(target, this);
return wrap;
}
//获取配置文件参数
@Override
public void setProperties(Properties properties) {
System.out.println("获取到的配置文件参数是"+properties);
}
}
<plugins>
<plugin interceptor="com.lagou.plugin.MySqlPagingPlugin">
<!--配置参数-->
<property name="name" value="Bob"/>
</plugin>
</plugins>
plugin源码分析:
Plugin实现了 InvocationHandler接⼝,因此它的invoke⽅法会拦截所有的⽅法调⽤。invoke⽅法会 对
所拦截的⽅法进⾏检测,以决定是否执⾏插件逻辑
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
/*
*获取被拦截⽅法列表,⽐如:
* signatureMap.get(Executor.class), 可能返回 [query, update, commit]
*/
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//检测⽅法列表是否包含被拦截的⽅法
if (methods != null && methods.contains(method)) {
//执⾏插件逻辑
return interceptor.intercept(new Invocation(target, method, args));
}
//执⾏被拦截的⽅法
return method.invoke(target, args);
} catch(Exception e){
}
}
⾸先,invoke⽅法会检测被拦截⽅法是否配置在插件的@Signature注解中,若是,则执⾏插件逻辑,否则执⾏被拦截⽅法。插件逻辑封装在intercept中,该⽅法的参数类型为Invocationo Invocation主要⽤于存储⽬标类,⽅法以及⽅法参数列表
第三方插件:
1.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插件
<!--注意:分⻚助⼿的插件 配置在通⽤馆mapper之前*-->*
<plugin interceptor="com.github.pagehelper.PageHelper">
<!—指定⽅⾔ —>
<property name="dialect" value="mysql"/>
</plugin>
③ 测试分⻚数据获取
@Test
public void pageHelperTest() throws IOException {
PageHelper.startPage(1, 1);
List<User> all = iUserMapper.selectUser();
for (User user : all) {
System.out.println(user);
}
PageInfo<User> userPageInfo = new PageInfo<>(all);
System.out.println("总条数"+userPageInfo.getTotal());
System.out.println("总页数"+userPageInfo.getPages());
System.out.println("当前页"+userPageInfo.getPageNum());
System.out.println("每页显示条数"+userPageInfo.getPageSize());
}
2.通用mapper
① 导⼊通用mappe的坐标
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>3.1.2</version>
</dependency>
② 在mybatis核⼼配置⽂件中配置PageHelper插件
<plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
</plugin>
③ 实体类
@Table(name = "user")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) //设置主键的生成策略
private Integer id;
private String username;
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
'}';
}
}
public interface UserMapper extends Mapper<User> {
}
@Test
public void mapperTest() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setId(1);
User user1 = mapper.selectOne(user);
System.out.println(user1);
}
注意:
public class UserTest {
@Test
public void test1() throws IOException {
Inputstream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = build.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setId(4);
//(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);
example.createCriteria().andLike("val", "1");
//⾃定义查询
List<User> users1 = userMapper.selectByExample(example);
}
}