1 MyBatis简介
MyBatis的前身是Apache的开源项目iBatis。iBatis一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。2010年这个项目由Apache software foundation迁移到Google code,并更名为MyBatis。2013年11月,MyBatis迁移到GitHub上,目前由GitHub提供维护。
MyBatis的优势在于灵活,它几乎可以代替JDBC,同时提供了接口编程。目前MyBatis的数据访问层DAO是不需要实现类的,它只需要一个接口和XML(或者注解)。MyBatis提供自动映射、动态SQL、级联、缓存、注解、代码和SQL分离等特性,使用方便,同时也可以对SQL进行优化。因为其具有封装少、映射多样化、支持存储过程、可以进行SQL优化等特点,使得它取代了Hibernate成为了Java互联网中首选的持久框架。
2 MyBatis和Hibernate的区别
MyBatis和Hibernate的增删改查,对于业务逻辑层来说大同小异,对于映射层而言Hibernate的配置不需要接口和SQL,相反MyBatis是需要的。对于Hibernate而言,不需要编写大量的SQL,就可以完全映射,同时提供了日志、缓存、级联(级联比MyBatis强大)等特性,此外还提供HQL(Hibernate Query Language)对POJO进行操作,使用十分方便,但是它也有致命的缺陷。
由于无需SQL,当多表关联超过3个的时候,通过Hibernate的级联会造成太多性能的丢失。遇到存储过程,Hibernate只能作罢。在管理系统的时代,对于性能的要求不是那么苛刻,但是在互联网时代性能就是系统的根本,响应过慢就会丧失客户,试想一下谁会去用一个经常需要等待超过10秒以上的应用呢?
以上的问题MyBatis都可以解决,MyBatis可以自由书写SQL、支持动态SQL、处理列表、动态生成表名、支持存储过程。这样就可以灵活地定义查询语句,满足各类需求和性能优化的需要,这些在互联网系统中是十分重要的。
但MyBatis也有缺陷。首先,它要编写SQL和映射规则,其工作量稍微大于Hibernate。其次,它支持的工具也很有限,不能像Hibernate那样有许多的插件可以帮助生成映射代码和关联关系,而即使使用生成工具,往往也需要开发者进一步简化,MyBatis通过手工编码,工作量相对大些。所以对于性能要求不太苛刻的系统,比如管理系统、ERP等推荐使用Hibernate;而对于性能要求高、响应快、灵活的系统则推荐使用MyBatis。
3 MyBatis环境
笔者的MyBatis环境如下,数据库是Mysql 8.0.11:
4 MyBatis的核心组件
SqlSessionFactoryBuilder(构造器):它会根据配置或者代码来生成SqlSessionFactory,采用的是分步构建的Builder模式。
SqlSessionFactory(工厂接口):依靠它来生成SqlSession,使用的是工厂模式。
SqlSession(会话):一个既可以发送SQL执行返回结果,也可以获取Mapper的接口。在现有的技术中,一般我们会让其在业务逻辑代码中“消失”,而使用的是MyBatis提供的SQL Mapper接口编程技术,它能提高代码的可读性和可维护性。
SQL Mapper(映射器):MyBatis新设计存在的组件,它由一个Java接口和XML文件(或注解)构成,需要给出对应的SQL和映射规则。它负责发送SQL去执行,并返回结果。
4.1 SqlSessionFactory(工厂接口)
使用MyBatis首先是使用配置或者代码来生产SqlSessionFactory,而MyBatis提供了构造器SqlSessionFactoryBuilder。它提供了一个类org.apache.ibatis.session.Configuration作为引导,采用的是Builder模式。具体的分布则是在Configuration类里面完成的。
在MyBatis中,既可以通过读取配置的XML文件的形式生成SqlSessionFactory,也可以通过Java代码的形式来生成SqlSessionFactory,建议使用XML的形式。当配置了XML或者提供代码后,MyBatis会读取配置文件,通过Configuration类对象构建整个MyBatis的上下文。注意,SqlSessionFactory是一个接口,在MyBatis中它存在两个实现类:SqlSessionManager和DefaultSqlSessionFactory。一般而言,具体是由DefaultSqlSessionFactory来实现的,而SqlSessionManager使用在多线程的环境中,它的具体实现依靠DefaultSqlSessionFactory。
每个基于MyBatis的应用都是以一个SqlSessionFactory 的实例为中心的,而SqlSessionFactory唯一的作用就是生产MyBatis的核心接口对象SqlSession,所以它的责任是唯一的。我们往往会采用单例模式处理它,下面讨论使用配置文件和Java代码两种形式来生成SqlSessionFactory的方法。
4.1.1 使用XML构建SqlSessionFactory
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>
<!-- 别名 -->
<typeAliases>
<typeAlias alias="role"
type="com.hys.mybatis.example1.pojo.Role" />
</typeAliases>
<!--数据库环境 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url"
value="jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC&useSSL=false" />
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
<!-- 映射文件 -->
<mappers>
<mapper
resource="com/hys/mybatis/example1/mapper/RoleMapper.xml" />
</mappers>
</configuration>
- <typeAlias>元素定义了一个别名,它代表着com.hys.mybatis.example1.pojo.Role这个类。这样定义后,在MyBatis上下文中就可以使用别名来代替权限定名了。
- <environment>元素的定义,这里描述的是数据库。它里面的<transactionManager>元素是配置事务管理器,这里采用的是MyBatis的JDBC管理器方式。然后采用<dataSource>元素配置数据库,其中属性type="POOLED"代表采用MyBatis内部提供的连接池方式,最后定义一些关于JDBC的属性信息。
- <mapper>元素代表引入的那些映射器,在谈到映射器时会详细讨论它。
SqlSessionFactory sqlSessionFactory = null;
String resource = "com/hys/mybatis/example1/config/mybatis-config.xml";
InputStream inputStream;
try {
inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
上面代码就是用来生成SqlSessionFactory的代码。首先读取mybatis-config.xml,然后通过SqlSessionFactoryBuilder的build方法来创建SqlSessionFactory。整个过程比较简单,而里面的步骤还是比较烦琐的,只是MyBatis采用了Builder模式为开发者隐藏了这些细节。这样一个SqlSessionFactory就被创建出来了。
采用XML创建的形式,信息在配置文件中,有利于我们日后的维护和修改,避免了重新编译代码,所以推荐使用这种方式。
4.1.2 使用代码创建SqlSessionFactory
//数据库连接信息
PooledDataSource dataSource = new PooledDataSource();
dataSource.setDriver("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC&useSSL=false");
dataSource.setUsername("root");
dataSource.setPassword("root");
dataSource.setDefaultAutoCommit(false);
//采用MyBatis的JDBC事务方式
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
//创建Configuration对象
Configuration configuration = new Configuration(environment);
//注册一个MyBatis上下文别名
configuration.getTypeAliasRegistry().registerAlias("role", Role.class);
//加入一个映射器
configuration.addMapper(RoleMapper.class);
//使用SqlSessionFactoryBuilder构建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
return sqlSessionFactory;
注意代码中的注释,它和XML方式实现的功能是一致的,只是方式不太一样而已。但是代码冗长,如果发生系统修改,那么有可能需要重新编译代码才能继续,所以这不是一个很好的方式。除非有特殊的需要,比如在配置文件中,需要配置加密过的数据库用户名和密码,需要我们在生成SqlSessionFactory前加密为明文的时候,才会考虑使用这样的方式。
4.2 SqlSession
在MyBatis中,SqlSession是其核心接口。在MyBatis中有两个实现类,DefaultSqlSession和SqlSessionManager。DefaultSqlSession是单线程使用的,而SqlSessionManager在多线程环境下使用。SqlSession的作用类似于一个JDBC中的Connection对象,代表着一个连接资源的启用。具体而言,它的作用有3个:
- 获取Mapper接口。
- 发送SQL给数据库。
- 控制数据库事务。
SqlSession sqlSession = sqlSessionFactory.openSession();
以上就是创建SqlSession的代码,通过SqlSessionFactory的openSession方法。
SqlSession控制数据库事务的方法如下:
//定义SqlSession
SqlSession sqlSession = null;
try {
//打开SqlSession会话
sqlSession = sqlSessionFactory.openSession();
//do something...
sqlSession.commit();
} catch (Exception e) {
sqlSession.rollback();
} finally {
//在finally语句中确保资源被顺利关闭
if (sqlSession != null) {
sqlSession.close();
}
}
这里使用commit方法提交事务,或者使用rollback方法回滚事务。因为它代表着一个数据库的连接资源,使用后要及时关闭它,如果不关闭,那么数据库的连接资源就会很快被耗费光,整个系统就会陷入瘫痪状态,所以用finally语句保证其顺利关闭。
4.3 映射器
映射器是MyBatis中最重要、最复杂的组件,它由一个接口和对应的XML文件(或注解)组成。它可以配置以下内容:
- 描述映射规则。
- 提供SQL语句,并可以配置SQL参数类型、返回类型、缓存刷新等信息。
- 配置缓存。
- 提供动态SQL。
首先定义一个POJO如下:
public class Role {
private Long id;
private String roleName;
private String note;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
映射器的主要作用就是将SQL查询到的结果映射为一个POJO,或者将POJO的数据插入到数据库中,并定义一些关于缓存等的重要内容。
注意,开发只是一个接口,而不是一个实现类。MyBatis会为这个接口生成一个代理对象,代理对象会去处理相关的逻辑。
4.3.1 用XML实现映射器
用XML定义映射器分为两个部分:接口和XML。先定义一个映射器接口如下:
public interface RoleMapper {
public Role getRole(Long id);
}
在上述所说的mybatis-config.xml配置文件中有这样一段代码:
<!-- 映射文件 -->
<mappers>
<mapper
resource="com/hys/mybatis/example1/mapper/RoleMapper.xml" />
</mappers>
它的作用就是引入一个XML文件,用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.hys.mybatis.example1.mapper.RoleMapper">
<select id="getRole" parameterType="long" resultType="role">
SELECT
id,
role_name AS roleName, note FROM t_role WHERE id = #{id}
</select>
</mapper>
有了这两个文件,就完成了一个映射器的定义。
- <mapper>元素中的属性namespace所对应的是一个接口的全限定名,于是MyBatis上下文就可以通过它找到对应的接口。
- <select>元素表明这是一条查询语句,而属性id标识了这条SQL,属性parameterType="long"说明传递给SQL的是一个long型的参数,而resultType="role"表示返回的是一个role类型的返回值。而role是之前配置文件mybatis-config.xml配置的别名,指代的是com.hys.mybatis.example1.pojo.Role。
- 这条SQL中的#{id}表示传递进去的参数。#是将传入的数据都当成一个字符串,会对传入的数据自动加上引号;$将传入的数据直接显示生成在SQL中。注意:使用$占位符可能会导致SQL注入攻击,能用#的地方就不要使用$,写order by子句的时候应该用$而不是#。
4.3.2 注解实现映射器
除XML方式定义映射器外,还可以采用注解方式定义映射器,它只需要一个接口就可以通过MyBatis的注解来注入SQL,代码如下:
public interface RoleMapper {
@Select("SELECT id, role_name AS roleName, note FROM t_role WHERE id = #{id}")
public Role getRole(Long id);
}
这完全等同于XML方式创建映射器。如果它和XML方式同时定义时,XML方式将覆盖掉注解方式,所以MyBatis官方推荐使用的是XML方式。
虽然注解方式实现起来更加简单,但是如果SQL比较复杂,放入@Select中会明显增加注解的内容,代码的可读性会下降,不利于日后的维护和修改。同时考虑到SQL会因数据库版本的不同而不同,需要做定制化开发。显然用XML的方式更简便,只需要生成不同数据库版本的XML即可。
4.3.3 SqlSession发送SQL
Role role = (Role) sqlSession.selectOne("com.hys.mybatis.example1.mapper.RoleMapper.getRole", 1L);
selectOne方法表示使用查询并且只返回一个对象,而参数则是一个String对象和一个Object对象。String对象是由一个命名空间加上SQL id组合而成的,它完全定位了一条SQL,这样MyBatis就会找到对应的SQL。Object对象是传进SQL的参数,这里是传进主键。
如果在MyBatis中只有一个id为getRole的SQL,那么也可以简写为:
Role role = (Role) sqlSession.selectOne("getRole", 1L);
4.3.4 用Mapper接口发送SQL
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
Role role = roleMapper.getRole(1L);
SqlSession还可以获取Mapper接口,通过Mapper接口发送SQL。通过SqlSession的getMapper方法来获取一个Mapper接口,就可以调用它的方法了。
4.3.5 对比两种发送SQL方式
这里建议使用SqlSession获取Mapper的方式来发送SQL,理由如下:
- 使用Mapper接口编程可以消除SqlSession带来的功能性代码,提高可读性,而SqlSession发送SQL,需要一个SQL id来匹配SQL,比较晦涩难懂。使用Mapper接口,类似roleMapper.getRole(1L)则是完全面向对象的语言,更能体现业务的逻辑。
- 使用roleMapper.getRole(1L)方式,IDE会提示错误和校验,而使用sqlSession.selectOne("getRole", 1L)语法,只有在运行中才能知道是否会产生错误。
目前使用Mapper接口编程已成为主流,尤其在Spring中运用MyBatis时,Mapper接口的使用就更为简单。
4.4 生命周期
所谓生命周期就是每一个对象应该存活的时间,比如一些对象一次用完后就要关闭,使他们被JVM销毁,以避免继续占用资源,所以我们会根据每一个组件的作用来确定其生命周期。
4.4.1 SqlSessionFactoryBuilder
SqlSessionFactoryBuilder的作用在于创建SqlSessionFactory,创建成功后,SqlSessionFactoryBuilder就失去了作用,所以它只能存在于创建SqlSessionFactory的方法中,而不要让其长期存在。4.4.2 SqlSessionFactory
SqlSessionFactory可以被认为是一个数据库连接池,它的作用是创建SqlSession接口对象。因为MyBatis的本质就是Java对数据库的操作,所以SqlSessionFactory的生命周期存在于整个MyBatis的应用之中,所以一旦创建了SqlSessionFactory,就要长期保存它,直至不再使用MyBatis应用,所以可以认为SqlSessionFactory的生命周期就等同于MyBatis的应用周期。
由于SqlSessionFactory是一个对数据库的连接池,所以它占据着数据库的连接资源。如果创建多个SqlSessionFactory,那么就存在多个数据库连接池,这样不利于对数据库资源的控制,也会导致数据库连接资源被消耗光,出现系统宕机等消息,所以尽量避免发生这样的情况。因此在一般的应用中我们往往希望SqlSessionFactory作为一个单例,让它在应用中被共享。
4.4.3 SqlSession
SqlSession相当于一个数据库连接(Connection对象),你可以在一个事务里面执行多条SQL,然后通过它的commit、rollback等方法,提交或者回滚事务。所以它应该存活在一个业务请求中,处理完整个请求后应该关闭这条连接,让它归还给SqlSessionFactory,否则数据库资源就很快被耗费精光,系统就会瘫痪,所以用try...catch...finally...语句来保证其正确关闭。
4.4.4 Mapper
Mapper是一个接口,它由SqlSession所创建,所以它的最大生命周期至多和SqlSession保持一致,尽管它很好用,但是由于SqlSession的关闭,它的数据库连接资源也会消失,所以它的生命周期应该小于等于SqlSession的生命周期。Mapper代表的是一个请求中的业务处理,所以它应该在一个请求中,一旦处理完了相关的业务,就应该废弃它。
5 实例
POJO:
package com.hys.mybatis.example1.pojo;
public class Role {
private Long id;
private String roleName;
private String note;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
接口:
package com.hys.mybatis.example1.mapper;
import java.util.List;
import com.hys.mybatis.example1.pojo.Role;
public interface RoleMapper {
public int insertRole(Role role);
public int deleteRole(Long id);
public int updateRole(Role role);
public Role getRole(Long id);
public List<Role> findRoles(String roleName);
}
SQL映射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.hys.mybatis.example1.mapper.RoleMapper">
<insert id="insertRole" parameterType="role">
INSERT INTO
t_role(role_name,note) VALUES(#{roleName}, #{note})
</insert>
<delete id="deleteRole" parameterType="long">
DELETE FROM t_role WHERE
id = #{id}
</delete>
<update id="updateRole" parameterType="role">
UPDATE t_role SET
role_name = #{roleName}, note = #{note} WHERE id =
#{id}
</update>
<select id="getRole" parameterType="long" resultType="role">
SELECT
id,
role_name AS roleName, note FROM t_role WHERE id = #{id}
</select>
<select id="findRoles" parameterType="string" resultType="role">
SELECT
id, role_name AS roleName, note FROM t_role WHERE role_name LIKE
CONCAT('%', #{roleName}, '%')
</select>
</mapper>
配置文件:
<?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>
<!-- 别名 -->
<typeAliases>
<typeAlias alias="role"
type="com.hys.mybatis.example1.pojo.Role" />
</typeAliases>
<!--数据库环境 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url"
value="jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC&useSSL=false" />
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
<!-- 映射文件 -->
<mappers>
<mapper
resource="com/hys/mybatis/example1/mapper/RoleMapper.xml" />
</mappers>
</configuration>
构建SqlSessionFactory:
package com.hys.mybatis.example1.utils;
import java.io.IOException;
import java.io.InputStream;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
public class SqlSessionFactoryUtils {
private final static Class<SqlSessionFactoryUtils> LOCK = SqlSessionFactoryUtils.class;
private static SqlSessionFactory sqlSessionFactory = null;
private SqlSessionFactoryUtils() {}
public static SqlSessionFactory getSqlSessionFactory() {
synchronized (LOCK) {
if (null != sqlSessionFactory) {
return sqlSessionFactory;
}
String resource = "com/hys/mybatis/example1/config/mybatis-config.xml";
InputStream inputStream;
try {
inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
return null;
}
return sqlSessionFactory;
}
}
public static SqlSession openSqlSession() {
if (null == sqlSessionFactory) {
getSqlSessionFactory();
}
return sqlSessionFactory.openSession();
}
}
测试类:
package com.hys.mybatis.example1.test;
import org.apache.ibatis.session.SqlSession;
import com.hys.mybatis.example1.mapper.RoleMapper;
import com.hys.mybatis.example1.pojo.Role;
import com.hys.mybatis.example1.utils.SqlSessionFactoryUtils;
public class Test {
public static void main(String[] args) {
SqlSession sqlSession = null;
try {
sqlSession = SqlSessionFactoryUtils.openSqlSession();
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
Role role = roleMapper.getRole(1L);
System.out.println(role.getRoleName());
System.out.println(role.getNote());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != sqlSession) {
sqlSession.close();
}
}
}
}
运行结果:
log4j:WARN No appenders could be found for logger (org.apache.ibatis.logging.LogFactory).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
Robert Hou
Programmer
参考资料:[1]杨开振 周吉文 梁华辉 谭茂华.Java EE 互联网轻量级框架整合开发:SSM框架(Spring MVC+Spring+MyBatis)和Redis实现[M].北京:电子工业出版社,2017.7