MyBatis 初步

目前我对 MyBatis 的了解不是很深,停留在企业比较常用的”数据库框架”上,系统性的学习要看官方文档

这篇随笔主要围绕 SpringBoot 中 gradle 环境的搭建来讲,是我从《深入浅出SpringBoot2》中讨的一些知识。

可以跟着文章做一个基础环境的项目。

引入插件

仓库地址:mvnrepository阿里云

因为用的 gradle ,在 build.gradle 中的 dependencies 中加入依赖包:

implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'mysql:mysql-connector-java:8.0.29'

创建数据表和插入数据

终端进入 MySQL 中 xxx 数据库,输入 SQL 语句:

CREATE TABLE t_user(
	id INT(12) NOT NULL AUTO_INCREMENT,
    user_name VARCHAR(60) NOT NULL,
    sex INT(3) NOT NULL DEFAULT 1 CHECK (sex in (1,2)),
    note VARCHAR(256) NULL,
    PRIMARY KEY(id)
);
 INSERT INTO t_user(id,user_name,note)  VALUES(1,"user_name_1","zhangsan");

创建实体类并设置别名

因为类的全限定名很长,所以使用 @Alias(value = “xxx”) 的方式,一般用来和数据表属性相对的类上(实体类)。

数据表与之对应的实体类如下(自行加入 getter 和 setter ):

package mybatis.pojo;

import mybatis.enumeration.SexEnum;
import org.apache.ibatis.type.Alias;

/**
 * @author enrace
 * @Alias MyBatis give the class other name.
 */
@Alias(value = "user")
public class User {

    private Long id = null;

    private String userName = null;

    private String note = null;

    /**
     * The sex is numeration here need use typeHandler to switch.
     */
    private SexEnum sex = null;

    public User() {}

    /** setter 和 getter 方法自行加入即可 **/
}

创建enum 枚举类型和 typeHandler

typeHandler 是 MyBatis 的重要配置之一,用于不同类型的数据进行自定义转换。

我学习了将 Java 中的 enum (枚举类)的实例和数据库的 int 进行转换。

先看看 enum 的使用,enum 的代码如下(自行加入 getter 和 setter ):

package mybatis.enumeration;

/**
 * @author enrace
 */
public enum SexEnum {
    
    MALE(1, "男"),
    FEMALE(2,"女");
    
    private int id;
    
    private String name;
    
    SexEnum(int id, String name) {
        this.id = id;
        this.name = name;
    }
    
    public static SexEnum getEnumById(int id) {
        for (SexEnum sex : SexEnum.values() ) {
            if (sex.getId() == id) {
                return sex;
            }
        }
        return null;
    }

    /** setter 和 getter 方法自行加入即可 **/
}

enum 中有两个类型 MALE 和 FEMALE 分别对应 MySQL 中 int 的 1 和 2。

其中的 getEnumById() 的方法是通过接收 int 返回 enum 的实例。

接着说说 typeHandler,这是通过继承的方式自定以了一个 typeHandler:

package mybatis.typehandler;

import mybatis.enumeration.SexEnum;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;

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

/**
 * @MappendJdbcTypes Declare JdbcType to be an integer.
 * @author enrace
 */
@MappedJdbcTypes(JdbcType.INTEGER)
@MappedTypes(value = SexEnum.class)
public class SexTypeHandler extends BaseTypeHandler<SexEnum> {
    /**
     * Read gender by column name.
     */
    @Override
    public SexEnum getNullableResult(ResultSet rs, String col) throws SQLException {
        int sex = rs.getInt(col);
        if(sex != 1 && sex != 2) {
            return null;
        }
        return SexEnum.getEnumById(sex);
    }

    /**
     * Read gender by subscript.
     * @param rs
     * @param idx
     * @return SexEnum
     * @throws SQLException
     */
    @Override
    public SexEnum getNullableResult(ResultSet rs, int idx) throws SQLException{
        int sex = rs.getInt(idx);
        if (sex != 1 && sex != 2) {
            return null;
        }
        return SexEnum.getEnumById(sex);
    }

    /**
     * Read gender from a stored procedure.
     * @param cs
     * @param idx
     * @return SexEnum
     * @throws SQLException
     */
    @Override
    public SexEnum getNullableResult(CallableStatement cs, int idx) throws SQLException {
        int sex = cs.getInt(idx);
        if (sex != 1 && sex != 2) {
            return null;
        }
        return SexEnum.getEnumById(sex);
    }

    /**
     * Set not null gender parameter.
     * @param ps
     * @param idx
     * @param sex
     * @param jdbcType
     * @throws SQLException
     */
    @Override
    public void setNonNullParameter(PreparedStatement ps, int idx, SexEnum sex, JdbcType jdbcType)
        throws SQLException {
        ps.setInt(idx, sex.getId());
    }

}

这个类的几种方法,主要是通过 enum.getEnumById() 返回一个 enum 类型的结果。

我将 SexTypeHandler 分成简述和细述:

简叙:

网上了解的是: 一个 setxxx 方法,表示向 PreparedStatement 里面设置值。三个 getxxx 方法,一个是根据列名获取值,一个是根据列索引位置获取值,最后一个是存储过程。

细述:

ResultSet 类型我去查了一下,表示数据库结果集的数据表,其中的 getXXX 表示在结果集中检索 XXX 类型。

ResultSet.getInt(col) 方法通过 SQL 子句中指定的列的标签获取 sex 的 int。

ResultSet.getInt(idx) 方法通过第几列的方式从数据表中获取 sex 的 int。

CallableStatement 类型可以返回一个对象或多个对象ResultSet。

网上了解到 CallableStatement 类型用于从Java程序调用存储过程,存储过程是我们在数据库中为某些任务编译的一组语句。 当我们处理具有复杂场景的多个表时,存储过程是有益的,而不是向数据库发送多个查询,我们可以将所需的数据发送到存储过程,并在数据库服务器本身中执行逻辑。

自己不是很明白存储过程,不过 CallableStatement 可以获取到结果,自然能获取到我们需要的 int 类型。

cs.getInt(idx) 方法通过传入的 int 检索指定JDBC INT类型的值。

ps.setInt(idx, sex.getId()) 设置给定的 java int 指定参数值。驱动将一个 SQL INTEGER 值发送到数据库。

定义 MyBatis 操作接口

操作接口(Mapper 接口)使用来帮助数据库和 POJO 映射的。

注意: 仅仅为一个接口,不需要任何实现类。

package mybatis.dao;

import mybatis.pojo.User;
import org.springframework.stereotype.Repository;

/**
 * @author enrace
 */
@Repository
public interface MyBatisUserDao {
    /**
     * Get User.
     * @param id
     * @return User
     */
    public User getUser(Long id);
}

@Repository 用于标注数据访问组件,即 DAO 组件。

除了 操作接口还需要创建映射文件,映射文件的 namespace 是与操作接口对应的。

创建映射文件(小坑)和添加配置

映射文件让 POJO (类) 能够与数据库的数据对应,是 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="mybatis.dao.MyBatisUserDao">
    <select id="getUser" parameterType="long" resultType="user">
        select id, user_name as userName, sex, note from t_user where id = #{id}
    </select>
</mapper>

主要的几个属性:

namespace 指定一个接口,就是需要方法需要执行 sql 的接口。

<select> 标签代表一个查询语句。

id 指代这个 SQL,它与接口是同名的(个人认为是映射)。

parameterType 是说明属性配置为 Long (个人理解为传入参数类型)。

resultType 这里指定返回的类型(记得 @Alias 设置的别名就是 user ,那么到时会返回一个 User 实例)。

然后再去 application.properties 添加如下信息:

# 数据库 url
spring.datasource.url = jdbc:mysql://localhost:3306/xxx
# 数据库用户名
spring.datasource.username = zhangsan
# 数据库密码
spring.datasource.password = passwd123
# 最大等待连接中的数量你,设置 0 没有限制
spring.datasource.tomcat.max-idle = 10
# 最大连接活动数
spring.datasource.tomcat.max-active = 50
# 最大等待毫秒数,单位 ms ,超过时间会出错误信息
spring.datasource.tomcat.max-wait = 10000
# 数据库连接池初始化连接数
spring.datasource.tomcat.initial-size = 5

# 映射文件配置
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
# 扫描别名包,和注解 @Alias 联用
mybatis.type-aliases-package=mybatis.pojo
# 配置 typeHandler 的扫描包
mybatis.type-handlers-package=mybatis.typehandler
#logging.level.root = DEBUG
#logging.level.org.springframework = DEBUG
#logging.level.org.org.mybatis = DEBUG

小坑

映射文件有了,我将它放在了项目 mybatis.mapper 包下,但是后面执行报了以下异常:

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): mybatis.dao.MyBatisUserDao.getUser

分析过程:

classpath 指的文件目录是什么?

classpath 指的是 build/resources (gradle 构建包) 或是 target/classes (maven 构建包)。

结果:

发现没有 mapper 目录和我的映射文件,手动添加可正常执行。

解决方法一

在 resources 资源目录创建 mybatis 目录和 mapper 子目录,将映射文件放入其中,原因是 resources 中的文件 gradle build 的时候会保留下来。这种是资源文件分离的方式。

解决方法二

build.gradle 中添加下面的代码:

sourceSets {
    main {
        resources {
            srcDirs 'src/main/java'
        }
    }
}

这是资源路径设置,添加代码后 gradle build 的时候不会删除 java 目录下的 非 .java 后缀文件。

使用 MapperFactoryBean 装配 MyBatis 接口

上面的 MyBatisUserDao 是一个 Mapper 接口,不可以使用 new 为其 生成对象实例。需要用到两个类,它 们 是 MapperFactoryBean 和 MapperScannerConfigurer 。其中 MapperFactoryBean 针对接口配置,MapperScannerConfigurer 则是扫描装配。书中提到 @MapperScan 可以后面去使用一下,它更为简便也是用来将对应接口扫描装配到 Spring IoC 容器中的。

接下来我们创建一个 Bean 来配置 MyBatisUserDao 接口,在 @SpringBootApplication 注解文件下增加代码:

  @Autowired
    SqlSessionFactory sqlSessionFactory = null;

    /**
     * Define a Mapper interface of MyBatis.
     * @return MapperFactoryBean\<MyBatisUserDao\>
     */
    @Bean
    public MapperFactoryBean<MyBatisUserDao> initMyBatisUserDao() {
        MapperFactoryBean<MyBatisUserDao> bean = new MapperFactoryBean<>();
        bean.setMapperInterface(MyBatisUserDao.class);
        bean.setSqlSessionFactory(sqlSessionFactory);
        return bean;
    }

SqlSessionFactory 是 Spring Boot 自动为我们生成的。

开发服务层

由于不是很难理解,直接上代码。

服务接口类代码,如下:

package mybatis.service;

import mybatis.pojo.User;

/**
 * @author enrace
 */
public interface MyBatisUserService {
    /**
     * Get user object.
     * @param id
     * @return User
     */
    public User getUser(Long id);

}

实现类代码,如下:

package mybatis.service.impl;

import mybatis.dao.MyBatisUserDao;
import mybatis.pojo.User;
import mybatis.service.MyBatisUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author enrace
 */
@Service
public class MyBatisUserServiceImpl implements MyBatisUserService {

    @Autowired
    private MyBatisUserDao myBatisUserDao = null;

    @Override
    public User getUser(Long id) {
        return myBatisUserDao.getUser(id);
    }
}

实现类中,通过 @Autowired 自动装配 MyBatisUserDao 的 Bean ,我们就实现了 getUser() 方法,从而可以获得 User 对象。

创建控制器

有了控制器就可以通过 url 测试结果。

控制器代码,如下:

package mybatis.controller;

import mybatis.pojo.User;
import mybatis.service.MyBatisUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author enrace
 */
@RestController
public class MyBatisController {

    @Autowired
    private MyBatisUserService myBatisUserService = null;

    @RequestMapping("/getUser")
    public User getUser(Long id) {
        return myBatisUserService.getUser(id);
    }

}

@RequestMapping(“/getUser”) 设置请求的映射,通过传入 id 得到用户( JSON )格式。

完成

启动 Spring Boot 项目,访问 localhost:8080/getUser?id = 1

至此,您已经了解到了 MyBatis 基本的执行过程,祝:事事无忧,天天无 BUG。