1. 前言
MyBatis是一款优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。相对于Hibernate和ApacheOJB等“一站式”ORM解决方案而言,MyBatis是一款“半自动化”的ORM实现。
- 本文较为简略,主要是入门参考,深入学习请参考Mapper、PageHelper插件作者刘增辉书籍《MyBatis从入门到精通》
2. MyBatis简介
2.1 三款框架的ORM对比图
以下针对Spring JDBC
、Spring Data Jpa
、Mybatis
**三款框架做了个粗略的对比。一般应用的性能瓶颈并不是在于ORM,所以这三个框架技术选型应该考虑项目的场景
、团队的技能掌握情况
、开发周期(开发效率)
…
框架对比 | Spring JDBC | Spring Data Jpa | Mybatis |
---|---|---|---|
性能 | 性能最好 | 性能最差 | 居中 |
代码量 | 多 | 少 | 多 |
学习成本 | 低 | 高 | 居中 |
推荐指数 | ❤❤❤ | ❤❤❤❤❤ | ❤❤❤❤❤ |
- 个人观点:
抛开学习成本而言,相对于中小型企业来说jpa开发无异于是最快速的,但鉴于国内市场环境而言,学习MyBatis是最佳选择。低学习成本和动态SQL解耦的特点使得更容易被人们所接受。对于业务复杂且对性能要求较高的项目来说Mybatis
往往能更好的胜任,可以自己进行SQL优化,同时更让我喜欢的是Mybatis分页插件与通用Mapper(单表CURD无需自己手写)有了这两款插件的支持,怎么能不选择MyBatis?
2.2 MyBatis
作为一款使用广泛的开源软件,它的特点有哪些呢?
优点
- SQL 被统一提取出来,便于统一管理和优化
- SQL 和代码解耦,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰、更易维护、更易单元测试
- 提供映射标签,支持对象与数据库的 ORM 字段关系映射
- 提供对象关系映射标签,支持对象关系组件维护
- 灵活书写动态 SQL,支持各种条件来动态生成不同的 SQL
缺点
- 编写 SQL 语句时工作量很大,尤其是字段多、关联表多时,更是如此
- SQL 语句依赖于数据库,导致数据库移植性差
MyBatis 几个重要的概念
Mapper 配置可以使用基于 XML 的 Mapper 配置文件来实现,也可以使用基于 Java 注解的 MyBatis 注解来实现,甚至可以直接使用 MyBatis 提供的 API 来实现。
Mapper 接口是指自行定义的一个数据操作接口,类似于通常所说的 DAO 接口。早期的 Mapper 接口需要自定义去实现,现在 MyBatis 会自动为 Mapper 接口创建动态代理对象。Mapper 接口的方法通常与 Mapper 配置文件中的 select、insert、update、delete 等 XML 结点存在一一对应关系。
Executor,MyBatis 中所有的 Mapper 语句的执行都是通过 Executor 进行的,Executor 是 MyBatis 的一个核心接口。
SqlSession,是 MyBatis 的关键对象,是执行持久化操作的独享,类似于 JDBC 中的 Connection,SqlSession 对象完全包含以数据库为背景的所有执行 SQL 操作的方法,它的底层封装了 JDBC 连接,可以用 SqlSession 实例来直接执行被映射的 SQL 语句。
SqlSessionFactory,是 MyBatis 的关键对象,它是单个数据库映射关系经过编译后的内存镜像。SqlSessionFactory 对象的实例可以通过SqlSessionFactoryBuilder 对象类获得,而SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先定制的 Configuration 的实例构建出。
MyBatis 的工作流程如下:
- 首先加载 Mapper 配置的 SQL 映射文件,或者是注解的相关 SQL 内容。
- 创建会话工厂,MyBatis 通过读取配置文件的信息来构造出会话工厂(SqlSessionFactory)。
- 创建会话。根据会话工厂,MyBatis 就可以通过它来创建会话对象(SqlSession),会话对象是一个接口,该接口中包含了对数据库操作的增、删、改、查方法。
- 创建执行器。因为会话对象本身不能直接操作数据库,所以它使用了一个叫做数据库执行器(Executor)的接口来帮它执行操作。
- 封装 SQL 对象。在这一步,执行器将待处理的 SQL 信息封装到一个对象中(MappedStatement),该对象包括 SQL 语句、输入参数映射信息(Java 简单类型、HashMap 或 POJO)和输出结果映射信息(Java 简单类型、HashMap 或 POJO)。
- 操作数据库。拥有了执行器和 SQL 信息封装对象就使用它们访问数据库了,最后再返回操作结果,结束流程。
在我们具体的使用过程中,就是按照上述的流程来执行。
3. springboot集成MyBatis
导入依赖
在pom.xml文件中添加MyBatis 依赖 MyBatis-Spring-Boot-Starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!--mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
注: 在IDEA新建项目时,可以直接勾选上MyBatis和MySQL,IDEA会自动导入依赖,但是在MySQL依赖中记得去除runtime,这个会导致你的MySQL驱动无法导入com.mysql.cj.jdbc.Driver报错。
mybatis-spring-boot-starter 是 MyBatis 帮助我们快速集成 Spring Boot 提供的一个组件包,使用这个组件可以做到以下几点:
- 构建独立的应用
- 几乎可以零配置
- 需要很少的 XML 配置
初始化脚本(创建表)
CREATE TABLE `t_user` (
`id` int(8) NOT NULL AUTO_INCREMENT COMMENT '主键自增',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(50) NOT NULL COMMENT '密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
application 配置
application.yml 添加相关配置:
#数据库配置 spring:
datasource:
url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT&useSSL=true
username: root
password: rootroot
driver-class-name: com.mysql.cj.jdbc.Driver mybatis:
mapper-locations: classpath:com/demo/mapper/*.xml
type-aliases-package: com.demo.entity
# 驼峰命名规范 如:数据库字段是 order_id 那么 实体字段就要写成 orderId
configuration.map-underscore-to-camel-case: true
其中:
- mybatis.config-location,配置 mybatis-config.xml 路径,mybatis-config.xml 中配置 MyBatis 基础属性;
- mybatis.mapper-locations,配置 Mapper 对应的 XML 文件路径;
- mybatis.type-aliases-package,配置项目中实体类包路径;
- spring.datasource.*,数据源配置。
- application.properties 配置文件改成yml时,一定要把原来的properties 文件删掉再新建yml文件不然启动类会产生扫描找不到而报错。
- 上述项目目录如下图:
创建实体类
package com.demo.entity;
import java.io.Serializable;
/**
* @Author: MaoLin
* @Date: 2019/2/8 16:33
* @Version 1.0
*/
public class User implements Serializable {
private static final long serialVersionUID = 8655851615465363473L;
private Long id;
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public Long getId() {
return id;
}
public void setId(Long 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;
}
}
持久层
MyBatis 以前只有 XML 配置这种使用的形式,到了后来注解使用特别广泛,MyBatis 也顺应潮流提供了注解的支持,从这里可以看出 MyBatis 一直都跟随着主流技术的变化来完善自己。接下来给大家介绍一下两种方法的使用。
UserMapper
package com.demo.mapper;
import com.demo.entity.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @Author: MaoLin
* @Date: 2019/2/8 16:35
* @Version 1.0
*/
@Mapper
public interface UserMapper {
/**
* 注解方法
* 根据用户名查询用户结果集
* @param username 用户名
* @return 查询结果
*/
@Select("SELECT * FROM t_user WHERE username = #{username}")
List<User> findByUsername(@Param("username") String username);
/**
* xml配置方法
* 保存用户信息
* @param user 用户信息
* @return 成功 1 失败 0
*/
int insert(User user);
}
UserMapper
映射文件
<?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.battcn.mapper.UserMapper">
<insert id="insert" parameterType="com.battcn.entity.User">
INSERT INTO `t_user`(`username`,`password`) VALUES (#{username},#{password})
</insert>
</mapper>
注:其中Select查询为注解的方法,不需要使用映射文件,直接注解就能使用,insert是采用的xml方法,SQL语句需要在映射文件中写,具体操作的使用,请参考mybatis 官方文档。
测试
junit
测试类来检验代码的正确性。直接在IDEA项目的test测试文件里面操作即可。
package com.demo;
import com.demo.entity.User;
import com.demo.mapper.UserMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
private static final Logger log = LoggerFactory.getLogger(DemoApplicationTests.class);
@Autowired
private UserMapper userMapper;
@Test
public void test1() throws Exception {
final int row1 = userMapper.insert(new User("u1", "p1"));
log.info("[添加结果] - [{}]", row1);
final int row2 = userMapper.insert(new User("u2","p2"));
log.info("[添加结果] - [{}]", row2);
final int row3 = userMapper.insert(new User("u1", "p3"));
log.info("[添加结果] - [{}]", row3);
final List<User> u1 = userMapper.findByUsername("u1");
log.info("[根据用户名查询] - [{}]", u1);
}
}
4. 通用Mapper与分页插件的集成
插件介绍
这两款插件作者均是同一个人,如果你想深入了解Mybatis
以及插件开发可以购买作者的书籍
分页插件
在没有分页插件之前,写一个分页需要两条SQL语句,一条查询一条统计,然后才能计算出页码,这样的代码冗余而又枯燥,更重要的一点是**数据库迁移
**,众所周知不同的数据库分页写法是不同的,而Mybatis
不同于Hibernate
的是它只提供动态SQL和结果集映射。值得庆幸的是,它虽然没有为分页提供良好的解决方案,但却提供了Interceptor
以供开发者自己扩展,这也是这款分页插件的由来….
通用Mapper
通用 Mapper
是一个可以实现任意 MyBatis
通用方法的框架,项目提供了常规的增删改查操作以及 Example
相关的单表操作。通用 Mapper 是为了解决 MyBatis 使用中 90% 的基本操作,使用它可以很方便的进行开发,可以节省开发人员大量的时间。
导入依赖
你只需要添加通用 Mapper 提供的 starter 就完成了最基本的集成,依赖如下:
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>版本号</version>
</dependency>
此时最新版本号2.1.5,你也可以从官方地址查看:
http://mvnrepository.com/artifact/tk.mybatis/mapper-spring-boot-starter
注意:引入该 starter 时,和 MyBatis 官方的 starter 没有冲突,但是官方的自动配置不会生效!
application 配置
# 通用mapper配置
mapper:
mappers: tk.mybatis.mapper.common.Mapper
not-empty: false
# 主键自增回写,默认为 MYSQL
identity: MYSQL
# 分页插件配置
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql
实体类
通用Mapper
采用了JPA规范包中的注解,这种的设计避免了重复造轮子,更是让Spring Data Jpa
的应用可以轻松切换到Mybatis
和上面实体大体一致,只是增加了注解。
package com.demo.entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
/**
* @Author: MaoLin
* @Date: 2019/2/8 16:33
* @Version 1.0
*/
@Table(name = "t_user")
public class User implements Serializable {
private static final long serialVersionUID = 8655851615465363473L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public Long getId() {
return id;
}
public void setId(Long 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;
}
}
持久层
UserMapper
- 继承
BaseMapper<T>
就可以了,这点是不是有点类似JpaRepository
,同时也可以根据自己需要扩展出更适合自己项目的BaseMapper
,它的灵活也是众多开发者喜爱的因素之一
package com.demo.mapper;
import com.demo.entity.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import tk.mybatis.mapper.common.BaseMapper;
import java.util.List;
/**
* @Author: MaoLin
* @Date: 2019/2/8 16:35
* @Version 1.0
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 根据用户名查询用户结果集
*
* @param username 用户名
* @return 查询结果
*/
@Select("SELECT * FROM t_user WHERE username = #{username}")
List<User> findByUsername(@Param("username") String username);
/**
* 保存用户信息
*
* @param user 用户信息
* @return 成功 1 失败 0
*/
@Insert("INSERT INTO `t_user`(`username`,`password`) VALUES (#{username},#{password})")
int insert(User user);
/**
* 根据用户名统计(TODO 假设它是一个很复杂的SQL)
*
* @param username 用户名
* @return 统计结果
*/
int countByUsername(String username);
}
UserMapper
映射文件
<?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.demo.mapper.UserMapper">
<select id="countByUsername" resultType="java.lang.Integer">
SELECT count(1) FROM t_user WHERE username = #{username}
</select>
</mapper>
测试
package com.demo;
import com.demo.entity.User;
import com.demo.mapper.UserMapper;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
private static final Logger log = LoggerFactory.getLogger(DemoApplicationTests.class);
@Autowired
private UserMapper userMapper;
@Test
public void test1() throws Exception {
final int row1 = userMapper.insert(new User("u1", "p1"));
log.info("[添加结果] - [{}]", row1);
final int row2 = userMapper.insert(new User("u2","p2"));
log.info("[添加结果] - [{}]", row2);
final int row3 = userMapper.insert(new User("u1", "p3"));
log.info("[添加结果] - [{}]", row3);
final List<User> u1 = userMapper.findByUsername("u1");
log.info("[根据用户名查询] - [{}]", u1);
final User user1 = new User("u1", "p1");
final User user2 = new User("u1", "p2");
final User user3 = new User("u3", "p3");
userMapper.insertSelective(user1);
log.info("[user1回写主键] - [{}]", user1.getId());
userMapper.insertSelective(user2);
log.info("[user2回写主键] - [{}]", user2.getId());
userMapper.insertSelective(user3);
log.info("[user3回写主键] - [{}]", user3.getId());
final int count = userMapper.countByUsername("u1");
log.info("[调用自己写的SQL] - [{}]", count);
// TODO 模拟分页
for (int i = 0;i<10;i++){
userMapper.insertSelective(new User("u"+i, "p"+i));
}
// TODO 分页 + 排序 this.userMapper.selectAll() 这一句就是我们需要写的查询,有了这两款插件无缝切换各种数据库
final PageInfo<Object> pageInfo = PageHelper.startPage(1, 10).setOrderBy("id desc").doSelectPageInfo(() -> this.userMapper.selectAll());
log.info("[lambda写法] - [分页信息] - [{}]", pageInfo.toString());
PageHelper.startPage(1, 10).setOrderBy("id desc");
final PageInfo<User> userPageInfo = new PageInfo<>(this.userMapper.selectAll());
log.info("[普通写法] - [{}]", userPageInfo);
}
}
结果
控制台结果:
5. 小结&参考资料
小结
要多查官方文档,一直在更新,你在使用的时候,可能有点已经不能再继续使用了,所以要多看官方文档。
本篇博客也只是简单入门,如果要深入研究,可以看一下作者的书籍。
源码GIT地址:https://github.com/chapaofan/MyBatis