MyBatis系列之简介及其核心组件

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&amp;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子句的时候应该用$而不是#
        注意,我们并没有配置SQL执行后和role的对应关系,这里采用的是一种被称为自动映射的功能,MyBatis在默认情况下提供自动映射,只要SQL返回的列名能和POJO对应起来即可。这里SQL返回的列名id和note是可以和之前定义的POJO的属性对应起来的,而表里的列role_name通过SQL别名的改写,使其成为roleName,也是和POJO对应起来的,所以此时MyBatis就可以把SQL查询的结果通过自动映射的功能映射成为一个POJO。

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

猜你喜欢

转载自blog.csdn.net/weixin_30342639/article/details/81042804
今日推荐