MyBatis 简介

MyBatis 官方文档: https://mybatis.org/mybatis-3/zh/index.html

原始 JDBC 操作的分析

原始 JDBC 开发存在的问题如下:

  1. 数据库连接创建、释放频繁,造成系统资源浪费从而影响系统性能
  2. SQL 语句在代码中硬编码,造成代码不易维护,实际应用SQL变化的可能较大,SQL变动需要修改Java代码
  3. 查询操作时,需要手动将结果集中的数据封装到实体中,插入操作时需要手动将实体的数据设置到SQL语句的占位符位置

应对上述问题给出的解决方案:

  1. 使用数据库连接池初始化连接资源
  2. 将SQL语句抽取到XML配置文件中
  3. 使用反射、内省等底层技术,自动将实体与表进行属性与字段的映射

什么是 MyBatis

  • MyBatis 是一个优秀的、基于 Java 的持久层框架,它内部封装了 JDBC ,使开发者只需要关注SQL语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁琐的过程。
  • MyBatis 通过 xml 或注解的方式将要执行的各种 statement 配置起来,并通过 Java 对象和 statement 中 SQL 的动态参数进行映射,生成最终执行的 SQL 语句。
  • 最后 MyBatis 框架执行 SQL 并将结果集映射为 Java 对象并返回。采用 ORM 思想解决了实体和数据库映射的问题,对 JDBC 进行了封装,屏蔽了 JDBC API 底层访问细节,使我们不用与 JDBC API 打交道,就可以完成对数据库的持久化操作。

MyBatis 快速入门

MyBatis 开发步骤

  1. 添加 MyBatis 的坐标
  2. 创建 User 数据表
  3. 编写 User 实体类
  4. 编写映射文件 UserMapper.xml
  5. 编写核心文件 SqlMapConfig.xml
  6. 编写测试类

添加坐标

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>

创建数据表

创建实体类

package com.vsneko.domain;

public class User {
private int id;
private String name;
private int age;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}

编写映射文件

resources 目录下创建配置文件 com/vsneko/mapper/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="UserMapper">

<!-- 查询操作 -->
<select id="selectAll" resultType="com.vsneko.domain.User">
select * from user
</select>

</mapper>

编写核心配置文件

resources 目录下创建配置文件 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>
<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://localhost:3306/mybatis?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="1025"/>
</dataSource>
</environment>
</environments>

<mappers>
<mapper resource="com/vsneko/mapper/UserMapper.xml"/>
</mappers>
</configuration>

编写测试代码

@Test
public void test1() throws IOException {
// 获取核心配置文件
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");

// 获取 session 工厂对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

// 获取 session 回话对象
SqlSession sqlSession = sqlSessionFactory.openSession();

// 执行操作
List<User> userList = sqlSession.selectList("UserMapper.selectAll");

// 显示数据
System.out.println(userList);
// [User{id=1, name='Zhangsan', age=18}, User{id=2, name='Lisi', age=19}]

// 释放资源
sqlSession.close();
}

MyBatis 的数据库基本操作

MyBatis 事务默认是不提交的,执行更新操作之后要提交事务。(或者在调用 openSession() 方法时传入 true)

sqlSession.commit();

查询操作

<!-- 查询操作 -->
<select id="selectAll" resultType="com.vsneko.domain.User">
select * from user
</select>

插入操作

<!-- 插入操作 -->
<insert id="insertUser" parameterType="com.vsneko.domain.User">
insert into user values(#{id}, #{name}, #{age})
</insert>

修改操作

<!-- 修改操作 -->
<update id="updateUser" parameterType="com.vsneko.domain.User">
update user set name=#{name} and age=#{age} where id=#{id}
</update>

删除操作

如果传入的参数只有一个,那么在表达式里面写任意文本都可以,但要注意可读性。

<!-- 删除操作 -->
<delete id="deleteUser" parameterType="java.lang.Integer">
delete from user where id=#{id}
</delete>

MyBatis 核心配置文件

层级关系

  • configuration 配置

    • properties 属性

    • settings 设置

    • typeAliases 类型别名

    • typeHandlers 类型处理器

    • objectFactory 对象工厂

    • plugins 插件

    • environments 环境

      • environment 环境变量
        • transactionManager 事务管理器
        • dataSource 数据源
    • databaseIdProvider 数据库厂商标识

    • mappers 映射器

environments 标签

<!-- 默认环境为 development -->
<environments default="development">
<environment id="development">
<!-- 配置事务管理类型为 JDBC -->
<transactionManager type="JDBC"/>
<!-- 配置数据源类型为连接池 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="1025"/>
</dataSource>
</environment>
</environments>

transactionManager

事务管理器 transactionManager 类型有两种

  • JDBC: 这个配置就是直接使用了 JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作用域。
  • MANAGED:这个配置几乎没做什么,它从来不提交或者回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。默认情况下它会关闭连接,然而一些容器并不希望这样,因此需要将 closeConnection 属性设置为 false 来阻止它默认的关闭行为。

dataSource

数据源 dataSource 类型有三种

  • UNPOOLED:这个数据源的实现只是每次被请求时打开和关闭连接
  • POOLED:这种数据源的实现利用 “池” 的概念将 JDBC 连接对象组织起来
  • JNDI:这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中在外部配置数据源,然后放置一个 JNDI 上下文的引用

mappers 标签

<mappers>
<mapper resource="com/vsneko/mapper/UserMapper.xml"/>
</mappers>

mapper

mapper 标签的作用是加载映射的,加载方式有如下几种:

  1. 使用相对于类路径的资源引用
<mapper resource="com/vsneko/mapper/UserMapper.xml" />
  1. 使用完全限定资源定位符(URL)
<mapper url="file:///C:/UserMapper.xml" />
  1. 使用映射器接口实现类的完全限定类名
<mapper class="com.vsneko.builder.UserMapper" />
  1. 将包内的映射器接口实现全部注册为映射器
<package name="com.vsneko.builder" />

properties 标签

实际开发中,习惯将数据源的配置信息单独抽取成一个 properties 文件,该标签可以加载额外配置的 properties 文件。

<!-- 加载外部配置文件 -->
<properties resource="jdbc.properties" />

<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>

typeAliases 标签

类型别名 typeAliases 是为 Java 类型设置一个短的名字。

<!-- 自定义别名 -->
<typeAliases>
<typeAlias type="com.vsneko.domain.User" alias="user" />
</typeAliases>

这样,在编写映射文件的时候,参数类型可以直接写别名。

<!-- 插入操作 -->
<insert id="insertUser" parameterType="user">
insert into user values(#{id}, #{name}, #{age})
</insert>

MyBatis 框架已经为我们设置好了一些常用的类型别名。

数据类型 别名
String string
Long long
Integer int
Double double
Boolean boolean

typeHandlers 标签

无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用到类型处理器,将获取的值以合适的方式转换成 Java 类型。下表描述了一下默认的类型转换器(部分)。

类型处理器 Java 类型 JDBC 类型
BooleanTypeHandler java.lang.Boolean, boolean 数据库兼容的 BOOLEAN
ByteTypeHandler java.lang.Byte, byte 数据库兼容的 NUMERIC 或 BYTE
ShortTypeHandler java.lang.Short, short 数据库兼容的 NUMERIC 或 SHORT INTEGER
IntegerTypeHandler java.lang.Integer, int 数据库兼容的 NUMERIC 或 INTEGER
LongTypeHandler java.lang.Long, long 数据库兼容的 NUMERIC 或 LONG INTEGER

你可以重写类型处理器,或者创建你自己的类型处理器来处理不支持的或非标准的类型。

具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口,

或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler ,然后可以选择性地将它映射到一个 JDBC 类型。

开发步骤:

  1. 定义转换类,继承 BaseTypeHandler
  2. 覆盖 4 个未实现的方法,其中 setNonNullParameter 为 Java 程序设置数据到数据库的回调方法,getNullableResult 为查询时 mysql 的字符串类型转换成 java 的 Type 类型方法。
  3. 在 MyBatis 核心配置文件中进行注册
  4. 测试转换是否正确
package com.vsneko.handle;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;

public class DateTypeHandler extends BaseTypeHandler<Date> {

// 将 Java 类型转换成数据库需要的类型
public void setNonNullParameter(PreparedStatement preparedStatement, int i, Date date, JdbcType jdbcType) throws SQLException {
long time = date.getTime();
preparedStatement.setLong(i, time);
}

// 将数据库中的类型转换成 Java 类型
// String 要转换的字段名称
// ResultSet 查询出的结果集
public Date getNullableResult(ResultSet resultSet, String s) throws SQLException {
long time = resultSet.getLong(s);
return new Date(time);
}

// 将数据库中的类型转换成 Java 类型
public Date getNullableResult(ResultSet resultSet, int i) throws SQLException {
long time = resultSet.getLong(i);
return new Date(time);
}

// 将数据库中的类型转换成 Java 类型
public Date getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
long time = callableStatement.getLong(i);
return new Date(time);
}
}
<!-- 注册自定义类型转换器 -->
<typeHandlers>
<typeHandler handler="com.vsneko.handle.DateTypeHandler" />
</typeHandlers>

plugins 标签

MyBatis 可以使用第三方的插件来对功能进行扩展,分页助手 PageHelper 是将分页的复杂操作进行封装,使用简单的方式即可获得分页的相关数据。

开发步骤:

  1. 导入通用 PageHelper 的坐标
  2. 在 MyBatis 核心配置文件中配置 PageHelper 插件
  3. 测试分页数据获取
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.2</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>1.4</version>
</dependency>
<!-- 配置分页助手插件(5.X版本) -->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="helperDialect" value="mysql"/>
</plugin>
</plugins>
// pageNum, pageSize
PageHelper.startPage(1, 2);

List<User> users = userMapper.selectAll();

// 获取分页相关参数
PageInfo<User> pageInfo = new PageInfo<User>(users);
System.out.println("当前页: " + pageInfo.getPageNum());
System.out.println("每页数量: " + pageInfo.getPageSize());
System.out.println("总数量: " + pageInfo.getTotal());
System.out.println("总页数: " + pageInfo.getPages());
System.out.println("上一页: " + pageInfo.getPrePage());
System.out.println("总页数: " + pageInfo.getNextPage());
System.out.println("是否是第一页: " + pageInfo.isIsFirstPage());
System.out.println("是否是最后页: " + pageInfo.isIsLastPage());

MyBatis 的 API

SqlSessionFactoryBuilder

SqlSessionFactoryBuilder 是 SqlSession 工厂构建器。常用 API为

SqlSessionFactory build(InputStream inputStream)

通过加载 MyBatis 的核心文件的输入流的形式来构建一个 SqlSessionFactory 对象。

// 获取核心配置文件
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
// 获取 session 工厂对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

SqlSessionFactory

SqlSessionFactory 是 SqlSession 工厂对象,它有多个方法来创建 SqlSession 实例,其中常用的有如下两个:

方法 说明
openSession() 会默认开启一个事务,但事务不会自动提交,执行更新操作需要手动提交事务。
openSession(boolean autoCommit) 参数为是否自动提交,如果设置为 true,则不需要手动提交事务。
// 获取 session 回话对象
SqlSession sqlSession = sqlSessionFactory.openSession();

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

MyBatis 代理开发方式

说明

采用 MyBatis 的代理开发方式实现 DAO 层的开发,这种方式是企业开发的主流。

Mapper 接口开发方法只需要程序员编写 Mapper 接口(相当于 DAO 接口),由 MyBatis 框架根据接口定义创建接口的动态代理对象。

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.xml -->
<mapper namespace="com.vsneko.dao.UserMapper">
<!-- 删除操作 -->
<delete id="deleteUser" parameterType="int">
delete from user where id=#{id}
</delete>
</mapper>
package com.vsneko.dao;

// DAO 层接口
public interface UserMapper {
void deleteUser(int id);
}
public static void main(String[] args) throws IOException {

// 获取资源流
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");

// 创建 SqlSession 工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

// 创建 SqlSession 对象
SqlSession sqlSession = sqlSessionFactory.openSession(true);

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

// 操作数据
userMapper.deleteUser(1);
}

MyBatis 动态 SQL 语句

概述

在 MyBatis 的映射文件中,有时候业务逻辑比较复杂,SQL语句是动态变化的,这时候就需要用到 MyBatis 的动态 SQL 语句了。

官方描述:

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。

如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。

动态SQL-if

<select id="selectById" resultType="user" parameterType="user">
select * from user
<where>
<if test="id!=0">
and id=#{id}
</if>
<if test="name!=null">
and name=#{name}
</if>
<if test="age!=0">
and age=#{age}
</if>
</where>
</select>

动态SQL-foreach

<select id="selectByIds" resultType="user" parameterType="list">
select * from user
<where>
<foreach collection="list" open="id in (" close=")" separator="," item="id">
#{id}
</foreach>
</where>
</select>

SQL片段的抽取

这个元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。 参数可以静态地(在加载的时候)确定下来,并且可以在不同的 include 元素中定义不同的参数值。

<sql id="selectUser">select * from user</sql>

<select id="selectByIds" resultType="user" parameterType="list">
<include refid="selectUser" />
<where>
<foreach collection="list" open="id in (" close=")" separator="," item="id">
#{id}
</foreach>
</where>
</select>

MyBatis 多表操作

一对一关系

public class Orders {
private int id;
private Date orderTime;
private double total;
private int uid;

// 当前订单对应的用户
private User user;

// Getting and setting...
}
<resultMap id="orderMap" type="orders">
<!-- 手动指定字段与实体属性的映射关系 -->
<!-- column: 数据库字段名 property: 实体类属性名 -->
<id column="oid" property="id" />
<result column="orderTime" property="orderTime" />
<result column="total" property="total" />
<result column="uid" property="user.id" />
<result column="username" property="user.username" />
<result column="password" property="user.password" />
<result column="birthday" property="user.birthday" />
</resultMap>
<select id="findAll" resultMap="orderMap">
SELECT *,o.id oid FROM orders o, user u WHERE o.uid=u.id
</select>

或者使用 association 标签:

<resultMap id="orderMap" type="orders">
<!-- 手动指定字段与实体属性的映射关系 -->
<!-- column: 数据库字段名 property: 实体类属性名 -->
<id column="oid" property="id" />
<result column="orderTime" property="orderTime" />
<result column="total" property="total" />

<association property="user" javaType="user">
<id column="uid" property="id" />
<result column="username" property="username" />
<result column="password" property="password" />
<result column="birthday" property="birthday" />
</association>
</resultMap>

一对多关系

public class User {
private int id;
private String username;
private String password;
private Date birthday;

// 当前用户的所有订单
private List<Orders> ordersList;

// Getting and setting...
}
<resultMap id="userMap" type="user">
<id column="uid" property="id" />
<result column="username" property="username" />
<result column="password" property="password" />
<result column="birthday" property="birthday" />

<!-- 配置集合信息 -->
<collection property="ordersList" ofType="orders">
<id column="oid" property="id" />
<result column="orderTime" property="orderTime" />
<result column="total" property="total" />
</collection>
</resultMap>
<select id="findAll" resultMap="userMap">
SELECT *, o.id oid FROM user u, orders o WHERE u.id=o.uid
</select>

多对多关系

多对多关系的配置与一对多的配置一样,只是SQL语句会有些区别,多对多关系有一个中间表。具体配置参考一对多的配置。

MyBatis 整合 Spring

在 spring 配置文件中配置 sessionFactory 之后就可以直接在 service 层自动注入了。

需要 mybatis-spring 的依赖。

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.5</version>
</dependency>
<!-- 加载配置文件 -->
<context:property-placeholder location="classpath:jdbc.properties" />

<!-- 配置数据源信息 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>

<!-- 配置 sessionFactory -->
<bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!-- 加载 MyBatis 核心配置文件 -->
<property name="configLocation" value="classpath:SqlMapConfig.xml" />
<!-- 为实体类创建别名 -->
<property name="typeAliasesPackage" value="com.vsneko.domain" />
</bean>

<!-- 扫描 Mapper 所在包 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.vsneko.mapper" />
</bean>

在 service 层中使用:

@Service("userService")
public class UserService {

@Autowired
private UserMapper userMapper;

public List<User> findAll(){
return userMapper.selectAll();
}
}