# Mybatis

整理了有关mybatis框架的知识，方便查阅和学习

MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code，并且改名为MyBatis 。2013年11月迁移到Github。MyBatis是一个优秀的持久层框架，它对jdbc的操作数据库的过程进行封装，使开发者只需要关注 SQL 本身，而不需要花费精力去处理例如注册驱动、创建connection、创建statement、手动设置参数、结果集检索等jdbc繁杂的过程代码。

Mybatis通过xml或注解的方式将要执行的各种statement（statement、preparedStatement、CallableStatement）配置起来，并通过java对象和statement中的sql进行映射生成最终执行的sql语句，最后由mybatis框架执行sql并将结果映射成java对象并返回。

## Mybatis优势

### JDBC 问题总结

JDBC编程步骤：

1. 加载数据库驱动
2. 创建并获取数据库连接
3. 创建jdbc statement 语句对象
4. 设置sql语句
5. 设置sql语句参数（preparedStatement）
6. 使用statement执行sql并获取结果
7. 对sql执行结果进行解析处理
8. 释放资源（resultSet、preparement、connection）

主要问题：

1. 数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能，如果使用数据库链接池可解决此问题。
2. Sql语句在代码中硬编码，造成代码不易维护，实际应用sql变化的可能较大，sql变动需要改变java代码。
3. 使用preparedStatement向占有位符号传参数存在硬编码，因为sql语句的where条件不一定，可能多也可能少，修改sql还要修改代码，系统不易维护。
4. 对结果集解析存在硬编码（查询列名），sql变化导致解析代码变化，系统不易维护，如果能将数据库记录封装成pojo对象解析比较方便。

### Mybatis VS Hibernate

| Hibernate              | Mybatis             |
| ---------------------- | ------------------- |
| 以对象为中心的ORM             | 以数据库中心              |
| 不适合大吞吐量                |                     |
| 极为方便的ORM操作,以及HQL       | 需要写SQL，较为冗余         |
| 简单查询支持好，但复杂SQL以及多表查询较差 | 复杂SQL支持好            |
| 学习成本高，SQL优化困难          | 学习成本低，针对原生SQL进行优化即可 |
| 内部实现复杂，拓展难度大           | 内部实现简单              |
| 随项目增长维护成本逐渐升高          | 维护成本相对较低            |

Mybatis和hibernate不同，它不完全是一个ORM框架，因为MyBatis需要程序员自己编写Sql语句，不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句，并将java对象和sql语句映射生成最终执行的sql，最后将sql执行的结果再映射生成java对象。

Mybatis学习门槛低，简单易学，程序员直接编写原生态sql，可严格控制sql执行性能，灵活度高，非常适合对关系数据模型要求不高的软件开发，例如互联网软件、企业运营类软件等，因为这类软件需求变化频繁，一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性，如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件，工作量大。

Hibernate对象/关系映射能力强，数据库无关性好，对于关系模型要求高的软件（例如需求固定的定制化软件）如果用hibernate开发可以节省很多代码，提高效率。但是Hibernate的学习门槛高，要精通门槛更高，而且怎么设计O/R映射，在性能和对象模型之间如何权衡，以及怎样用好Hibernate需要具有很强的经验和能力才行。

总之，按照用户的需求在有限的资源环境下只要能做出维护性、扩展性良好的软件架构都是好架构，所以框架只有适合才是最好。

## MyBatis架构

![img](/files/rXZKKRhFrThRJqZsBZpp)

1. mybatis配置，`SqlMapConfig.xml`，mybatis的全局配置文件，配置了mybatis的运行环境等信息。`mapper.xml`文件即sql映射文件，文件中配置了操作数据库的sql语句。此文件需要在`SqlMapConfig.xml`中加载。
2. 通过`mybatis`环境等配置信息构造`SqlSessionFactory`即会话工厂
3. 由会话工厂创建sqlSession即会话，操作数据库需要通过`sqlSession`进行。
4. `mybatis`底层自定义了Executor执行器接口操作数据库，`Executor`接口有两个实现，一个是基本执行器、一个是缓存执行器。
5. `MappedStatement`也是`mybatis`一个底层封装对象，它包装了`mybatis`**配置信息**及**sql映射信息**等。`mapper.xml`文件中**一个sql对应一个`MappedStatement`对象**，**sql的id即是`MappedStatement`的id**。
6. `MappedStatement`对sql执行输入参数进行定义，包括`HashMap`、基本类型、`pojo`，`Executor`通过`MappedStatement`在执行sql前将输入的java对象映射至sql中，输入参数映射就是jdbc编程中对`preparedStatement`设置参数。
7. `MappedStatement`对sql执行输出结果进行定义，包括`HashMap`、基本类型、`pojo`，`Executor`通过MappedStatement在执行sql后将输出结果映射至java对象中，输出结果映射过程相当于jdbc编程中对结果的解析处理过程。

## 环境构建

官方网址：[https://github.com/mybatis/mybatis-3/releases](https://gitee.com/link?target=https%3A%2F%2Fgithub.com%2Fmybatis%2Fmybatis-3%2Freleases)

* mybatis-3.2.7.jar----mybatis的核心包
* lib----mybatis的依赖包
* mybatis-3.2.7.pdf----mybatis使用手册

**添加maven依赖**：

```xml
<!--添加数据库驱动-->
<!--添加数据库连接池-->
<!--添加mybatis依赖-->
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.4.2</version>
</dependency>
```

**mybatis默认使用log4j进行日志输出，需要在resources下添加log4j配置文件`log4j.properties`**

```properties
# Global logging configuration
log4j.rootLogger=DEBUG, stdout
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
```

**核心配置文件编写：**

在classpath/resources下创建`SqlMapConfig.xml`，如下：

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <!-- 和spring整合后 environments配置将废除-->
  <environments default="development">
    <environment id="development">
      <!-- 使用jdbc事务管理-->
      <transactionManager type="JDBC" />
      <!-- 数据库连接池-->
      <dataSource type="POOLED">
        <property name="driver" value="oracle.jdbc.driver.OracleDriver" />
        <property name="url" value="jdbc:oracle:thin:@localhost:1521:XE" />
        <property name="username" value="jsd1703" />
        <property name="password" value="jsd1703" />
      </dataSource>
    </environment>
  </environments>
  <!--加载sql映射文件，否则不会生效-->
  <mappers>
    <mapper resource="User.xml"/>
  </mappers>
</configuration>
```

### 配置sql映射文件

`User.xml`:

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="user">
    <!--
        id sql的唯一标识
        parameterType 传入的参数的类型
        resultType 返回结果集类型
        #{} 占位符，如果是常见的基本类型，那么中的变量名称可以随意设置
    -->
    <!--查询单个-->
    <select id="findById" parameterType="java.lang.Integer" resultType="me.feathers.pojo.User">
        SELECT *
        FROM WEB_USER
        WHERE id = #{id}
    </select>

    <!--查询多个-->
  	<!--注意，在sql语句上不可以使用/**/或者-- 注释，否则会被解析为ognl表达式，报 org.apache.ibatis.ognl.ExpressionSyntaxException: Malformed OGNL expression:  [org.apache.ibatis.ognl.ParseException-->
    <select id="findAll" parameterType="java.lang.String"
            resultType="me.feathers.pojo.User">
        SELECT *
        FROM WEB_USER
        WHERE username LIKE '%${value}%'
    </select>

    <!--插入-->
    <!--如果传入的是pojo类型，那么#{}中必须是pojo的属性-->
    <!--
		selectKey：用来生成主键
		keyProperty:返回的主键存储在pojo中的哪个属性,
		order:selectKey的执行顺序（相对于insert语句来说） 有berfore和after两种选择，oracle的序列需要先生成，所以这里使用berfore
				而mysql的自增原理执行完insert语句之后才能将主键生成，mysql需要使用after
	-->
    <insert id="save" parameterType="me.feathers.pojo.User">
        <selectKey keyProperty="id" order="BEFORE" resultType="java.lang.Long">
            SELECT "sq_web_user".nextval FROM dual
        </selectKey>
        INSERT INTO WEB_USER (ID, PASSWORD, STATE, USERNAME)
        VALUES (${id},#{password}, #{state}, #{username})
    </insert>

       <delete id="delById" parameterType="java.lang.Integer">
        DELETE FROM WEB_USER
        WHERE ID = #{id}
    </delete>

    <update id="update" parameterType="me.feathers.pojo.User">
        update WEB_USER set username=#{username}, PASSWORD=#{password}, STATE=#{state} 
      	where id=#{id}
    </update>
  
</mapper>
```

> 注意：
>
> `#{}`表示一个占位符号，通过`#{}`可以实现`preparedStatement`向占位符中设置值，自动进行java类型和jdbc类型转换，`#{}`可以有效防止sql注入。`#{}`可以接收简单类型值或pojo属性值。 如果`parameterType`传输单个简单类型值，`#{}`括号中可以是value或其它名称。
>
> `${}`表示拼接sql串，通过`${}`可以将`parameterType` 传入的内容拼接在sql中且不进行jdbc类型转换， `${}`可以接收简单类型值或pojo属性值，如果`parameterType`传输单个简单类型值，`${}`括号中只能是value。

> mysql的主键生成策略：
>
> 使用uuid：`select uuid()`
>
> 使用 ast\_insert\_id()寒素， `select AST_INSERT_ID()`，返回auto\_increment自增列新记录id值。

### 书写Java代码

在sql映射文件中已经包含了增删改查的sql语句，现在可以通过代码完成增删改查的操作了。

```java
// 查找单个
@Test
public void testFindById() throws IOException {
  // 1. 加载配置文件
  InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
  // 2. 使用核心配置文件拿到会话工厂
  SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
  // 3. 使用工厂获取会话
  SqlSession s = factory.openSession();
  // 4. 使用session进行操作
  // 参1： sql语句，namespace.sqlId
  User u = s.selectOne("user.findById", 1);
  System.out.println(u+"---------");
  // 5. 关闭会话
  s.close();
}
```

查找多个：

```java
List<User> us = s.selectList("user.findAll", "ro");
```

插入：

```java
User user = s.selectOne("user.findById", 4);
int count = s.delete("user.delById", user);
s.commit(); //注意提交
```

更新：

```java
User user = s.selectOne("user.findById", 4);
user.setPassword("456789");
user.setUsername("李四");
int count = s.update("user.update", user);
s.commit();
```

删除：

```java
int count = s.delete("user.delById", 4); // count为1 代表删除了一条
```

> selectOne查询一条记录，如果使用selectOne查询多条记录则抛出异常：
>
> ```
> org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 3
> 	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:70)
> ```

### MyBatis 解决的问题

1. 数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能，如果使用数据库链接池可解决此问题。

   解决：在`SqlMapConfig.xml`中配置数据链接池，使用连接池管理数据库链接。
2. Sql语句写在代码中造成代码不易维护，实际应用sql变化的可能较大，sql变动需要改变java代码。

   解决：将Sql语句配置在`XXXXmapper`.xml文件中与java代码分离。
3. 向sql语句传参数麻烦，因为sql语句的where条件不一定，可能多也可能少，占位符需要和参数一一对应。

   解决：Mybatis自动将java对象映射至sql语句，通过statement中的parameterType定义输入参数的类型。
4. 对结果集解析麻烦，sql变化导致解析代码变化，且解析前需要遍历，如果能将数据库记录封装成pojo对象解析比较方便。

   解决：Mybatis自动将sql执行结果映射至java对象，通过statement中的resultType定义输出结果的类型。

## MyBatis常见类的作用

### SqlSessionFactoryBuilder

`SqlSessionFactoryBuilder`用于创建`SqlSessionFacoty`，`SqlSessionFacoty`一旦创建完成就不需要`SqlSessionFactoryBuilder`了，因为`SqlSession`是通过`SqlSessionFactory`生产，所以可以将`SqlSessionFactoryBuilder`当成一个工具类使用，最佳使用范围是方法范围即方法体内局部变量。

### SqlSessionFactory

是一个接口，接口中定义了openSession()的不同重载方法，SqlSessionFactory的最佳使用范围是整个应用运行期间，一旦创建后可以重复使用，通常以单例模式管理SqlSessionFactory。

### SqlSession

SqlSession是一个面向用户的接口，封装了对数据库的操作，如：查询、插入、更新、删除等。

通过`SqlSessionFactory`创建`SqlSession`，而`SqlSessionFactory`是通过`SqlSessionFactoryBuilder`进行创建。

每个线程都应该有它自己的SqlSession实例。SqlSession的实例不能共享使用，它也是线程不安全的。因此最佳的范围是请求或方法范围。绝对不能将SqlSession实例的引用放在一个类的静态字段或实例字段中。

打开一个 SqlSession；使用完毕就要关闭它。通常把这个关闭操作放到 finally 块中以确保每次都能执行关闭。如下：

```java
SqlSession session = sqlSessionFactory.openSession();
try {
  // do work
} finally {
  session.close();
}
```

## 使用MyBatis开发DAO

使用MyBatis开发Dao有两种方式：

1. 使用原始的Dao开发方式
2. 使用Mapper接口开发方式

### 原始Dao开发

interface: `UserDao`

```java
public interface UserDao {
    void save(User user);
    User findById(Integer id);
    List<User> findAll(String name);
    void update(User user);
    void delById(Integer id);
}
```

实现：

```java
/**
 * @author Feathers
 * @create 2017-07-17-14:59
 */
public class UserDaoImpl implements UserDao {

    private SqlSessionFactory ssf;

    public UserDaoImpl(SqlSessionFactory ssf){
        this.ssf = ssf;
    }

    @Override
    public void save(User user) {
        SqlSession s = ssf.openSession();
        try {
            s.insert("user.save", user);
            s.commit();
        } finally {
            s.close();
        }
    }

    @Override
    public User findById(Integer id) {
        SqlSession s = ssf.openSession();
        User user;
        try {
            user = s.selectOne("user.findById", id);
            s.commit();
        } finally {
            s.close();
        }
        return user;
    }

    @Override
    public List<User> findAll(String name) {
        SqlSession s = ssf.openSession();
        List<User> users;
        try {
            users = s.selectList("user.findAll", name);
            s.commit();
        } finally {
            s.close();
        }
        return users;
    }

    @Override
    public void update(User user) {
        SqlSession s = ssf.openSession();
        try {
            s.update("user.update", user);
            s.commit();
        } finally {
            s.close();
        }
    }

    @Override
    public void delById(Integer id) {
        SqlSession s = ssf.openSession();
        try {
            s.delete("user.delById", id);
            s.commit();
        } finally {
            s.close();
        }
    }
}
```

测试类：

```java
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import me.feathers.dao.UserDao;
import me.feathers.dao.UserDaoImpl;
import me.feathers.pojo.User;

/**
 * @author Feathers
 * @create 2017-07-17-15:12
 */
public class UserDaoTest {

    private UserDao dao;

    @BeforeTest
    public void test() throws IOException {
        // 1. 加载配置文件
        InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 2. 使用核心配置文件拿到会话工厂
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);

        dao = new UserDaoImpl(factory);
    }

    @Test(priority = 1)
    public void testSave() {
        dao.save(new User("赵六", "123456", 0));
    }

    @Test(priority = 2)
    public void testFindById(){
        User byId = dao.findById(4);
        System.out.println(byId);
    }

    @Test(priority = 3)
    public void testUpdate() {
        User byId = dao.findById(4);
        byId.setUsername("王五");
        dao.update(byId);
    }

    @Test(priority = 4)
    public void testFindAll(){
        List<User> all = dao.findAll("");
        System.out.println(all);
    }

    @Test(priority = 5)
    public void testDelById(){
        dao.delById(4);
    }
}
```

> 原始Dao开发中存在以下问题：
>
> * Dao方法体存在重复代码：通过SqlSessionFactory创建SqlSession，调用SqlSession的数据库操作方法
> * 调用sqlSession的数据库操作方法需要指定statement的id，这里存在硬编码，不利于开发维护。

#### MyBatis模板工具类

```java
/*MyBatisUtil,负责加载配置文件，创建sqlSessionFactory，获取sqlSession*/
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;

public class MyBatisUtil {

    private static SqlSessionFactory sqlSessionFactory;

    static {
        try {
            InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static SqlSession getSqlSession() {
        return sqlSessionFactory == null ? null : sqlSessionFactory.openSession();
    }

    public static void close(SqlSession sqlSession){
        if (sqlSession != null) {
            sqlSession.close();
        }
    }
}
/*MyBatis回调接口，负责完成执行的操作*/
import org.apache.ibatis.session.SqlSession;
public interface MyBatisCallback {
    Object doInMyBatis(SqlSession ses);
}
package me.feathers.online.util;

import org.apache.ibatis.session.SqlSession;

/**
 * mybatis模板工具类
 *
 * @author Feathers
 * @create 2017-07-30-10:51
 */
public class MyBatisTemplate {

    public static Object excute(MyBatisCallback callback) {
        SqlSession sqlSession = MyBatisUtil.getSqlSession();
        Object o = null;
        try {
            o = callback.doInMyBatis(sqlSession);
        } finally {
            MyBatisUtil.close(sqlSession);
        }
        return o;
    }
}
/**调用实例**/
 @Override
public User findById(Long id) {
  return (User) MyBatisTemplate.excute(ses -> {
    return ses.selectOne("user.findById", id);;
  });
}
```

> 如果你要添加Transaction，请修改Template类

### Mapper动态代理方式

#### 开发规范

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`的类型相同

#### 定义Mapper接口

UserMapper.java:

```java
public interface UserMapper {

    void save(User user);

    User findById(Integer id);

    List<User> findAll(String name);

    void update(User user);

    void delById(Integer id);
}
```

#### Mapper.xml

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--1. Mapper.xml文件中的namespace与mapper接口的类路径相同。-->
<mapper namespace="me.feathers.mapper.UserMapper">

    <!--2. Mapper接口方法名和Mapper.xml中定义的每个statement的id相同 -->
    <!--3. Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同-->
    <!--4. Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同-->
    <select id="findById" parameterType="java.lang.Integer" resultType="me.feathers.pojo.User">
        SELECT *
        FROM WEB_USER
        WHERE id = #{id}
    </select>

  	<!--注意，这里结果集类型不是java.util.List-->
    <select id="findAll" parameterType="java.lang.String"
            resultType="me.feathers.pojo.User">
        SELECT *
        FROM WEB_USER
        WHERE username LIKE '%${value}%'
    </select>

    <insert id="save" parameterType="me.feathers.pojo.User">
        <selectKey keyProperty="id" order="BEFORE" resultType="java.lang.Long">
            SELECT "sq_web_user".nextval FROM dual
        </selectKey>
        INSERT INTO WEB_USER (ID, PASSWORD, STATE, USERNAME)
        VALUES (${id},#{password}, #{state}, #{username})
    </insert>

    <delete id="delById" parameterType="java.lang.Integer">
        DELETE FROM WEB_USER
        WHERE ID = #{id}
    </delete>

    <update id="update" parameterType="me.feathers.pojo.User">
        update WEB_USER set username=#{username}, PASSWORD=#{password}, STATE=#{state} where
            id=#{id}
    </update>

</mapper>
```

#### 加载映射文件

```xml
<!-- 加载映射文件 -->
<mappers>
  <mapper resource="Mapper.xml"/>
</mappers>
```

#### 编写测试类

```java
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import me.feathers.dao.UserDao;
import me.feathers.dao.UserDaoImpl;
import me.feathers.mapper.UserMapper;
import me.feathers.pojo.User;

/**
 * @author Feathers
 * @create 2017-07-17-15:12
 */
public class UserMapperTest {

    SqlSessionFactory factory;

    @BeforeTest
    public void test() throws IOException {
        // 1. 加载配置文件
        InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 2. 使用核心配置文件拿到会话工厂
        factory = new SqlSessionFactoryBuilder().build(in);
    }

    @Test(priority = 1)
    public void testSave() {
        SqlSession session = factory.openSession();
        //获取mapper接口的代理对象
        UserMapper userMapper = session.getMapper(UserMapper.class);
        //调用代理对象方法
        userMapper.save(new User("赵六", "123456", 0));
    }

    @Test(priority = 2)
    public void testFindById(){
        SqlSession session = factory.openSession();
        //获取mapper接口的代理对象
        UserMapper userMapper = session.getMapper(UserMapper.class);
        User byId = userMapper.findById(4);
        System.out.println(byId);
    }

    @Test(priority = 3)
    public void testUpdate() {
        SqlSession session = factory.openSession();
        //获取mapper接口的代理对象
        UserMapper userMapper = session.getMapper(UserMapper.class);
        //调用代理对象方法
        User byId = userMapper.findById(4);
        byId.setUsername("王五");
        userMapper.update(byId);
    }

    @Test(priority = 4)
    public void testFindAll(){
        SqlSession session = factory.openSession();
        //获取mapper接口的代理对象
        UserMapper userMapper = session.getMapper(UserMapper.class);
        //调用代理对象方法
        List<User> all = userMapper.findAll("");
        System.out.println(all);
    }

    @Test(priority = 5)
    public void testDelById() {
        SqlSession session = factory.openSession();
        //获取mapper接口的代理对象
        UserMapper userMapper = session.getMapper(UserMapper.class);
        //调用代理对象方法
        userMapper.delById(4);
    }
}
```

## SqlMapConfig.xml

配置内容：

* **properties**（属性）
* settings（全局配置参数）
* **typeAliases**（类型别名）
* typeHandlers（类型处理器）
* objectFactory（对象工厂）
* plugins（插件）
* environments（环境集合属性对象）
* environment（环境子属性对象）
* transactionManager（事务管理）
* dataSource（数据源）
* **mappers**（映射器）

### properties

将数据库信息提取到 `db.properties`中，然后引入到myBatis的核心配置文件中进行配置

```xml
<!--引入db.properties-->
<properties resource="config/db.properties"/>

<!-- 和spring整合后 environments配置将废除-->
<environments default="development">
  <environment id="development">
    <!-- 使用jdbc事务管理-->
    <transactionManager type="JDBC" />
    <!-- 数据库连接池-->
    <dataSource type="POOLED">
      <property name="driver" value="${driverClass}" />
      <property name="url" value="${url}" />
      <property name="username" value="${user}" />
      <property name="password" value="${password}" />
    </dataSource>
  </environment>
</environments>
```

MyBatis 将按照下面的顺序来加载属性：

* 在 properties 元素体内定义的属性首先被读取。
* 然后会读取properties 元素中resource或 url 加载的属性，它会覆盖已读取的同名属性。

### typeAliases

类型别名，MyBatis支持的类型别名：

| 别名         | 映射的类型      |
| ---------- | ---------- |
| \_byte     | byte       |
| \_long     | long       |
| \_short    | short      |
| \_int      | int        |
| \_integer  | int        |
| \_double   | double     |
| \_float    | float      |
| \_boolean  | boolean    |
| string     | String     |
| byte       | Byte       |
| long       | Long       |
| short      | Short      |
| int        | Integer    |
| integer    | Integer    |
| double     | Double     |
| float      | Float      |
| boolean    | Boolean    |
| date       | Date       |
| decimal    | BigDecimal |
| bigdecimal | BigDecimal |
| map        | Map        |

> 作用：
>
> 在MyBatis中的类型属性可以使用一上的类型别名替代，比如`parameterType="int"`就同`parameterType="java.lang.Integer"`是一样的
>
> 好处，简化了开发

自定以类型别名：

> 类型别名的定义在MyBatis `SqlMapConfig.xml`的核心配置文件中！！！

```xml
<typeAliases>
  <!-- 单个别名定义 -->
  <typeAlias alias="user" type="me.feathers.pojo.User"/>
  <!-- 批量别名定义，扫描整个包下的类，每个类的别名为类的类名（首字母大写或小写都可以） -->
  <!--<package name="me.feathers.pojo"/>-->
  <!--<package name="其它包"/>-->
</typeAliases>
```

### mappers

配置映射器。

* 使用相对类路径的资源：`<mapper resource="sqlmap/User.xml" />`
* 使用mapper接口类路径：`<mapper class="me.feathers.mapper.UserMapper"/>`
* 自动注册指定包下的所有mapper接口，`<package name="me.feathers.mapper"/>`

## 输入映射和输出映射

Mapper.xml映射文件中定义了操作数据库的sql，每个sql是一个statement，映射文件是mybatis的核心。

### parameterType 输入类型

1. **传递简单类型**
2. **传递pojo对象**
3. **传递pojo包装类型**

**传递pojo包装类型：**

开发中通过pojo传递查询条件 ，查询条件是综合的查询条件，不仅包括用户查询条件还包括其它的查询条件（比如将用户购买商品信息也作为查询条件），这时可以使用包装对象传递输入参数。

**UserVo （Value Object）**:

```xml
// 一定要提供getter/setter方法，ognl 表达式底层和el表达式相同，是使用getter setter方法获取属性的
public class UserVo {

    private User user;

    public void setUser(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }
}
```

Mapper接口方法：

```
List<User> findUserByUserVo(UserVo userVo);
```

Mapper配置文件：

```
<select id="findUserByUserVo" parameterType="me.feathers.pojo.vo.UserVo"
        resultType="user">
  SELECT * FROM WEB_USER where username like '%${user.username}%' 
</select>
<!--这里使用ognl取出查询参数，并拼接到sql语句中-->
```

### resultType 输出类型

1. 输出简单类型
2. 输出pojo类型
3. 输出pojo列表

这里不再演示，请查看上面的代码。

### resultMap

**resultType可以将查询结果映射为pojo**，但需要pojo的属性名和sql查询的列名一致方可映射成功。如果查询字段与pojo的属性名不一致，可以通过resultMap将字段名和属性名一一对应，完成映射。

```xml
<resultMap type="user" id="userListResultMap">
	<id column="id_" property="id"/>
  	<result column="username_" property="username"/>
   	<result column="password_" property="password"/>
</resultMap>
```

`<id/>`标签：此属性表示查询结果集的唯一标识，非常重要。如果是多个字段为复合唯一约束则定义多个`<id />`。

* `Property`：表示User类的属性。
* `Column`：表示sql查询出来的字段名。Column和property放在一块儿表示将sql查询出来的字段映射到指定的pojo类属性上。
* `<result />`：普通结果，即pojo的属性。

如果列名和表名一致，可以不写。

## 动态sql

通过mybatis提供的各种标签方法实现动态拼接sql。

### if

之前在传递参数到sql中时，如果参数为null，则会发生错误，为了避免查询参数不合法的情况，我们可以使用if进行有效性判断。

```xml
<select id="findAll" parameterType="java.lang.String"
        resultType="User">
  SELECT *
  FROM WEB_USER
  WHERE 1=1
  <if test="username!=null and username!=''">
    AND username LIKE '%${value}%'
  </if>
</select>
```

> 注意，字符串参数一定要进行空字符串判断

### where

上面使用了`1=1` 来保证sql语句的完成性，我们也可以使用where标签替代。

```xml
<select id="findAll" parameterType="java.lang.String"
        resultType="User">
  SELECT *
  FROM WEB_USER
  <where>
    <if test="username!=null and username!=''">
    	AND username LIKE '%${value}%'
  	</if>
  </where>
</select>
```

备注：`<where />`可以自动处理第一个and。

### foreach

当向sql传递数组或List，mybatis使用foreach进行解析：

```sql
SELECT * FROM USERS WHERE username LIKE '%张%' AND id IN (10,89,16)
```

sql片段：

```xml
<if test="ids!=null and ids.size>0">
  <!--ids 要遍历的集合，将集合遍历，拼接为sql字符串 open 开始拼接  close 结束拼接， separator 分隔符， 得到的结果为 and id in (10,89,16)-->
  <foreach collection="ids" open=" and id in(" close=")" item="id" separator="," > 
    #{id}
  </foreach>
</if>
```

### Sql 片段

xml中的sql可能会发生大量重复的sql片段，我们可以将他们抽取到sql标签中，然后使用include引用这些sql片段。

```xml
<select id="findUserList" parameterType="user" resultType="user">
  select * from user 
  <where>
    <if test="id!=null and id!=''">
      and id=#{id}
    </if>
    <if test="username!=null and username!=''">
      and username like '%${username}%'
    </if>
  </where>
</select>
```

抽出条件

```xml
<sql id="query_user_where">
	<if test="id!=null and id!=''">
		and id=#{id}
	</if>
	<if test="username!=null and username!=''">
		and username like '%${username}%'
	</if>
</sql>
```

使用include引用：

```xml
<select id="findUserList" parameterType="user" resultType="user">
  select * from user 
  <where>
    <include refid="query_user_where"/>
  </where>
</select>
```

## 关联查询

### 一对一关联查询

#### 预备工作

```sql
drop table wife;
drop table husband;

create table husband(
	hus_id number(19) primary key,
	hus_name varchar2(20), 
	age number(3), 
	birthday date
);

create table wife(
	wife_id number(19) primary key,
	wife_name varchar2(255), 
	age number(3), 
	birthday date, 
	hus_id number(19),
	foreign key(hus_id) references husband(hus_id) 
);

insert into husband(hus_id, hus_name, age, birthday) values(1, '张山', 24, sysdate);
insert into husband(hus_id, hus_name, age, birthday) values(2, '李斯', 25, sysdate);
insert into wife(wife_id, wife_name, age, birthday, hus_id) values(1, '小红', 24, sysdate, 2);
insert into wife(wife_id, wife_name, age, birthday, hus_id) values(2, '丽丽', 23, sysdate, 1);
```

有两张表，分别为妻子和丈夫， 属于一对一的关系。要求根据丈夫查找对应的妻子。

#### 方式一

一对一自动映射,试用resultType，定义夫妻信息po类，该类包含了丈夫和妻子的信息，即所要查的信息。

现在我们要查询所有的夫妻对，有如下sql语句：

```sql
 select w.wife_name, w.age wife_age, hus.* from husband hus join wife w on w.hus_id = HUS.hus_id;  --给age起个别名方便使用
```

定义sql映射：

```java
<select id="findAll" resultType="me.feathers.pojo.WifeAndHusband">
  SELECT
  w.wife_name,
  w.age wife_age,
  hus.*
  FROM husband hus
  JOIN wife w ON w.hus_id = HUS.hus_id
</select>
/**使用resultType，定义夫妻信息类，此po类中包括了丈夫信息和妻子信息**/

/**Husband类**/
public class Husband {
    private Long hus_id;
    private String hus_name;
    private int age;
    private Date birthday;
	// setter / getter
}
/**夫妻类继承husband类**/
public class WifeAndHusband extends Husband {
    private String wife_name;
    private int age;
	// setter / getter
}
```

这样就将所需要的信息封装到实体类中了。

测试类：

```java
 @Test
public void findAll() {
  SqlSession session = factory.openSession();
  //获取mapper接口的代理对象l
  HusbandMapper husbandMapper = session.getMapper(HusbandMapper.class);
  //调用代理对象方法
  List<WifeAndHusband> all = husbandMapper.findAll();
  all.forEach(System.out :: println);
}
```

> 这种方式在开发中最为常用，不仅可以处理这种一对一的表关系，根据订单查用户也是属于一对一的查询，此种方式也是试用的。

#### 方式二

一对一手动映射，定义一个resultMap，用于映射一对一查询结果。

sql语句仍然不变：`select w.wife_name, w.age wife_age, hus.* from husband hus join wife w on w.hus_id = HUS.hus_id;`

```java
/**定义husband 和 wife**/

/**Husband类**/
public class Husband {
    private Long hus_id;
    private String hus_name;
    private int age;
    private Date birthday;
	// setter / getter
}

/**Wife类**/
public class Wife {
   private Long wife_id;
   private String wife_name;
   private int age;
   private Date birthday;
   private Husband husband; // 在wife中维护一个husband对象
   // setter / getter
}
<!-- 定义resultMap -->
<resultMap id="wifeAndHus" type="me.feathers.pojo.Wife">
  <id property="wife_id" column="wife_id"/>
  <result property="age" column="age"/>
  <result property="wife_name" column="wife_name"/>
  <result property="birthday" column="birthday"/>
  <!-- 一对一关联映射 -->
  <association property="husband" javaType="me.feathers.pojo.Husband">
    <id property="hus_id" column="hus_id"/>
    <result property="age" column="age"/>
    <result property="hus_name" column="hus_name"/>
    <result property="birthday" column="birthday"/>
  </association>
</resultMap>

<select id="findAll1" resultMap="wifeAndHus">
  SELECT
  w.wife_name,
  w.age wife_age,
  hus.*
  FROM husband hus
  JOIN wife w ON w.hus_id = HUS.hus_id
</select>
```

> association：表示进行关联查询单条记录
>
> property：表示将关联查询数据库column列的值放入到property属性中
>
> javaType：表示关联查询的结果类型析

### 一对多级联查询

一对多关联映射同一对一关联映射的方式二相同，试用自定义的ResultMap

```xml
<!-- 一对多关联映射 这里会映射为orders集合  -->
<collection property="orders" ofType="cn.itheima.po.Orders"> 
  <id property="id" column="oid"/>	
  <!--用户id已经在user对象中存在，此处可以不设置-->
  <!-- <result property="userId" column="id"/> -->
  <result property="number" column="number"/>
  <result property="createtime" column="createtime"/>
  <result property="note" column="note"/>
</collection>
```

## 缓存

Mybatis的缓存是针对SQL查询结果的缓存，如果执行了两个完全相同的SQL（同一个SQL，同样的SQL参数），在有缓存的情况下，将不会再执行一次SQL，而是直接使用缓存的结果。

### 一级缓存

**一级缓存是是SqlSession级别的缓存，也就是说，如果Mapper处于同一个namespace下，并且处于同一个SqlSession下，缓存才会生效。**

在一级缓存下，所有的缓存数据存储到SqlSession对象中。

![image-20220307193905552](/files/NCeKNhv4JoFHvjLWK5w5)

```java
UserMapper userMapper = session.getMapper(UserMapper.class);
User user = userMapper.findById(123); // 打印了sql日志

UserMapper userMapper2 = session.getMapper(UserMapper.class);
User user2 = userMapper2.findById(123); // 没有打印sql日志，说明sql只执行了一次

System.out.println(userMapper == userMapper2); // false 
System.out.println(user == user2); // true 
```

**一级缓存的原理：**

1. 实际缓存结构是一个map，放置在SqlSession中
2. 当有一个SQL需要缓存时，将会按照`mapperID + offset + limit + sql + sql所有参数`的形式生成key
3. 使用这个key存储到map中，其值为查询结果
4. 当同一个SqlSession再次发出相同的SQL，就从缓存中取出数据。
5. 如果两次相同的操作中存在`commit`操作（包括增、删、改），SqlSession的一级缓存区域将会被全部清空。下次再去缓存中查询不到，将会从数据库查询。

### 二级缓存

\*\*二级缓存是以namespace为标记的缓存，可以是由一个SQLSessionFactory创建的SqlSession之间共享的缓存。\*\*默认不开启。

> 二级缓存是Mapper级别的

在二级缓存下，每个namespace都会生成一个Cache对象，独立于SqlSession，并与所有的SqlSession共享。

![image-20220307194041014](/files/Inl4DY7aM5jJlJus3X2g)

开启二级缓存（总开关）：

```xml
<settings>
  <setting name="cacheEnabled" value="false"/>
  <setting name="defaultStatementTimeout" value="5"/>
  <setting name="mapUnderscoreToCamelCase" value="true"/>
  <setting name="useGeneratedKeys" value="true"/>
</settings>
```

分开关，在要开启二级缓存的mapper中添加如下标签：

```xml
<mapper namespace="xxx">
	<cache />
</mapper>
```

二级缓存不一定使用内存存储，且查询结果类必须实现`Serializable`接口。

**二级缓存的原理：**

1. MyBatis的二级缓存是通过CacheExecutor实现的，CacheExecutor是Executor的代理对象
2. 所有的查询操作，在CacheExecutor中都会匹配缓存中是否存在，不存在则查询数据库
3. 每一个Mapper都会生成对应的一个MapperCache对象，用于缓存数据，他也是一个Map结构
4. 不同的Sql在map中使用如下格式的key存储：`MapperId + offset + limit + sql + sql所有参数`

### 三级缓存

一级缓存与二级缓存都是Mybatis自带的缓存机制，解决不了在分布式环境下的缓存问题。所以可以使用三级缓存，三级缓存一般使用第三方缓存中间件（Memcached、Redis、ehCache）。

比如，使用ehcache作为缓存机制，就需要引入第三方jar `mybatis-ehcache`。

## Spring整合MyBatis

1. `SqlSessionFactory`对象应该放到spring容器中作为单例存在。
2. 传统dao的开发方式中，应该从spring容器中获得`sqlsession`对象。
3. Mapper代理形式中，应该从spring容器中直接获得`mapper`的代理对象。
4. 数据库的连接以及数据库连接池事务管理都交给spring容器来完成。

```xml
<!--sqlMapConfig.xml-->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<typeAliases>
		<package name="cn.itcast.mybatis.pojo"/>
	</typeAliases>
	<mappers>
		<mapper resource="sqlmap/User.xml"/>
	</mappers>
</configuration>
<!-- applicationContext.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd 			   http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
	http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">

	<!-- 加载配置文件 -->
	<context:property-placeholder location="classpath:db.properties" />
	<!-- 数据库连接池 -->
	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
		destroy-method="close">
		<property name="driverClassName" value="${jdbc.driver}" />
		<property name="url" value="${jdbc.url}" />
		<property name="username" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
		<property name="maxActive" value="10" />
		<property name="maxIdle" value="5" />
	</bean>
	<!-- mapper配置 -->
	<!-- 让spring管理sqlsessionfactory 使用mybatis和spring整合包中的 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<!-- 数据库连接池 -->
		<property name="dataSource" ref="dataSource" />
		<!-- 加载mybatis的全局配置文件 -->
		<property name="configLocation" value="classpath:mybatis/SqlMapConfig.xml" />
	</bean>
</beans>
```

> db.properties 省略

参考模块代码：`spring-mybatis`

## 逆向工程

### mybatis-generator

Mybatis需要程序员手动编写SQL，表过多时，会出现大量的重复工作，所以，mybatis提供了逆向工程。可以根据数据表生成 Mapper接口，SqlMapper映射文件，实体类等。

**Maven插件方式具体操作：**

1. 添加maven依赖mybatis-generator-core
2. 依赖maven插件mybatis-generator-maven-plugin
3. 使用 idea mybatis plugins 插件 new 一个Mybatis GeneratorFile
4. 配置这个配置文件
5. 执行maven插件即可`mybatis-generator:generate`

### mybatis-generator-gui

可视化界面generator： [https://github.com/zouzg/mybatis-generator-gui.git](https://gitee.com/link?target=https%3A%2F%2Fgithub.com%2Fzouzg%2Fmybatis-generator-gui.git)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yangsx95.gitbook.io/notes/programming-language/java/orm/mybatis.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
