Mybatis 讲义

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编码进⾏分
离,功能边界清晰,⼀个专注业务,⼀个专注数据。
分析图示如下:


image.png

第三部分:Mybatis基本应⽤

3.1 快速⼊⻔

MyBatis官⽹地址:http://www.mybatis.org/mybatis-3/

image.png

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的映射⽂件概述

image.png

3.1.6 ⼊⻔核⼼配置⽂件分析:

MyBatis核⼼配置⽂件层级关系

image.png

MyBatis常⽤配置解析
1)environments标签
数据库环境的配置,⽀持多环境配置

image.png

其中,事务管理器(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 实例。常⽤的有如下两个:

image.png

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 接⼝开发需要遵循以下规范:

  1. Mapper.xml⽂件中的namespace与mapper接⼝的全限定名相同
  2. Mapper接⼝⽅法名和Mapper.xml中定义的每个statement的id相同
  3. Mapper接⼝⽅法的输⼊参数类型和mapper.xml中定义的每个sql的parameterType的类型相同
  4. 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核⼼配置⽂件层级关系

image.png

4.2 MyBatis常⽤配置解析

1)environments标签
数据库环境的配置,⽀持多环境配置

image.png

其中,事务管理器(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⽂件

image.png

4)typeAliases标签
类型别名是为Java 类型设置⼀个短的名字。原来的类型名称配置如下

image.png

配置typeAliases,为com.lagou.domain.User定义别名为user
image.png

上⾯我们是⾃定义的别名,mybatis框架已经为我们设置好的⼀些常⽤的类型的别名
image.png

image.png

4.2 映射配置⽂件mapper.xml

动态sql语句
动态sql语句概述
Mybatis 的映射⽂件中,前⾯我们的 SQL 都是⽐较简单的,有些时候业务逻辑复杂时,我们的 SQL是动态变化
的,此时在前⾯的学习中我们的 SQL 就不能满⾜要求了。
参考的官⽅⽂档,描述如下:

image.png

我们根据实体类的不同取值,使⽤不同的 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 ⼀对⼀查询的模型

⽤户表和订单表的关系为,⼀个⽤户有多个订单,⼀个订单只从属于⼀个⽤户
⼀对⼀查询的需求:查询⼀个订单,与此同时查询出该订单所属的⽤户


image.png

5.1.2⼀对⼀查询的语句

对应的sql语句:select * from orders o,user u where o.uid=u.id;
查询的结果如下:


image.png

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 ⼀对多查询的模型

⽤户表和订单表的关系为,⼀个⽤户有多个订单,⼀个订单只从属于⼀个⽤户
⼀对多查询的需求:查询⼀个⽤户,与此同时查询出该⽤户具有的订单


image.png

5.2.2 ⼀对多查询的语句

对应的sql语句:select *,o.id oid from user u left join orders o on u.id=o.uid;
查询的结果如下:


image.png

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 多对多查询的模型

⽤户表和⻆⾊表的关系为,⼀个⽤户有多个⻆⾊,⼀个⻆⾊被多个⽤户使⽤
多对多查询的需求:查询⽤户同时查询出该⽤户的所有⻆⾊


image.png

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;
查询的结果如下:


image.png

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注解组合完成复杂关系的配置


image.png

6.4 ⼀对⼀查询

6.4.1 ⼀对⼀查询的模型

⽤户表和订单表的关系为,⼀个⽤户有多个订单,⼀个订单只从属于⼀个⽤户
⼀对⼀查询的需求:查询⼀个订单,与此同时查询出该订单所属的⽤户


image.png

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 ⼀对多查询的模型

⽤户表和订单表的关系为,⼀个⽤户有多个订单,⼀个订单只从属于⼀个⽤户
⼀对多查询的需求:查询⼀个⽤户,与此同时查询出该⽤户具有的订单


image.png

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 多对多查询的模型

⽤户表和⻆⾊表的关系为,⼀个⽤户有多个⻆⾊,⼀个⻆⾊被多个⽤户使⽤
多对多查询的需求:查询⽤户同时查询出该⽤户的所有⻆⾊


image.png

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();
    }

查看控制台打印情况:


image.png

② 、同样是对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();
    }

查看控制台打印情况:

image.png

③、总结
1、第⼀次发起查询⽤户id为1的⽤户信息,先去找缓存中是否有id为1的⽤户信息,如果没有,从 数据库查询⽤户
信息。得到⽤户信息,将⽤户信息存储到⼀级缓存中。
2、 如果中间sqlSession去执⾏commit操作(执⾏插⼊、更新、删除),则会清空SqlSession中的 ⼀级缓存,这
样做的⽬的为了让缓存中存储的是最新的信息,避免脏读。
3、 第⼆次发起查询⽤户id为1的⽤户信息,先去找缓存中是否有id为1的⽤户信息,缓存中有,直 接从缓存中获取
⽤户信息
image.png

⼀级缓存原理探究与源码分析
⼀级缓存到底是什么?⼀级缓存什么时候被创建、⼀级缓存的⼯作流程是怎样的?相信你现在应该会有 这⼏个疑
问,那么我们本节就来研究⼀下⼀级缓存的本质
⼤家可以这样想,上⾯我们⼀直提到⼀级缓存,那么提到⼀级缓存就绕不开SqlSession,所以索性我们 就直接从
SqlSession,看看有没有创建缓存或者与缓存有关的属性或者⽅法
image.png

调研了⼀圈,发现上述所有⽅法中,好像只有clearCache()和缓存沾点关系,那么就直接从这个⽅ 法⼊⼿吧,分析
源码时,我们要看它(此类)是谁,它的⽗类和⼦类分别⼜是谁,对如上关系了解了,你才 会对这个类有更深的认
识,分析了⼀圈,你可能会得到如下这个流程图
image.png

再深⼊分析,流程⾛到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来把五个值存进去,对照上⾯的代码和下⾯的图示,你应该能 理解这五个值都是什么了


image.png

这⾥需要注意⼀下最后⼀个值,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&amp;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查询到的数据也将存在相同的⼆级缓存区域中


image.png

如何使⽤⼆级缓存
① 、开启⼆级缓存
和⼀级缓存默认开启不⼀样,⼆级缓存需要我们⼿动开启
⾸先在全局配置⽂件sqlMapConfig.xml⽂件中加⼊如下代码:

  <settings>
     <setting name="cacheEnabled" value="true"/>
 </settings>

其次在UserMapper.xml⽂件中开启缓存

<!--开启⼆级缓存-->
<cache></cache>

我们可以看到mapper.xml⽂件中就这么⼀个空标签,其实这⾥可以配置,PerpetualCache这个类是 mybatis默认实
现缓存功能的类。我们不写type就使⽤mybatis默认的缓存,也可以去实现Cache接⼝ 来⾃定义缓存。


image.png

我们可以看到⼆级缓存底层还是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();
    }

查看控制台情况:


image.png

④、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服务器上就⽆法获取刚刚那个 缓存,如下图所示:


image.png

为了解决这个问题,就得找⼀个分布式的缓存,专⻔⽤来存储缓存数据的,这样不同的服务器要缓存数 据都往它那
⾥存,取缓存数据也从它那⾥取,如下图所示:


image.png

如上图所示,在⼏个不同的服务器之间,我们使⽤第三⽅缓存框架,将缓存都放在这个第三⽅框架中, 然后⽆论有
多少台服务器,我们都能从缓存中获取数据。
这⾥我们介绍mybatis与redis的整合。
刚刚提到过,mybatis提供了⼀个eache接⼝,如果要实现⾃⼰的缓存逻辑,实现cache接⼝开发即可。
mybati s本身默认实现了⼀个,但是这个缓存的实现⽆法实现分布式缓存,所以我们要⾃⼰来实现。
redis分布式缓存就可以,mybatis提供了⼀个针对cache接⼝的redis实现类,该类存在mybatis-redis包 中
实现:

  1. 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中的四⼤对象都是代理对象


image.png

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中增
加⽅法,只要写好实体类,就能⽀持相应的增删改查⽅法
如何使⽤

  1. ⾸先在maven项⽬,在pom.xml中引⼊mapper的依赖
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper</artifactId>
            <version>3.1.2</version>
        </dependency>
  1. Mybatis配置⽂件中完成配置
        <plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
            <!-- 通⽤Mapper接⼝,多个通⽤接⼝⽤逗号隔开 -->
            <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
        </plugin>
  1. 实体类设置主键
  @Table(name = "t_user")
  public class User {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     private Integer id;
     private String username; 
}
  1. 定义通⽤mapper
import com.lagou.domain.User;
import tk.mybatis.mapper.common.Mapper;
public interface UserMapper extends Mapper<User> {
  定义自己的通用mapper方法
}

第九部分:Mybatis架构原理

9.1架构设计

image.png

我们把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语句以及相应的参数信息
image.png

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) 返回处理结果
将最终的处理结果返回。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358