跳至主要內容

一、MyBatis入门

hahg大约 13 分钟

一、MyBatis入门

MyBatis结构

1.1 MyBatis 的下载

​ MyBatis 可以在 Github 官网下载:[ https://github.com/mybatis/mybatis-3 ]

1.2 MyBatis 概述

1.2.1 MyBatis 简介

​ MyBatis是一个优秀的基于Java的持久层框架,它内部封装了JDBC,使开发者 只需关注SQL语句本身 ,而不用再花费精力去处理诸如注册驱动、创建 Connection 、配置 Statement 等繁杂过程。

​ Mybatis通过 xml 或注解 的方式将要执行的各种statement(statement、preparedStatement 等)配置起来,并通过Java对象和Statement中SQL的动态参数进行映射生成最终执行的SQL语句,最后由MyBatis框架执行SQL并将结果映射成Java对象并返回。

1.2.2 MyBatis与Hibernate

​ Hibernate框架是提供了全面的数据库封装机制的 “ 全自动 ” ORM,即实现了 POJO 和数据库表之间的映射,以及 SQL 的 自动生成和执行

​ 相对于此,MyBatis 只能算作是 “ 半自动 ” ORM。其着力点,是在 POJO 类与 SQL 语句之间的映射关系。也就是说,MyBatis 并不会为程序员自动生成 SQL 语句 。具体的 SQL 需要程序员自己编写,然后通过 SQL 语句映射文件,将 SQL 所需的参数,以及返回的结果字段映射到指定 POJO 。因此,MyBatis 成为了 “ 全自动 ” ORM的一种有益补充。

​ 与Hibernate相比,MyBatis具有以下几个特点:

  1. 在XML文件中配置SQL语句,实现了SQL语句与代码的分离,给程序的维护带来了很大便利。
  2. 因为需要程序员自己去编写SQL语句,程序员可以结合数据库自身的特点灵活控制SQL语句,因此能够实现比Hibernate等全自动ORM框架更高的查询效率,能够完成复杂查询。
  3. 简单,易于学习,易于使用,上手快。

1.3 MyBatis 体系结构

MyBatis体系结构

1.4 MyBatis工作原理

MyBatis工作原理

1.5 第一个 MyBatis 程序

​ 需求:实现将 Student 信息写入到数据库里。

1.5.0 导入 Jar 包

  • 导入下载好的 MyBatis 中的子文件夹 lib 里的全部 Jar 包和核心 Jar 包 mybatis-x.x.x.jar;
  • 导入 mysql-connector-java-x.x.xx.jar
  • 导入 hamcrest-core-1.3.jar(因为需要用到 log4j)

1.5.1 定义实体类 Student

Student.java

public class Student {
	public Integer id;
	public String name;
	public int age;
	public double scroe;

	// 有参和无参构造器
	// getter 和 setter
	// toString()
}

提示

id 属性的类型是 Integer,是为了便于判空;

成员变量 是类私有的,只要有了 getter 和 setter ,该成员变量就变成了 属性,对其他类公开。

1.5.2 在数据库里创建表

student数据库表

注意

在创建数据库表时,字段名要与属性名一致。(不一致时,需要用过 resultMap 解决)

1.5.3 定义 dao 接口

IStudentDao.java:定义 dao 接口,用于后面实现将学生信息插入数据库的操作。

public interface IStudentDao {
	void insertStudent(Student student);
}

1.5.4 定义映射文件

​ 映射文件主要完成 Dao 层中 SQL 语句的映射。映射文件名随意,一般为 mapper.xml。

​ 映射文件需要约束文件来解释,约束文件在 mybatis-x.x.x.jar 的 org.apache.ibatis.builder.xml 包里,如下图所示。

mapper.xml:而在映射文件中需要指定约束文件。

  • namespace 属性为当前映射的名字;
  • 因为执行插入操作,所以需要使用 <insert/> 标签;其属性 id 会在调用 sql 语句时使用;属性 parameterType 是传进 sql 语句的值的类型;然后可以使用 ” #{} “ 符号来取出 bean 里的属性。
<?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="test">
	<insert id="insertStudent" parameterType="com.hahg.beans.Student">
		insert into student(name,age,score) values (#{name},#{age},#{score})
	</insert>
</mapper>

1.5.4 配置主配置文件

  • <settings/> 标签用于使用 Log4j2 日志输出技术

  • <environments/> 标签里可以有 多个 <environment/> 子标签,用于在多个环境里切换。该标签的 default 属性用于选择当前使用的环境配置。

  • <transactionManager/> 标签为事务管理器,其属性 type 若为 “ JDBC ” 则代表使用 JDBC 默认的事务管理;若为 “ Manager ” ,则代表使用第三方事务管理。

  • <dataSource/> 标签的 type 属性的值 “ POOLED ” 代表使用数据库连接池。

  • <property/> 标签用于配置连接数据库的四要素—— driver、url、username、password。

    • url 的值中的 “ jdbc:mysql:///test …… ” ;三个 “ /// ” 是默认连接地址的缩写—— //127.0.0.1:3306/
    • 其中的 “ & ” 连接字符需要使用 “ &amp; " 来表示。
<?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>
    <settings>
		<setting name="logImpl" value="LOG4J2" />
	</settings>
	<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:///test?useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=UTC" />
				<property name="username" value="root" />
				<property name="password" value="password" />
			</dataSource>
		</environment>
	</environments>
	<mappers>
		<mapper resource="com/hahg/dao/mapper.xml" />
	</mappers>
</configuration>

1.5.5 定义 dao 接口的实现类

StudentDaoImpl.java:实现插入到数据库的操作。

  • 第 9 行加载主配置文件;
  • 第 11 行创建 SqlSessionFactory 对象;
  • 第 13 行创建 SqlSession 对象;
  • 第 15 行调用 insert() 方法执行插入操作,第一个参数为映射文件中执行语句的 id 名字,若有两个配置文件,且它们中有 id 名字一致时,需要在前面加上映射的名字例如, test.insertStudent ;第二个参数为需要插入的对象;
  • 第 17 行调用 commit() 方法,提交插入请求。
public class StudentDaoImpl implements IStudentDao {

	private SqlSession sqlSession;

	@Override
	public void insertStudent(Student student) {
		try {
			// 1. 加载配置文件
			InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
			// 2. 创建SqlSessionFactory对象
			SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
			// 3. 创建SqlSession对象
			sqlSession = sqlSessionFactory.openSession();
			// 4. 执行相关操作
			sqlSession.insert("insertStudent", student);
			
			sqlSession.commit();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally {
			if(sqlSession!=null) {
				sqlSession.close();
			}
		}
	}
}

1.5.6 定义测试类

MyTest.java:使用 junit 测试来运行方法。这里创建了实现类的对象和 Student 类的对象,并调用其中的 insertStudent(student) 方法。

public class MyTest {

	private IStudentDao dao;
	
	@Before
	public void before() {
		dao=new StudentDaoImpl();
	}
	
	@Test
	public void testInsert() {
		Student student = new Student("张三", 23, 93.5);
		dao.insertStudent(student);
	}
}

1.5.7 添加日志控制文件

log4j2.xml:新建该文件在 src 根目录下,并添加以下代码;本例的日志只需要在控制台输出,所以 <appenders/> 的子标签只有 <Console/> 标签。

<?xml version="1.0" encoding="UTF-8"?>

<configuration status="OFF">
	<appenders>
		<Console name="myConsole" target="SYSTEM_OUT">
			<PatternLayout pattern="[%-5p][%c %L] %m%n" />
		</Console>
	</appenders>
	
	<loggers>		
		<root level="debug">
			<appender-ref ref="myConsole" />
		</root>
	</loggers>
	
</configuration>

1.5.8 日志输出结果

1.6 使用工具类

​ 由于每一次执行 SqlSession 的方法,均需首先获取到该对象。而 SqlSession 对象的获取比较繁琐,所以可以将获取 SqlSession 对象定义为一个工具类方法。

​ SqlSession 对象是通过 SqlSessionFactory 对象创建的。由于 SqlSessionFactory 类为重量级对象,创建和销毁都极耗费时间,且为线程安全的,所以可以将 SqlSessionFactory 对象定义为单例的。

​ 线程安全的是指该对象的属性是不可以修改的,若有可修改的属性则线程不安全。

1.6.1 创建工具类

MyBatisUtils.java:因为 SqlSessionFactory 对象为单例的,所以将其设为类的静态成员变量,并在调用 getSqlSession() 方法时判断是否为空,若为空才进行创建。

public class MyBatisUtils {
	private static SqlSessionFactory sqlSessionFactory;

	public static SqlSession getSqlSession() {
		try {
			if (sqlSessionFactory==null) {
				InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
				sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return sqlSessionFactory.openSession();
	}
}

1.6.2 修改实现类

StudentDaoImpl.java:在实现类中直接调用工具类的静态方法获取 SqlSession 对象。

public class StudentDaoImpl implements IStudentDao {

	private SqlSession sqlSession;

	@Override
	public void insertStudent(Student student) {
		try {
			sqlSession=MyBatisUtils.getSqlSession();
			sqlSession.insert("insertStudent", student);
			
			sqlSession.commit();
		}finally {
			if(sqlSession!=null) {
				sqlSession.close();
			}
		}
	}
}

1.7 使用属性文件读取 DB 连接四要素

1.7.1 创建属性文件

​ 在 src 的根目录下创建 jdbc.properties 属性文件,并将 DB 连接四要素写入文件。需要注意的是要将 “ &amp; " 改为 ” & “。

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql:///test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
jdbc.user=root
jdbc.password=password

1.7.2 修改主配置文件

mybatis.xml

  • 第 7行,在 <setting/> 标签前加入 <properties/> 来注册配置文件;
  • 第 18 至 21 行,在 <property/> 标签里的 value 属性,需要使用 ” ${ } “ 占位符来引用配置文件。
<?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>
	<!-- 注册配置文件 -->
	<properties resource="jdbc.properties" />
	
	<settings>
		<setting name="logImpl" value="LOG4J2" />
	</settings>

	<environments default="development">
		<environment id="development">
			<transactionManager type="JDBC" />
			<dataSource type="POOLED">
				<!-- 使用占位符来引用jdbc配置文件 -->
				<property name="driver" value="${jdbc.driver}" />
				<property name="url" value="${jdbc.url}" />
				<property name="username" value="${jdbc.user}" />
				<property name="password" value="${jdbc.password}" />
			</dataSource>
		</environment>
	</environments>
	<mappers>
		<mapper resource="com/hahg/dao/mapper.xml" />
	</mappers>
</configuration>

1.8 源码分析

1.8.1 输入流的关闭

提示

在查看的源码时,Alt + ← / → 可以切换源代码界面;Ctrl + PgUp 可以切换标签页

SqlSessionFactoryBuilder.class:之前使用

sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream) 语句,

由下源码第 2 行可知,这个语句会调用 build( inputStream, null, null ) 三参数 方法。

public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
}

这时跟进其 bulid 方法源码查看其实现方式,在第 10 行代码自动关闭了输入流,所以在工具类使用输入流对象完毕后,不用手工进行关闭

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

1.8.2 SqlSession 的创建

​ 之前使用了 sqlSessionFactory.openSession(); 这个语句。

​ SqlSession 对象的创建,需要使用 SqlSessionFactory 接口对象的 openSession() 方法。而 SqlSessionFactory 接口的实现类为 DefaultSqlSessionFactory。

​ 这时跟进 DefaultSqlSessionFactory 源码。

DefaultSqlSessionFactory.class:下面为 openSession() 方法源码,可知在 openSession() 方法其调用了另一个 openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false) 方法。

public class DefaultSqlSessionFactory implements SqlSessionFactory {
    
  @Override
  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

跟进 openSessionFromDataSource 方法,发现其在同一个类,下面来分析源码:

  • 第 4 行获取 environment 环境变量;
  • 第 5 行根据 environment 变量来获取事务工厂对象 ;
  • 第 6 行利用事务工厂对象来新建事务;
  • 第 7 行使用配置文件对象来创建执行器对象,将来用于执行映射文件中的 SQL 语句;
  • 第 8 行使用【 configuration 配置对象、executor 执行器对象、和是否自动提交选择】这三个参数,创建了默认 SQL 事务对象,并将其返回。因为由上面代码可知的第三个形参传了 false 进来,所以没有自动提交,则需要自己调用 sqlSession.commit() 。
 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

1.8.3 增删改的执行

之前使用了 sqlSession.insert("insertStudent", student); 这个语句,对于 SqlSession 的

  • insert( String statement, Object parameter )
  • delete( String statement, Object parameter )
  • update( String statement ) 方法

其底层均是调用执行了 update( String statement, Object parameter ) 双参数方法。

跟进 update 双参数方法 第 19 ~ 28 行中,

  • 第 21 行将 dirty 这个变量设为 true;

  • 第 22 行中调用 configuration 对象的 getMappedStatement(statement) 方法,这个方法是根据形参即 映射文件中 Sql 的 id 名字 ,在映射文件中获取到了 sql 执行语句;

  • 并在第 23 行中使用 executor 执行器对象来执行更新语句。

DefaultSqlSession.class

public class DefaultSqlSession implements SqlSession {
    
  @Override
  public int insert(String statement, Object parameter) {
    return update(statement, parameter);
  }  
    
  @Override
  public int update(String statement) {
    return update(statement, null);
  }
    
  @Override
  public int delete(String statement, Object parameter) {
    return update(statement, parameter);
  }
    
  @Override
  public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
}

1.8.4 SqlSession 的提交

​ 在之前使用了 sqlSession.commit() 这个语句;

  • 根据第 3 ~ 5 行源码可知,本质就是调用了 commit(false) 这个方法。

  • 在第 10 行中需要跟进 isCommitOrRollbackRequired(force) 这个方法。这个方法的源码在 19 至 21 行。其返回了一个逻辑语句的布尔值。

    • 在 1.8.2 可知 autoCommit 是 false;所以 !autoCommit 是 true;
    • 根据 1.8.3 可知在执行插入语句后,dirty 是 true;
    • 所以该方法返回的是 true
  • 已知执行 executor.commit(true) 语句;

    • 继续跟进到 BaseExecutor.class 里,在该类代码的第 11 行可以看见事务的提交。
  • 在 11 行中,因为提交了事务,数据库数据同步了,所以 dirty 就改成了 false。

DefaultSqlSession.class

public class DefaultSqlSession implements SqlSession { 
  @Override
  public void commit() {
    commit(false);
  }

  @Override
  public void commit(boolean force) {
    try {
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
    
  private boolean isCommitOrRollbackRequired(boolean force) {
    return (!autoCommit && dirty) || force;
  }
}

BaseExecutor.class

public abstract class BaseExecutor implements Executor {
    
  @Override
  public void commit(boolean required) throws SQLException {
    if (closed) {
      throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    clearLocalCache();
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }
}

1.8.5 SqlSession 的关闭

​ 之前在 finally 块中使用 sqlSession.close() 这个语句,这个语句依旧是 DefaultSqlSession 类的一个方法。

  • 第 6 行,执行了 isCommitOrRollbackRequired(force) 这个方法
    • !autoCommit 根据上面可知是 true
    • 因为 1.8.4 执行了事务的提交并将 dirty 改成了 false,所以 (!autoCommit && dirty) 为 false
    • 根据第 6 行传进来的值为 false,所以 (!autoCommit && dirty) || force = false || false = false,最后结果为 false
  • 第 10 行为事务的关闭,第 17 ~ 21 行释放各种资源并将关闭标记置为 true。
public class DefaultSqlSession implements SqlSession {
    
  @Override
  public void close() {
    try {
      executor.close(isCommitOrRollbackRequired(false));
      closeCursors();
      dirty = false;
    } finally {
      ErrorContext.instance().reset();
    }
  }
    
   private boolean isCommitOrRollbackRequired(boolean force) {
    return (!autoCommit && dirty) || force;
  }
}

BaseExecutor.class:

  • 现在跟进第 6 行的 executor.close( isCommitOrRollbackRequired(false) )
    • 在 BaseExecutor 类的代码的第 7 行执行 rollback( forceRollback ) 方法,forceRollback 的值为 false。该方法代码在 25 ~ 37 行,在该方法的第 31 ~ 34 行的 finally 块中,因为传进来的参数为 false,即 required 为 false,所以不执行事务回滚。
    • 若没有 1.8.4 SqlSession 的提交,则 dirty 这个变量为 true;则 isCommitOrRollbackRequired(boolean force) 这个方法将返回 true,最后会执行事务回滚。
public abstract class BaseExecutor implements Executor {

  @Override
  public void close(boolean forceRollback) {
    try {
      try {
        rollback(forceRollback);
      } finally {
        if (transaction != null) {
          transaction.close();
        }
      }
    } catch (SQLException e) {
      // Ignore. There's nothing that can be done at this point.
      log.warn("Unexpected exception on closing transaction.  Cause: " + e);
    } finally {
      transaction = null;
      deferredLoads = null;
      localCache = null;
      localOutputParameterCache = null;
      closed = true;
    }
  }
    
  @Override
  public void rollback(boolean required) throws SQLException {
    if (!closed) {
      try {
        clearLocalCache();
        flushStatements(true);
      } finally {
        if (required) {
          transaction.rollback();
        }
      }
    }
  }
}

1.9 别名

​ 在一个映射文件一般对应数据库的一个表和一个 bean 对象。所以可以在使用缩写来表示,这样更简洁,不过需要修改配置文件。

<!-- 原始写法  -->
<insert id="insertStudent" parameterType="com.hahg.beans.Student">
    
<!-- 第一种改法 -->
<insert id="insertStudent" parameterType="Student">    
<!-- 第二种改法 -->
<insert id="insertStudent">    

​ 配置文件修改成以下代码,使用 <package/> 标签。使用该标签会将该包中所有实体类的简单类名指定为别名,写法简单方便。

<!-- 注册配置文件 -->
	<properties resource="jdbc.properties" />

	<settings>
		<setting name="logImpl" value="LOG4J2" />
	</settings>

<!-- 增加下面的代码 -->
	<typeAliases>
		<package name="com.hahg.beans" />
	</typeAliases>

​ 也可以通过 <typeAlias/> 标签一个个指定别名,好处是可以任意指定别名。坏处是必须逐个指定,比较繁琐。

<typeAliases>
		<typeAlias type="com.hahg.beans.Student" alias="Student" />
</typeAliases>