MyBatis 源码分析 - MyBatis入门

x33g5p2x  于2021-11-11 转载在 其他  
字(17.0k)|赞(0)|评价(0)|浏览(311)
序号内容链接
1MyBatis 源码分析 - MyBatis入门https://thinkwon.blog.csdn.net/article/details/114808852
2MyBatis 源码分析 - 配置文件解析过程https://thinkwon.blog.csdn.net/article/details/114808962
3MyBatis 源码分析 - 映射文件解析过程https://thinkwon.blog.csdn.net/article/details/115423167
4MyBatis 源码分析 - SQL 的执行过程https://thinkwon.blog.csdn.net/article/details/115603376
5MyBatis 源码分析 - 内置数据源https://thinkwon.blog.csdn.net/article/details/116331419
6MyBatis 源码分析 - 缓存原理https://thinkwon.blog.csdn.net/article/details/116809942
7MyBatis 源码分析 - 插件机制https://thinkwon.blog.csdn.net/article/details/116809961

1.本文速览

本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说明和演示。

2.什么是 MyBatis

MyBatis 的前身是 iBatis,其是 Apache 软件基金会下的一个开源项目。2010年该项目从 Apache 基金会迁出,并改名为 MyBatis。同期,iBatis 停止维护。

MyBatis 是一种半自动化的 Java 持久层框架(persistence framework),其通过注解或 XML 的方式将对象和 SQL 关联起来。之所以说它是半自动的,是因为和 Hibernate 等一些可自动生成 SQL 的 ORM(Object Relational Mapping) 框架相比,使用 MyBatis 需要用户自行维护 SQL。维护 SQL 的工作比较繁琐,但也有好处。比如我们可控制 SQL 逻辑,可对其进行优化,以提高效率。

MyBatis 是一个容易上手的持久层框架,使用者通过简单的学习即可掌握其常用特性的用法。这也是 MyBatis 被广泛使用的一个原因。

3.为什么要使用 MyBatis

技术之间通常没有高下之分。从应用场景的角度来说,符合应用场景需求的技术才是合适的选择。那下面我会通过写代码的方式,来比较一下这几种数据库访问技术的优缺点,并会在最后说明 MyBatis 的适用场景。

这里,先把本章所用到的一些公共类和配置贴出来

public class Article {
    private Integer id;
    private String title;
    private String author;
    private String content;
    private Date createTime;
    
    // 省略 getter/setter 和 toString
}

数据库相关配置放在了 jdbc.properties 文件中,详细内容如下:

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/coolblog?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=TRUE
jdbc.username=root
jdbc.password=****

表记录如下:

数据库sql如下:

DROP TABLE IF EXISTS `article`;

CREATE TABLE `article` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `author_id` int(11) NOT NULL,
  `title` varchar(32) DEFAULT NULL,
  `type` tinyint(4) DEFAULT NULL,
  `content` text,
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `article` WRITE;
/*!40000 ALTER TABLE `article` DISABLE KEYS */;

INSERT INTO `article` (`id`, `author_id`, `title`, `type`, `content`, `create_time`)
VALUES
	(1,1,'MyBatis 源码分析系列文章导读',8,'MyBatis 源码分析系列文章导读','2018-07-15 15:30:09'),
	(2,2,'HashMap 源码详细分析(JDK1.8)',1,'HashMap 源码详细分析(JDK1.8)','2018-01-18 15:29:13'),
	(3,1,'Java CAS 原理分析',1,'Java CAS 原理分析','2018-05-15 15:28:33'),
	(4,1,'Spring IOC 容器源码分析 - 获取单例 bean',4,'Spring IOC 容器源码分析 - 获取单例 bean','2018-06-01 00:00:00'),
	(5,1,'Spring IOC 容器源码分析 - 循环依赖的解决办法',4,'Spring IOC 容器源码分析 - 循环依赖的解决办法','2018-06-08 00:00:00'),
	(6,2,'Spring AOP 源码分析系列文章导读',4,'Spring AOP 源码分析系列文章导读','2018-06-17 00:00:00'),
	(7,2,'Spring AOP 源码分析 - 创建代理对象',4,'Spring AOP 源码分析 - 创建代理对象','2018-06-20 00:00:00'),
	(8,1,'Spring MVC 原理探秘 - 一个请求的旅行过程',4,'Spring MVC 原理探秘 - 一个请求的旅行过程','2018-06-29 00:00:00'),
	(9,2,'Spring MVC 原理探秘 - 容器的创建过程',4,'Spring MVC 原理探秘 - 容器的创建过程','2018-06-30 00:00:00'),
	(10,2,'Spring IOC 容器源码分析系列文章导读',4,'Spring IOC 容器源码分析系列文章导读','2018-05-30 00:00:00');

/*!40000 ALTER TABLE `article` ENABLE KEYS */;
UNLOCK TABLES;

# Dump of table author
# ------------------------------------------------------------

DROP TABLE IF EXISTS `author`;

CREATE TABLE `author` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `age` tinyint(4) DEFAULT NULL,
  `sex` tinyint(4) DEFAULT NULL,
  `email` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `author` WRITE;
/*!40000 ALTER TABLE `author` DISABLE KEYS */;

INSERT INTO `author` (`id`, `name`, `age`, `sex`, `email`)
VALUES
	(1,'coolblog.xyz',28,0,'coolblog.xyz@outlook.com'),
	(2,'nullllun',29,1,'coolblog.xyz@outlook.com');

/*!40000 ALTER TABLE `author` ENABLE KEYS */;
UNLOCK TABLES;

# Dump of table student
# ------------------------------------------------------------

DROP TABLE IF EXISTS `student`;

CREATE TABLE `student` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `age` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `student` WRITE;
/*!40000 ALTER TABLE `student` DISABLE KEYS */;

INSERT INTO `student` (`id`, `name`, `age`)
VALUES
	(1,'coolblog',20);

/*!40000 ALTER TABLE `student` ENABLE KEYS */;
UNLOCK TABLES;

3.1 使用 MyBatis 访问数据库

前面说过,MyBatis 是一种半自动化的 Java 持久化框架,使用 MyBatis 需要用户自行维护 SQL。这里,我们把 SQL 放在 XML 中,文件名称为 ArticleMapper.xml。相关配置如下:

<mapper namespace="xyz.coolblog.dao.ArticleDao">
    <resultMap id="articleResult" type="xyz.coolblog.model.Article">
        <id property="id" column="id"/>
        <result property="title" column="title"/>
        <result property="author" column="author"/>
        <result property="content" column="content"/>
        <result property="createTime" column="create_time"/>
    </resultMap>
    
    <select id="findByAuthorAndCreateTime" resultMap="articleResult">
        SELECT
            `id`, `title`, `author`, `content`, `create_time`
        FROM
            `article`
        WHERE
            `author` = #{author} AND `create_time` > #{createTime}
    </select>
</mapper>

上面的 SQL 用于从article表中查询出某个作者从某个时候到现在所写的文章记录。在 MyBatis 中,SQL 映射文件需要与数据访问接口对应起来,比如上面的配置对应xyz.coolblog.dao.ArticleDao接口,这个接口的定义如下:

public interface ArticleDao {
    List<Article> findByAuthorAndCreateTime(@Param("author") String author, @Param("createTime") String createTime);
}

要想让 MyBatis 跑起来,还需要进行一些配置。比如配置数据源、配置 SQL 映射文件的位置信息等。本节所使用到的配置如下:

<configuration>
    <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>
    
    <mappers>
        <mapper resource="mapper/ArticleMapper.xml"/>
    </mappers>
</configuration>

到此,MyBatis 所需的环境就配置好了。接下来把 MyBatis 跑起来吧,相关测试代码如下:

public class MyBatisTest {

    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void prepare() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        inputStream.close();
    }

    @Test
    public void testMyBatis() throws IOException {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            ArticleDao articleDao = session.getMapper(ArticleDao.class);
            List<Article> articles = articleDao.findByAuthorAndCreateTime("coolblog.xyz", "2018-06-10");
        } finally {
            session.commit();
            session.close();
        }
    }
}

在上面的测试代码中,prepare 方法用于创建SqlSessionFactory工厂,该工厂的用途是创建SqlSession。通过 SqlSession,可为我们的数据库访问接口ArticleDao接口生成一个代理对象。MyBatis 会将接口方法findByAuthorAndCreateTime和 SQL 映射文件中配置的 SQL 关联起来,这样调用该方法等同于执行相关的 SQL。

上面的测试代码运行结果如下:

如上,大家在学习 MyBatis 框架时,可以配置一下 MyBatis 的日志,这样可把 MyBatis 的调试信息打印出来,方便观察 SQL 的执行过程。在上面的结果中,==>符号所在的行表示向数据库中输入的 SQL 及相关参数。<==符号所在的行则是表示 SQL 的执行结果。

3.2 JDBC 和 Spring JDBC 简介

JDBC

JDBC 作为 Java 平台的数据库访问规范,它仅提供一种访问数据库的能力。使用步骤:加载数据库驱动,创建数据库连接对象,创建 SQL 执行语句对象,执行 SQL 和处理结果集。缺点: JDBC 流程繁琐;拼接 SQL 可能会导致 SQL 出错;将 SQL 和 Java 代码混在一起,会降低代码的可读性,不利于维护;执行结果的处理麻烦;用户还需要手动管理数据库连接,开始要手动获取数据库连接。使用好后,又要手动关闭数据库连接

Spring JDBC

Spring JDBC 在 JDBC 基础上,进行了比较薄的包装,易用性得到了不少提升。不过它也是存在一定缺陷的,比如 SQL 仍是写在代码中,需要用户自行处理 ResultSet 等。不过与 JDBC 相比,使用 Spring JDBC 无需手动加载数据库驱动,获取数据库连接,以及创建 Statement 对象等操作。总的来说,易用性上得到了不少的提升。

3.3 MyBatis 和 Hibernate 比较

MyBatis 和 Hibernate共同点:都是对jdbc的封装,都是持久层的框架,都用于dao层的开发。

MyBatisHibernate
映射关系配置Java对象与sql语句执行结果的对应关系,多表关联关系配置简单配置Java对象与数据库表的对应关系,多表关联关系配置复杂
SQL优化和移植性MyBatis 需要手动编写 SQL,支持动态 SQL、处理列表、动态生成表名、支持存储过程。开发工作量相对大些。直接使用SQL语句操作数据库,不支持数据库无关性,但sql语句优化容易。Hibernate 对SQL语句封装,提供了日志、缓存、级联(级联比 MyBatis 强大)等特性,此外还提供 HQL(Hibernate Query Language)操作数据库,数据库无关性支持好,但会多消耗性能。如果项目需要支持多种数据库,代码开发量少,但SQL语句优化困难。
学习成本MyBatis 是轻量级框架,学习使用门槛低Hibernate 是重量级框架,学习使用门槛高
使用场景适合于需求变化频繁,大型的项目,比如:互联网电子商务系统适合于需求相对稳定,中小型的项目,比如:办公自动化系统
总结MyBatis 是一个小巧、方便、高效、简单、直接、半自动化的持久层框架Hibernate 是一个强大、方便、高效、复杂、间接、全自动化的持久层框架

4.如何使用 MyBatis

使用 MyBatis 的方式主要有:单独使用 MyBatis,与 Spring 整合使用,与 Spring Boot 整合使用。鉴于目前使用 Spring Boot 的方式比较流行,接下来就介绍 MyBatis 与 Spring Boot 整合使用,其他两种方式读者感兴趣可以自行进行尝试。

4.1 在pom.xml添加依赖

<dependencies>
    <!-- mybatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.4</version>
    </dependency>

    <!-- mysql数据库驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- springboot 2.3.0及以上版本 解决@Validated @Valid 不起作用的问题 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

4.2 在application.yml配置

# 应用服务 WEB 访问端口
server:
  port: 8080

# spring配置
spring:
  application:
    name: spring-boot-mybatis
  # 数据库配置
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false&tinyInt1isBit=false&serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root

# mybatis配置
mybatis:
  # 给实体类配置别名
  type-aliases-package: com.jourwon.spring.boot.*.entity
  # 加载mybatis核心配置文件
  # configuration 和 configLocation 不能同时存在
  # config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/**/*Mapper.xml
  configuration:
    # 开发环境控制台打印sql语句
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 开启驼峰规则自动映射字段属性值:如字段为user_name的可以映射到userName属性中
    map-underscore-to-camel-case: true
    # 设置sql执行超时时间,以秒为单位的全局sql超时时间设置,当超出了设置的超时时间时,会抛出SQLTimeoutException
    default-statement-timeout: 30
    # 解决查询返回结果含null没有对应字段值问题
    call-setters-on-nulls: true

4.3 mapper文件

<?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.jourwon.spring.boot.mapper.UserMapper">
    <resultMap id="BaseResultMap" type="com.jourwon.spring.boot.model.entity.User">
        <id column="user_id" property="userId"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="mobile_phone_number" property="mobilePhoneNumber"/>
        <result column="email" property="email"/>
        <result column="delete_state" property="deleteState"/>
        <result column="create_time" property="createTime"/>
        <result column="update_time" property="updateTime"/>
    </resultMap>

    <sql id="Base_Column_List">
      user_id, username, password, mobile_phone_number, email, delete_state, create_time,
      update_time
    </sql>

    <select id="getByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from user
        where user_id = #{userId}
    </select>
</mapper>

4.4 创建用户表与添加用户数据

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `user_id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '用户名',
  `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '密码',
  `mobile_phone_number` char(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '手机号码',
  `email` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '邮箱',
  `delete_state` tinyint UNSIGNED NULL DEFAULT 0 COMMENT '用户状态,0表示未删除,1表示删除',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`user_id`) USING BTREE,
  UNIQUE INDEX `uk_username`(`username`) USING BTREE COMMENT '用户名唯一'
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = COMPACT;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'JourWon', '123456', '13800000000', 'JourWon@163.com', 0, '2021-01-31 20:54:01', NULL);
INSERT INTO `user` VALUES (2, '马云', '123456', '13800000011', 'JackMa@163.com', 0, '2021-01-31 20:54:01', NULL);
INSERT INTO `user` VALUES (3, '马化腾', '123456', '13800000022', 'PonyMa@163.com', 0, '2021-01-31 20:54:01', NULL);
INSERT INTO `user` VALUES (4, '李彦宏', '123456', '13800000033', 'RobinLee@163.com', 0, '2021-01-31 20:54:01', NULL);
INSERT INTO `user` VALUES (5, '任正非', '123456', '13800000044', 'RenZhengfei@163.com', 0, '2021-01-31 20:54:01', NULL);
INSERT INTO `user` VALUES (6, 'Jobs', '123456', '13800000055', 'Jobs@163.com', 0, '2021-01-31 20:54:01', NULL);
INSERT INTO `user` VALUES (7, 'Bill Gates', '123456', '13800000066', 'Bill Gates@163.com', 0, '2021-01-31 20:54:01', NULL);
INSERT INTO `user` VALUES (8, 'Buffett', '123456', '13800000077', 'Buffett@163.com', 0, '2021-01-31 20:54:01', NULL);

SET FOREIGN_KEY_CHECKS = 1;

4.5 创建实体类

User

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "User-用户")
public class User implements Serializable {

    private static final long serialVersionUID = 5949235389416637695L;

    @ApiModelProperty(value = "用户id")
    private Long userId;

    @ApiModelProperty(value = "用户名")
    private String username;

    @ApiModelProperty(value = "密码")
    private String password;

    @ApiModelProperty(value = "手机号码")
    private String mobilePhoneNumber;

    @ApiModelProperty(value = "邮箱")
    private String email;

    @ApiModelProperty(value = "删除状态(0:未删除,1:已删除)")
    private Short deleteState;

    @ApiModelProperty(value = "创建时间")
    private LocalDateTime createTime;

    @ApiModelProperty(value = "更新时间")
    private LocalDateTime updateTime;

}

4.6 新建dto和vo对象

UserDTO

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "UserDTO-用户")
public class UserDTO implements Serializable {

    private static final long serialVersionUID = -3869159805013367683L;

    @ApiModelProperty(value = "用户id")
    private Long userId;

    @ApiModelProperty(value = "用户名")
    private String username;

    @ApiModelProperty(value = "手机号码")
    private String mobilePhoneNumber;

    @ApiModelProperty(value = "邮箱")
    private String email;

    @ApiModelProperty(value = "删除状态(0:未删除,1:已删除)")
    private Short deleteState;

    @ApiModelProperty(value = "创建时间")
    private LocalDateTime createTime;

    @ApiModelProperty(value = "更新时间")
    private LocalDateTime updateTime;

}

UserVO

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "UserVO-用户")
public class UserVO implements Serializable {

    private static final long serialVersionUID = -2490262010558991748L;

    @ApiModelProperty(value = "用户id")
    private Long userId;

    @ApiModelProperty(value = "用户名")
    private String username;

    @ApiModelProperty(value = "手机号码")
    private String mobilePhoneNumber;

    @ApiModelProperty(value = "邮箱")
    private String email;

    @ApiModelProperty(value = "删除状态(0:未删除,1:已删除)")
    private Short deleteState;

    @ApiModelProperty(value = "创建时间")
    private LocalDateTime createTime;

    @ApiModelProperty(value = "更新时间")
    private LocalDateTime updateTime;

}

4.7 bean转换工具类

public class BeanTransformUtils {

    public static <T> T transform(Object source, Class<T> targetClass) {
        Assert.notNull(source, "Source must not be null");
        Assert.notNull(targetClass, "Target class must not be null");
        T targetObj = null;
        try {
            targetObj = targetClass.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        assert targetObj != null;
        BeanUtils.copyProperties(source, targetObj);
        return targetObj;
    }
}

4.8 用户service接口

public interface UserService {

    /** * 通过主键获取用户 * * @param userId * @return */
    UserDTO getByPrimaryKey(Long userId);
}

用户service实现类

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Override
    public UserDTO getByPrimaryKey(Long userId) {
        User user = userMapper.getByPrimaryKey(userId);
        if (null == user) {
            throw new RuntimeException("查不到userId为" + userId + "对应的用户");
        }

        return BeanTransformUtils.transform(user, UserDTO.class);
    }
}

4.9 用户controller

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private UserService userService;

    @GetMapping("/{userId}")
    @ApiOperation("通过主键获取用户")
    public UserVO getByPrimaryKey(@PathVariable("userId") Long userId) {
        UserDTO userDTO = userService.getByPrimaryKey(userId);
        return BeanTransformUtils.transform(userDTO, UserVO.class);
    }
}

4.10 启动类

@MapperScan(basePackages = "com.jourwon.spring.boot.**.mapper")
@SpringBootApplication
public class SpringBootMybatisApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootMybatisApplication.class, args);
    }

}

最终的工程目录结构如下图

使用postman调用接口输出结果

5.总结

到此,本篇文章就接近尾声了。本篇文章对 MyBatis 是什么,为何要使用,以及如何使用等三个方面进行阐述和演示。总的来说,本文的篇幅应该说清楚了这三个问题。

好了,本篇文章就到这里了,感谢大家的阅读。

参考

相关文章