ORM全称Object/Relation Mapping:表示对象-关系映射的缩写
ORM完成面向对象的编程语言到关系数据库的映射。当ORM框架完成映射后,程序员既可以利用面向对象程序设计语言的简单易用性,又可以利用关系数据库的技术优势。ORM把关系数据库包装成面向对象的模型。ORM框架是面向对象设计语言与关系数据库发展不同步时的中间解决方案。采用ORM框架后,应用程序不再直接访问底层数据库,而是以面向对象的放松来操作持久化对象,而ORM框架则将这些面向对象的操作转换成底层SQL操作。ORM框架实现的效果:把对持久化对象的保存、修改、删除等操作,转换为对数据库的操作。
MyBatis是一款优秀的基于ORM的半自动轻量级持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。MyBatis可以使用简单的XML或注解来配置和映射原生类型、接口和Java的POJO (Plain Old Java Objects,普通老式Java对 象)为数据库中的记录。
原是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)
Mybatis是一个半自动化的持久层框架,对开发人员开说,核心sql还是需要自己进行优化,sql和java编码进行分离,功能边界清晰,一个专注业务,一个专注数据。
MyBatis官网地址:http://www.mybatis.org/mybatis-3/
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8maven.compiler.encoding>
<java.version>1.8java.version>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
properties>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.4.5version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.6version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.12version>
dependency>
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL,
`password` varchar(50) DEFAULT NULL
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
public class User {
private int id;
private String username;
private String password;
//省略get个set方法
}
<mapper namespace="userMapper">
<select id="findAll" resultType="com.cyd.domain.User">
select * from User
select>
mapper>
<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>
//加载核心配置文件
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();
(1)编写UserMapper映射文件
<mapper namespace="userMapper">
<insert id="add" parameterType="com.cyd.domain.User">
insert into user values(#{id},#{username},#{password})
insert>
mapper>
(2)编写插入实体User的代码
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
int insert = sqlSession.insert("userMapper.add", user);
System.out.println(insert);
//提交事务
sqlSession.commit();
sqlSession.close();
(3)插入操作注意问题
(1)编写UserMapper映射文件
<mapper namespace="userMapper">
<update id="update" parameterType="com.cyd.domain.User">
update user set username=#{username},password=#{password} where id=#{id}
update>
mapper>
(2)编写修改实体User的代码
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
int update = sqlSession.update("userMapper.update", user);
System.out.println(update);
sqlSession.commit();
sqlSession.close();
(3)修改操作注意问题
(1)编写UserMapper映射文件
<mapper namespace="userMapper">
<delete id="delete" parameterType="java.lang.Integer">
delete from user where id=#{id}
delete>
mapper>
(2)编写删除数据的代码
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
int delete = sqlSession.delete("userMapper.delete",3);
System.out.println(delete);
sqlSession.commit();
sqlSession.close();
(3)删除操作注意问题
(1)编写UserDao接口
public interface UserDao {
List<User> findAll() throws IOException;
}
(2)编写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;
}
}
(3)测试传统方式
@Test
public void testTraditionDao() throws IOException {
UserDao userDao = new UserDaoImpl();
List<User> all = userDao.findAll();
System.out.println(all);
}
代理开发方式介绍
Mapper 接口开发需要遵循以下规范:
测试代理方式
@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 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 配置文档的顶层结构如下:
其中,事务管理器(transactionManager)类型有两种:
其中,数据源(dataSource)类型有三种:
实际开发中,习惯将数据源的配置信息单独抽取成一个properties文件,该标签可以加载额外配置的properties文件。
类型别名是为Java 类型设置一个短的名字。原来的类型名称配置如下
配置typeAliases,为com.lagou.domain.User定义别名为user
上面我们是自定义的别名,mybatis框架已经为我们设置好的一些常用的类型的别名
动态sql语句概述
Mybatis 的映射文件中,前面我们的 SQL 都是比较简单的,有些时候业务逻辑复杂时,我们的 SQL是动态变化的,此时在前面的学习中我们的 SQL 就不能满足要求了。
(1)动态SQL之 if 标签
我们根据实体类的不同取值,使用不同的 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>
当查询条件id和username都存在时,控制台打印的sql语句如下:
//获得MyBatis框架生成的UserMapper接口的实现类
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User condition = new User();
condition.setId(1);
condition.setUsername("lucy");
User user = userMapper.findByCondition(condition);
当查询条件只有id存在时,控制台打印的sql语句如下:
//获得MyBatis框架生成的UserMapper接口的实现类
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User condition = new User();
condition.setId(1);
User user = userMapper.findByCondition(condition);
(2)动态SQL之 foreach 标签
循环执行sql的拼接操作,例如:SELECT * FROM User WHERE id IN (1,2,5)。
<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>
测试代码片段如下:
//获得MyBatis框架生成的UserMapper接口的实现类
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
int[] ids = new int[]{2,5};
List<User> userList = userMapper.findByIds(ids);
System.out.println(userList);
foreach标签的属性含义如下,标签用于遍历集合,它的属性:
(3)SQL片段抽取
Sql 中可将重复的 sql 提取出来,使用时用 include 引用即可,最终达到 sql 重用的目的
<!--抽取sql片段简化编写-->
<sql id="selectUser">
select * from User
</sql>
<select id="findById" parameterType="int" resultType="user">
<include refid="selectUser"/>
where id=#{id}
</select>
<select id="findByIds" parameterType="list" resultType="user">
<include refid="selectUser"/>
<where>
<foreach collection="array" open="id in(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select>
用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户
一对一查询的需求:查询一个订单,与此同时查询出该订单所属的用户
对应的sql语句:select * from orders o,user u where o.uid=u.id;
查询的结果如下:
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;
}
public interface OrderMapper {
List<Order> findAll();
}
<mapper namespace="com.cyd.mapper.OrderMapper">
<resultMap id="orderMap" type="com.cyd.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.cyd.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.cyd.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>
OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
List<Order> all = mapper.findAll();
for(Order order : all){
System.out.println(order);
}
用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户
一对多查询的需求:查询一个用户,与此同时查询出该用户具有的订单
对应的sql语句:select *,o.id oid from user u left join orders o on u.id=o.uid;
查询的结果如下:
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;
}
public interface UserMapper {
List<User> findAll();
}
<mapper namespace="com.cyd.mapper.UserMapper">
<resultMap id="userMap" type="com.cyd.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.cyd.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>
mapper>
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> all = mapper.findAll();
for(User user : all){
System.out.println(user.getUsername());
List<Order> orderList = user.getOrderList();
for(Order order : orderList){
System.out.println(order);
}
System.out.println("----------------------------------");
}
用户表和角色表的关系为,一个用户有多个角色,一个角色被多个用户使用
多对多查询的需求:查询用户同时查询出该用户的所有角色
对应的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;
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;
}
List<User> findAllUserAndRole();
<resultMap id="userRoleMap" type="com.cyd.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.cyd.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>
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> all = mapper.findAllUserAndRole();
for(User user : all){
System.out.println(user.getUsername());
List<Role> roleList = user.getRoleList();
for(Role role : roleList){
System.out.println(role);
}
System.out.println("----------------------------------");
}
这几年来注解开发越来越流行,Mybatis也可以使用注解开发方式,这样我们就可以减少编写 Mapper 映射文件了。我们先围绕一些基本的CRUD来学习,再学习复杂映射多表操作。
我们完成简单的user表的增删改查的操作
private UserMapper userMapper;
@Before
public void before() throws IOException {
InputStream resourceAsStream =
Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
userMapper = sqlSession.getMapper(UserMapper.class);
}
@Test
public void testAdd() {
User user = new User();
user.setUsername("测试数据");
user.setPassword("123");
user.setBirthday(new Date());
userMapper.add(user);
}
@Test
public void testUpdate() throws IOException {
User user = new User();
user.setId(16);
user.setUsername("测试数据修改");
user.setPassword("abc");
user.setBirthday(new Date());
userMapper.update(user);
}
@Test
public void testDelete() throws IOException {
userMapper.delete(16);
}
@Test
public void testFindById() throws IOException {
User user = userMapper.findById(1);
System.out.println(user);
}
@Test
public void testFindAll() throws IOException {
List<User> all = userMapper.findAll();
for(User user : all){
System.out.println(user);
}
}
修改MyBatis的核心配置文件,我们使用了注解替代的映射文件,所以我们只需要加载使用了注解的 Mapper接口即可。
<mappers>
<mapper class="com.cyd.mapper.UserMapper">mapper>
mappers>
或者指定扫描包含映射关系的接口所在的包也可以
<mappers>
<package name="com.lagou.mapper">package>
mappers>
实现复杂关系映射之前我们可以在映射文件中通过配置来实现,使用注解开发后,我们可以使用@Results注解,@Result注解,@One注解,@Many注解组合完成复杂关系的配置。
用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户
一对一查询的需求:查询一个订单,与此同时查询出该订单所属的用户
对应的sql语句:
select * from orders;
select * from user where id=查询出订单的uid;
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;
}
public interface OrderMapper {
List<Order> findAll();
}
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);
}
@Test
public void testSelectOrderAndUser() {
List<Order> all = orderMapper.findAll();
for(Order order : all){
System.out.println(order);
}
}
用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户
一对多查询的需求:查询一个用户,与此同时查询出该用户具有的订单
select * from user;
select * from orders where uid=查询出用户的id;
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;
}
List<User> findAllUserAndOrder();
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 = "orderList",column = "id",
javaType = List.class,
many = @Many(select ="com.lagou.mapper.OrderMapper.findByUid"))
})
List<User> findAllUserAndOrder();
}
public interface OrderMapper {
@Select("select * from orders where uid=#{uid}")
List<Order> findByUid(int uid);
}
List<User> all = userMapper.findAllUserAndOrder();
for(User user : all){
System.out.println(user.getUsername());
List<Order> orderList = user.getOrderList();
for(Order order : orderList){
System.out.println(order);
}
System.out.println("-----------------------------");
}
用户表和角色表的关系为,一个用户有多个角色,一个角色被多个用户使用
select * from user;
select * from role r,user_role ur where r.id=ur.role_id and ur.user_id=用户的id
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;
}
List<User> findAllUserAndRole();
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);
}
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> all = mapper.findAllUserAndRole();
for(User user : all){
System.out.println(user.getUsername());
List<Role> roleList = user.getRoleList();
for(Role role : roleList){
System.out.println(role);
}
System.out.println("----------------------------------");
}
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();
}
查看控制台打印情况:第一次查询时发出了sql语句,然后第二次查询直接打印用户对象,没有发出查询sql语句。
2、同样是对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();
}
一级缓存到底是什么?一级缓存什么时候被创建、一级缓存的工作流程是怎样的?相信你现在应该会有这几个疑问,那么我们本节就来研究一下一级缓存的本质。
大家可以这样想,上面我们一直提到一级缓存,那么提到一级缓存就绕不开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执行的,代码如下:
创建缓存key会经过一系列的update方法,udate方法由一个CacheKey这个对象来执行的,这个update方法最终由updateList的list来把五个值存进去,对照上面的代码和下面的图示,你应该能理解这五个值都是什么了。
这里需要注意一下最后一个值,configuration.getEnvironment().getId()这是什么,这其实就是定义在 mybatis-config.xml中的标签,见如下。
那么我们回归正题,那么创建完缓存之后该用在何处呢?总不会凭空创建一个缓存不使用吧?绝对不会的,经过我们对一级缓存的探究之后,我们发现一级缓存更多是用于查询操作,毕竟一级缓存也叫做查询缓存吧,为什么叫查询缓存我们一会儿说。我们先来看一下这个缓存到底用在哪了,我们跟踪到query方法如下:
如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。 localcache对象的put方法最终交给Map进行存放
二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。但是一级缓存是基于sqlSession的,而二级缓存是基于mapper文件的namespace的,也就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中。
1、开启二级缓存
和一级缓存默认开启不一样,二级缓存需要我们手动开启
首先在全局配置文件sqlMapConfig.xml文件中加入如下代码:
<settings>
<setting name="cacheEnabled" value="true"/>
settings>
其次在UserMapper.xml文件中开启缓存
<cache>cache>
我们可以看到mapper.xml文件中就这么一个空标签,其实这里可以配置,PerpetualCache这个类是mybatis默认实现缓存功能的类。我们不写type就使用mybatis默认的缓存,也可以去实现Cache接口来自定义缓存。
从PerpetualCache类中我们可以看到二级缓存底层还是HashMap结构
开启了二级缓存后,还需要将要缓存的pojo实现Serializable接口,为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们要再取这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接口。
2、测试
(1)测试二级缓存和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();
}
(2)测试执行commit()操作,二级缓存数据清空
@Test
public void testTwoCache(){
// 根据 sqlSessionFactory 产生 session
SqlSession sqlSession1 = sessionFactory.openSession();
SqlSession sqlSession2 = sessionFactory.openSession();
SqlSession sqlSession3 = sessionFactory.openSession();
String statement = "com.cyd.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();
}
查看控制台情况:
mybatis中还可以配置userCache和flushCache等配置项,userCache是用来设置是否禁用二级缓存的,在statement中设置useCache = false 可以禁用当前 select 语句的二级缓存,即每次查询都会发出 sql 去查询,默认情况是true,即该sql使用二级缓存。
<select id="selectUserByUserId" useCache="false" resultType="com.cyd.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.cyd.pojo.User" parameterType="int">
select * from user where id=#{id}
select>
一般下执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。所以我们不用设置,默认即可。
上面我们介绍了 mybatis自带的二级缓存,但是这个缓存是单服务器工作,无法实现分布式缓存。 那么什么是分布式缓存呢?假设现在有两个服务器1和2,用户访问的时候访问了1服务器,查询后的缓存就会放在1服务器上,假设现在有个用户访问的是2服务器,那么他在2服务器上就无法获取刚刚那个缓存,如下图所示:
为了解决这个问题,就得找一个分布式的缓存,专门用来存储缓存数据的,这样不同的服务器要缓存数据都往它那里存,取缓存数据也从它那里取,如下图所示:
如上图所示,在几个不同的服务器之间,我们使用第三方缓存框架,将缓存都放在这个第三方框架中,然后无论有多少台服务器,我们都能从缓存中获取数据。
刚刚提到过,mybatis提供了一个eache接口,如果要实现自己的缓存逻辑,实现cache接口开发即可。
mybatis本身默认实现了一个,但是这个缓存的实现无法实现分布式缓存,所以我们要自己来实现。
redis分布式缓存就可以,mybatis提供了一个针对cache接口的redis实现类,该类存在mybatis-redis包中实现:
1、pom文件
<dependency>
<groupId>org.mybatis.cachesgroupId>
<artifactId>mybatis-redisartifactId>
<version>1.0.0-beta2version>
dependency>
2、配置文件 Mapper.xml
<mapper namespace="com.cyd.mapper.IUserMapper">
<cache type="org.mybatis.caches.redis.RedisCache" />
<select id="findAll" resultType="com.cyd.pojo.User" useCache="true">
select * from user
select>
mapper>
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操作缓存;不过该项目在设计细节上有一些区别;
RedisCache在mybatis启动的时候,由MyBatis的CacheBuilder创建,创建的方式很简单,就是调用RedisCache的带有String参数的构造方法,即RedisCache(String id);而在RedisCache的构造方法中,调用了 RedisConfigurationBuilder 来创建 RedisConfig 对象,并使用 RedisConfig 来创建JedisPool。
RedisConfig类继承了 JedisPoolConfig,并提供了 host、port等属性的包装,简单看一下RedisConfig的属性:
RedisConfig对象是由RedisConfigurationBuilder创建的,看下这个类:
核心的方法就是parseConfiguration方法,该方法从classpath中读取一个redis.properties文件:并将该配置文件中的内容设置到RedisConfig对象中,并返回;
host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
password= database=0 clientName=
接下来,就是RedisCache使用,RedisConfig类创建完成jedisPool;在RedisCache中实现了一个简单的模板方法,用来操作Redis:
模板接口为RedisCallback,这个接口中就只需要实现了一个doWithRedis方法而已:
接下来看看Cache中最重要的两个方法:putObject和getObject,通过这两个方法来查看mybatis-redis 储存数据的格式:
可以很清楚的看到,mybatis-redis在存储数据的时候,是使用的hash结构,把cache的id作为这个hash的key (cache的id在mybatis中就是mapper的namespace);这个mapper中的查询缓存数据作为 hash 的 field,需要缓存的内容直接使用SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责对象的序列化和反序列化;
一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以MyBatis为例,我们可基于MyBatis插件机制实现分页、分表,监控等功能。由于插件和业务无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能。
Mybatis作为一个应用广泛的优秀的ORM开源框架,这个框架具有强大的灵活性,在四大组件(Executor
、StatementHandler
、ParameterHandler
、ResultSetHandler
)处提供了简单易用的插件扩展机制。Mybatis对持久层的操作就是借助于四大核心对象。MyBatis支持用插件对四大核心对象进行拦截,对mybatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的动态代理实现的,换句话说,MyBatis中的四大对象都是代理对象。
在四大对象创建的时候
拦截
插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来说
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是被重重代理后的对象。
如果我们想要拦截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.cyd.plugin.ExamplePlugin"/>
plugins>
这样MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。待准备工作做完后,MyBatis处于就绪状态。我们在执行SQL时,需要先通过DefaultSqlSessionFactory 创建 SqlSession。Executor 实例会在创建 SqlSession 的过程中被创建, Executor实例创建完毕后,MyBatis会通过JDK动态代理为实例生成代理类。这样,插件逻辑即可在 Executor相关方法被调用前执行。
以上就是MyBatis插件机制的基本原理。
Mybatis 插件接口-Interceptor
设计实现一个自定义插件
@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();
}
/**
* 主要是为了把这个拦截器生成一个代理放到拦截器链中
*
* @Description 包装目标对象 为目标对象创建代理对象
* @Param target为要拦截的对象
* @Return 代理对象
*/
@Override
public Object plugin(Object target) {
Object wrap = Plugin.wrap(target, this);
return wrap;
}
/**
* 获取配置文件的属性
* 插件初始化的时候调用,也只调用一次,插件配置的属性从这里设置进来
*
* @param properties
*/
@Override
public void setProperties(Properties properties) {
System.out.println("获取到的配置文件的参数是:" + properties);
}
}
sqlMapConfig.xml
<plugins>
<plugin interceptor="com.cyd.plugin.MySqlPagingPlugin">
<property name="name" value="Bob"/>
plugin>
plugins>
mapper接口
public interface UserMapper {
List<User> selectUser();
}
mapper.xml
<mapper namespace="com.cyd.mapper.UserMapper">
<select id="selectUser" resultType="com.cyd.pojo.User">
SELECT id,username FROM user
select>
mapper>
测试类
public class PluginTest {
@Test
public void test() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> byPaging = userMapper.selectUser();
for (User user : byPaging) {
System.out.println(user);
}
}
}
MyBatis可以使用第三方的插件来对功能进行扩展,分页助手PageHelper是将分页的复杂操作进行封装,使用简单的方式即可获得分页的相关数据。
开发步骤:
1、导入通用PageHelper坐标
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelperartifactId>
<version>3.7.5version>
dependency>
<dependency>
<groupId>com.github.jsqlparsergroupId>
<artifactId>jsqlparserartifactId>
<version>0.9.1version>
dependency>
2、在mybatis核心配置文件中配置PageHelper插件
*
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql"/>
plugin>
3、测试分页代码实现
@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());
通用Mapper就是为了解决单表增删改查,基于Mybatis的插件机制。开发人员不需要编写SQL,不需要在DAO中增加方法,只要写好实体类,就能支持相应的增删改查方法。
如何使用
1、首先在maven项目,在pom.xml中引入mapper的依赖
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapperartifactId>
<version>3.1.2version>
dependency>
2、 Mybatis配置文件中完成配置
<plugins>
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql"/>
plugin>
<plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
plugin>
plugins>
3、实体类设置主键
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
}
4、定义通用mapper
import com.cyd.domain.User;
import tk.mybatis.mapper.common.Mapper;
public interface UserMapper extends Mapper<User> {
}
5、测试
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 接口
// 保存一个实体,null值也会保存,不会使用数据库默认值
int insert = userMapper.insert(user);
// 保存实体,null的属性不会保存,会使用数据库默认值
int i = userMapper.insertSelective(user);
// update 接口
// 根据主键更新实体全部字段,null值会被更新
int i1 = userMapper.updateByPrimaryKey(user);
// 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);
}
}