MyBatis 介绍

MyBatis 是一款优秀的持久层框架,用于简化 JDBC 开发。

持久层:

  • 是负责将数据到保存到数据库的那一层代码。即,操作数据库的Java代码为持久层。

    而Mybatis就是对JDBC代码进行了封装。

  • 持久层是JavaEE三层架构中的一层。

    JavaEE三层架构:表现层、业务层、持久层。

框架:

  • 框架就是一个半成品软件,是一套可重用的、通用的、软件基础代码模型。
  • 使用框架的好处:在框架的基础之上构建软件编写更加高效、规范、通用、可扩展。

使用JDBC存在以下问题:

  • 硬编码

    手动注册驱动、获取连接、SQL语句等。

  • 操作繁琐

    手动设置参数、封装结果集等。

使用Mybatis:

  • 硬编码可以配置到配置文件。
  • 操作繁琐的地方Mybatis都自动完成。

配置 Mybatis

使用Maven导入Mybatis。

  1. pom.xml 配置文件中导入坐标:

    <dependencies>
        <!--mybatis 依赖-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.5</version>
        </dependency>
    
        <!--mysql 驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
    </dependencies>
    
  2. 编写 mybatis-config.xml 文件:

    在模块下的 resources 目录下创建Mybatis的配置文件 mybatis-config.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>
      <!-- 需要注意配置标签的前后顺序(按照MyBatis官方文档目录中的顺序去定义) -->
      <!-- 别名或包扫描 -->
      <typeAliases>
        <!-- name属性的值是实体类所在包 -->
        <!-- 包扫描后,别名默认为类名(不区分大小写) -->
        <!-- 使用别名可以简化映射配置文件中 resultType 属性值的编写 -->
        <package name="package.name.pojo"/>
      </typeAliases>
    
      <!--
        environments:配置数据库的连接环境信息,
          可以配置多个environment信息,
          通过对应的default属性切换不同的environment
      -->
      <environments default="development">
        <!-- 可以配置多个<environment> -->
        <!-- 使用 id 给每段环境起名 -->
        <!-- 在 <environments> 中使用 default='environment-id' 来指定使用哪儿段配置 -->
        <environment id="development">
          <transactionManager type="JDBC"/>
          <dataSource type="POOLED">
            <!-- 
              数据库连接信息
              注意url、username和password
            -->
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql:///mybatis1?useSSL=false"/>
            <property name="username" value="root"/>
            <property name="password" value="1234"/>
          </dataSource>
        </environment>
    
        <environment id="test">
          <transactionManager type="JDBC"/>
          <dataSource type="POOLED">
            <!-- 数据库连接信息 -->
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql:///mybatis2?useSSL=false"/>
            <property name="username" value="root"/>
            <property name="password" value="1234"/>
          </dataSource>
        </environment>
      </environments>
      <mappers>
        <!-- 加载映射文件 -->
        <!-- ...... -->
        <!-- 在下节中讲解 -->
      </mappers>
    </configuration>
    

更多MyBatis配置请查看官方文档:MyBatis文档——配置


使用 Mybatis

  • 编写 Mapper

    java 目录下创建与 pojo 对应的 Mapper接口 ClassNameMapper.java

    public interface ClassNameMapper {
        // 方法
    }
    
  • 编写 SQL 映射文件

    在模块的 resources 目录下创建与 pojo 对应的映射配置文件 ClassNameMapper.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">
    <!-- namespace:名称空间 -->
    <mapper namespace="package.name.mapper">
      <!-- 相应的SQL语句(XML映射) -->
    </mapper>
    
  • 在Myb atis的配置文件 mybatis-config.xml 中加载映射文件:

    1. 直接加载映射文件:
    <mappers>
      <mapper resource="com/abc/mapper/ClassNameMapper.xml"/>
    </mappers>
    

    使用这种方式加载映射文件,多个映射文件需要定义多个 <mapper>,过于繁琐。

    1. Mapper代理方式(推荐):

    如果 Mapper接口名称和SQL映射文件名称相同,并在同一目录下,则可以使用包扫描的方式简化SQL映射文件的加载。

    <mappers>
      <package name="com.abc.mapper"/>
    </mappers>
    

注意:使用Mapper代理方式加载映射文件,Mapper接口与其对应的配置文件,它们的路径(包名)要相同。

即,在java目录下的 Mapper接口的包名,要和映射配置文件在 resources下的路径要一致。

Mapper接口的包名为 com.abc.mapper,那么其映射配置文件的路径则为 com/abc/mapper

了解 Maven项目结构,请查看:Maven标准化项目结构

实例

在 Mapper 接口中定义方法,方法名就是SQL映射文件中SQL语句的id,并保持参数类型和返回值类型一致。

例如,为 User对象定义 selectAll()selectById()方法:

  • 创建 User表:

    CREATE DATABASE mybatis;
    USE mybatis;
    
    DROP table IF EXISTS tb_user;
    
    CREATE table tb_user (
        id INT PRIMARY KEY AUTO_INCREMENT,
        username VARCHAR(20),
        password VARCHAR(20),
        gender CHAR(1),
        addr VARCHAR(30)
    );
    
    INSERT INTO tb_user VALUES (NULL, 'zhangsan', '123', '男', '北京');
    INSERT INTO tb_user VALUES (NULL, '李四', '234', '女', '天津');
    INSERT INTO tb_user VALUES (NULL, '王五', '11', '男', '西安');
    
  • pojo包下定义 User实体类:

    package com.linner.pojo;
    
    public class User {
        private Integer id;
        private String username;
        private String password;
        private String gender;
        private String addr;
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public String getGender() {
            return gender;
        }
    
        public void setGender(String gender) {
            this.gender = gender;
        }
    
        public String getAddr() {
            return addr;
        }
    
        public void setAddr(String addr) {
            this.addr = addr;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", username='" + username + '\'' +
                    ", password='" + password + '\'' +
                    ", gender='" + gender + '\'' +
                    ", addr='" + addr + '\'' +
                    '}';
        }
    }
    
  • mapper包下定义 UserMapper:

    package com.linner.pojo;
    
    import com.linner.pojo.User;
    import org.apache.ibatis.annotations.Select;
    
    import java.util.List;
    
    public interface UserMapper {
        List<User> selectAll();
        User selectById(int id);
        // 更多操作接口
    }
    
  • resources目录下创建与 UserMapper包名对应的路径,并添加 UserMapper.xml映射配置文件:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.linner.mapper.UserMapper">
        <!-- 
          select语句使用<select>
            id          为对应Mapper类中的方法名
            resultType  为对应的实体类, 使用了<typeAliases>包扫描, 省略了包名
         -->
        <select id="selectAll" resultType="user">
            SELECT *
            FROM tb_user;
        </select>
    
        <select id="selectById" resultType="user">
            SELECT *
            FROM tb_user
            WHERE id = #{id};
        </select>
    
        <!-- 更多XML映射 -->
    </mapper>
    
  • resources目录下,配置 mybatis-config.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>
        <!-- 别名或包扫描 -->
        <!-- 使用别名可以简化映射配置文件中 resultType 属性值的编写 -->
        <typeAliases>
            <package name="com.linner.pojo"/>
        </typeAliases>
        <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:///mybatis?useSSL=false&useServerPerpStmts=true&characterEncoding=UTF-8"/>
                    <property name="username" value="root"/>
                    <property name="password" value="1234"/>
                </dataSource>
            </environment>
        </environments>
        <mappers>
            <!-- Mapper代理方式-->
            <package name="com.linner.mapper"/>
        </mappers>
    </configuration>
    
  • 编写测试类:

    package com.linner.test;
    
    import com.linner.mapper.UserMapper;
    import com.linner.pojo.User;
    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.junit.Test;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.List;
    
    public class UserMapperTest {
    
        @Test
        public void testSelectAll() throws IOException {
            // 1. 加载mybatis的核心配置文件,获取SqlSessionFactory
            String resource = "./mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    
            // 2. 获取SqlSession对象,用它来执行SQL
            SqlSession sqlSession = sqlSessionFactory.openSession();
    
            // 3. 执行sql语句
            // 3.1 方式一:
            // List<User> users = sqlSession.selectList("com.linner.mapper.UserMapper.selectAll");
            // 3.2 方式二:
            // 3.2.1 获取UserMapper接口的代理对象
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            // 3.2.2 执行对应Mapper对象的方法
            List<User> users = userMapper.selectAll();
    
            // 4. 处理数据(模拟)
            System.out.println(users);
    
            // 5. 释放资源(仅需释放SqlSession对象)
            sqlSession.close();
        }
    
        @Test
        public void testSelectById() throws IOException {
            // 模拟接收参数
            int id = 1;
    
            // 1. 获取sqlSessionFactory
            String resource = "./mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    
            // 2. 获取SqlSession对象
            SqlSession sqlSession = sqlSessionFactory.openSession();
    
            // 3. 获取Mapper接口的代理对象
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
            // 4. 执行方法
            User user = userMapper.selectById(id);
            // 5. 处理数据(模拟)
            System.out.println(user);
    
            // 6. 释放资源
            sqlSession.close();
        }
    }
    

XML 映射器

通过在 XxxMapper.xml 中使用元素来定义各种CRUD操作。

常用的元素如下(按照定义的顺序给出):

  • <sql> – 可被其它语句引用的可重用语句块。
  • <insert> – 映射插入语句。
  • <update> – 映射更新语句。
  • <delete> – 映射删除语句。
  • <select> – 映射查询语句。

Select

查询所有数据

xml 配置文件:

<select id="selectAll" resultType="user">
    SELECT *
    FROM tb_user
</select>

Mapper 接口方法:

List<User> selectAll();
  • id:相应 Mapper 接口中的方法名。
  • resultType:相应 Mapper 接口中的返回值类型(可自动封装为对象,不区分大小写)。
  • Mapper 接口返回值:返回一个List<User>。这个返回值可以是UserList<User>ArrayList<User>等,MyBatis会根据返回值自动封装。

根据id字段查询

xml 配置文件:

<select id="selectById" resultType="user">
    SELECT * FROM tb_user WHERE id = #{id};
</select>

Mapper 接口方法:

User selectById(int id);

<select>中,#{id}为相应 Mapper 接口中的参数 id#{id}被称为参数占位符,相当于JDBC中的?占位符。

多条件动态查询

xml 配置文件:

<select id="selectByCondition" resultType="user">
    SELECT *
    FROM tb_user
    <where>
        <if test="username != null and username != '' ">
            username = #{username}
        </if>
        <if test="password != null and password != '' ">
            AND password = #{password}
        </if>
        <if test="gender != null and gender != '' ">
            AND gender = #{gender}
        </if>
        <if test="addr != null and addr != '' ">
            AND addr = #{addr}
        </if>
    </where>
</select>

多条件查询使用了动态SQL<where><if>。如果仅需查询某部分字段,仅仅使用SQL语句在实现上有难度。而Mybatis就提供了动态SQL方便了我们的实现。

  • <if>:能根据User对象的值来决定是否在SQL语句中加入其包含的语句。

  • <where>:只会在子元素返回任何内容的情况下才插入WHERE子句。

    而且,若子句的开头为ANDOR<where>元素也会将它们去除。

单条件动态查询

xml 配置文件:

<select id="selectByConditionSingle" resultMap="brandResultMap">
    SELECT *
    FROM tb_brand
    <where><!--使用where标签确保不会出错-->
        <choose><!--相当于switch-->
            <when test="status != null"><!--相当于case-->
                status = #{status}
            </when>
            <when test="companyName != null and companyName != '' "><!--相当于case-->
                company_name like #{companyName}
            </when>
            <when test="brandName != null and brandName != '' "><!--相当于case-->
                brand_name like #{brandName}
            </when>
            <!--没有条件输入很可能会报错,使用<otherwise>保底-->
            <!--如果没有使用<where>,则必须使用<otherwise>-->
            <!--<otherwise>
                1 = 1
            </otherwise>-->
        </choose>
    </where>
</select>

单条件动态查询使用了<where><choose><when><otherwise>,它们都是MyBatis提供的动态SQL元素。

<choose><when>(必须)和<otherwise>元素配合使用。它会根据子元素<when>test属性来判断要选择哪个<when><otherwise>元素。

如果<choose>外没有被<where>包裹则必须使用<otherwise>来包裹一个永真的条件SQL语句,以确保SQL语句不会出错。如果被<where>包裹了,<where>会根据需要判断是否要添加WHERE子句。

<choose><when><otherwise>switch语句的作用十分相似。

Insert

xml 配置文件:

<insert id="add" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO tb_user (username, password, ordered, gender,
                          addr)
    VALUES (#{username}, #{password}, #{ordered}, #{gender},
            #{addr});
</insert>

Mapper 接口方法:

boolean add(User user);

接口方法直接传入对象即可,对象成员要与 VALUES 子句中的参数一一对应(参数符号中的名称要与对象的成员名称相同)。

如果XML映射中只有一个参数,那么这个参数的名称不必与接口的参数名称相同。

  • id:含义与 <insert> 的含义相同,为相应 Mapper 接口中的方法名(以下 id均为此含义,省略)。

  • useGeneratedKeys

    • 值为 true 时,Mybatis 会使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(如自增的主键)。
    • 默认为 false
  • keyPropertygetGeneratedKeys 获取到的主键值所要赋予的对象成员名。MyBatis 会使用 getGeneratedKeys 的返回值来设置它的值。

    如,keyProperty="id"——直接将获取到的主键值赋值给 user.id

    如果生成列不止一个,可以用逗号分隔多个属性名称。

  • Mapper 接口返回值:boolean,插入成功返回 true,插入失败返回 false

    返回值也可以选择忽略,将add()接口的返回值设置为void即可。

Update

xml 配置文件:

<update id="update">
    UPDATE tb_user
    <set>
        <if test="username != null and username != '' ">
            username = #{username},
        </if>
        <if test="password != null and password != '' ">
            password = #{password},
        </if>
        <if test="ordered != null">
            ordered = #{ordered},
        </if>
        <if test="gender != null and (gender == '男' or gender == '女')">
            gender = #{gender},
        </if>
        <if test="addr != null and addr != ''">
            addr = #{addr}
        </if>
    </set>
    WHERE id = #{id};
</update>

Mapper 接口方法:

int update(User user);

<update> 中有一些用 <set> 包裹起来的 <if>。这是因为需要使用 <set> 动态包含需要更新的列,忽略其它不更新的列。从而能根据User对象的值来决定要更新哪些数据,并且动态地改变SQL语句。

Update的Mapper接口返回值是int类型,返回更新的行数。同样可以将接口的返回值设置为void来忽略它。

Delete

删除单行数据

xml 配置文件:

<delete id="deleteById">
    DELETE
    FROM tb_user
    WHERE id = #{id};
</delete>

Mapper 接口方法:

int deleteById(int id);

删除多行数据

xml 配置文件:

<delete id="deleteByIds">
    DELETE
    FROM tb_brand
    WHERE id
    IN
    <!--
        <foreach>参数:
            - item: 代表数组参数中的每个元素
            - separator: 分隔符
            - open: 插入开始符
            - close: 插入结束符
    -->
    <foreach collection="ids" item="id" separator="," open="(" close=")">
        #{id}
    </foreach>
</delete>

Mapper 接口方法:

int deleteByIds(@Param("ids") int[] ids);

Mybatis提供了 <foreach> 标签遍历数组,拼接SQL语句。<foreach> 同样也是动态SQL

更多XML映射器请查看官方文档:MyBatis文档——XML映射器

结果映射

resultMap

假设SQL表中的字段与实体类的成员变量名无法一一对应,会导致SQL语句的传参出现问题。例如:

CREATE table tb_user (
    user_id INT PRIMARY KEY AUTO_INCREMENT,
    user_name VARCHAR(20),
    user_password VARCHAR(20),
    user_gender CHAR(1),
    user_addr VARCHAR(30)
);
public class User {
    private Integer id;
    private String username;
    private String password;
    private String gender;
    private String addr;

    // setter 和 getter
    // ...
}

那么可以使用<resultMap>元素来对表字段和成员名做一个映射(结果映射):

<!--
    <resultMap>:
        - id: 唯一标识
        - type: 映射的类型,支持别名
-->
<resultMap id="userResultMap" type="user">
    <!--
        两种标签:
            - <id>: 完成主键字段的映射
                - column: 表的列名
                - property: 实体类的属性名
            - <result>: 完成一般字段的映射
                - column: 表的列名
                - property: 实体类的属性名
    -->
    <id column="user_id" property="id">
    <result column="user_name" property="userame"/>
    <result column="user_password" property="password"/>
    <result column="user_gender" property="gender"/>
    <result column="user_addr" property="addr"/>
</resultMap>

<select id="selectAll" resultMap="userResultMap">
    SELECT *
    FROM tb_user
</select>

要使用结果映射,需要把<select>中的resultType属性替换为resultMap,并且其属性值为<resultMap>id值。

自动映射

当自动映射查询结果时,MyBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性(忽略大小写)。

接上节例子,使用SQL语句的 AS 来实现:

<select id="selectAll" resultType="user">
    SELECT 
        user_id AS "id",
        user_name AS "username",
        user_password AS "password",
        user_gender AS gender,
        user_addr AS "addr"
    FROM tb_user
</select>
  • 通常数据库列使用大写字母组成的单词命名,单词间用下划线分隔;而 Java 属性一般遵循驼峰命名法约定。为了在这两种命名方式之间启用自动映射,需要将 mapUnderscoreToCamelCase 设置为 true

    <!-- 配置mybatis自动转换为驼峰式命名 -->
      <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    
  • <resultMap>和自动映射可以混用。

参数

MyBatis有两种参数:

  • #{p} —— 会自动转义。
  • ${p} —— 不会自动转义。

官方文档:MyBatis——XML映射器参数

MyBatis 参数封装:

  • 单个参数:

    1. POJO类型: 直接使用,保证 属性名参数占位符名称 一致

    2. Map类型: 直接使用,保证 键名参数占位符名称 一致

    3. Collection: 封装为Map集合

      相当于:

      map.put("arg0", collection集合)
      map.put("collection", collection集合)
      
      • 使用@Param注解,替换Map集合中默认的arg键名
    4. List: 封装为Map集合

      相当于:

      map.put("arg0", list集合)
      map.put("collection", list集合)
      map.put("list", list集合)
      
      • 使用@Param注解,替换Map集合中默认的arg键名
    5. Array: 封装为Map集合

      相当于:

      map.put("arg0", 数组)
      map.put("array", 数组)
      
      • 使用@Param注解,替换Map集合中默认的arg键名
    6. 其他类型: 直接使用,且占位符名称和参数名称可以不相同

  • 多个参数: 封装为Map集合

    • 每个参数有两个键:

      相当于:

      map.put("arg0", 参数值1)
      map.put("param1", 参数值1)
      map.put("arg1", 参数值2)
      map.put("param2", 参数值2)
      
    • 使用@Param注解,替换Map集合中默认的arg键名:

      相当于:

      @Param("username") 参数类型 参数名
      map.put("username", 参数值1)
      map.put("param1", 参数值1)
      

示例:

public interface UserMapper {
    User selectById(int id);
    List<User> selectByCondition(
            @Param("username") String username,
            @Param("password") String password);
}

注解实现CRUD

对于简单的SQL语句来说,使用注解开发会比配置文件开发更加方便。

@Select(value = "SELECT * FROM tb_user WHERE id = #{id}")
User selectById(int id);

注意:注解是用来替换映射配置文件方式配置的,所以使用了注解,就不需要再映射配置文件中书写对应的 statement

Mybatis 针对 CURD 操作都提供了对应的注解:

  • 查询 :@Select
  • 添加 :@Insert
  • 修改 :@Update
  • 删除 :@Delete

注解适合用于完成简单功能,而使用配置文件来完成复杂功能。如果使用注解来完成动态SQL之类的复杂功能,就需要使用到MyBatis提供的SQL构建器来完成。详情请阅读官方SQL构建器文档:MyBatis文档——SQL语句构建器


动态SQL

动态 SQL 是 MyBatis 的强大特性之一。

MyBatis提供的动态SQL元素有:

  • <if>
  • <choose> (<when>, <otherwise>)
  • <trim> (<where>, <set>)
  • <foreach>

更多与动态SQL请查看官方文档:MyBatis文档——动态SQL


SqlSessionFactory工具类抽取

MyBatis重复代码会造成一些问题:

  • 不利于后期的维护。
  • SqlSessionFactory工厂类进行重复创建。

对于Mybatis的基础操作出现的重复代码,可以使用一个静态代码块来自动加载:

public class SqlSessionFactoryUtils {

    private static SqlSessionFactory sqlSessionFactory;

    static {
        // 静态代码块会随着类的加载而自动执行,且只执行一次
        try {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public static SqlSessionFactory getSqlSessionFactory(){
        return sqlSessionFactory;
    }
}

工具类抽取以后,以后在对Mybatis的SqlSession进行操作的时候,就可以直接使用:

SqlSessionFactory sf = SqlSessionFactoryUtils.getSqlSessionFactory();