第二章 MyBatis的基础用法

第二章 MyBatis的基础用法
本章目录:

  • ORM框架与MyBatis的关系
  • 初步掌握MyBatis的用法(CRUD操作)
  • 掌握并理解MyBatis的Mapper
  • 使用MyBatipse插件开发MyBatis应用
  • 掌握并理解MyBatis核心API及作用域
  • 掌握并理解Mapper组件的作用域
  • MyBatis的三种属性配置方式及其优先级
  • 设置(settings)配置的方法及作用
  • 类型别名的配置方法
  • 配置对象工厂(Object Factory)及自定义对象工厂
  • 加载Mapper的4种方式
  • 掌握并理解MyBatis内置的类型处理器
  • 开发自定义的类型处理器
  • 使用类型处理器处理枚举
  • 数据库环境配置与事务管理器配置
  • 数据源配置及配置第三方数据源
  • 使用databaseIdProvider 自适应不同类型的数据库
  • select的用法
  • insert的用法
  • 使用useGeneratedKeys返回自增长主键的值
  • 使用selectKey生成主键
  • update和delete的用法
  • 使用sql元素定义可复用的SQL片段
  • Mapper管理的SQL中的参数处理
  • 为参数提供额外的声明
  • SQL中的字符串替换
  • 掌握MBG的功能和用法

前言

MyBatis是一个古老的框架,现在仍有很多项目使用MyBatis技术
MyBatis与普通ORM框架不同,MyBatis并不是真正的ORM框架,它只是一个半自动的SQL映射框架。从字面上看,MyBatis没有ORM框架的功能强大(因为是半自动的),但正是由于这种半自动,反而让MyBatis拥有更多的灵活性:①MyBatis需要开发者自己编写SQL语句,因此开发者可以充分的自由执行SQL优化,但是对开发者具有一定的SQL功底要求;②MyBatis需要开发者自己定义ResultSet与对象之间的映射关系,因此可以简单地避免循环引用等问题

2-1MyBatis是ORM框架吗

何谓ORM

ORM的全称是Object/Relation Mapping,即对象/关系映射。可以将ORM理解为一种规范:完成面向对象编程语言到关系数据库的映射,ORM将Java的面向对象机制语言转换到数据库中进行交互,可以将ORM理解为数据之间的桥梁
因为数据库是关系型数据库,而Java语言是面向对象语言,它们的访问方式不同,如果来回切换,开发体验会非常糟糕,于是需要一种工具,它可以把关系数据库包装成面向对象的模型,这个工具就是ORM
ORM框架是解决面向对象程序设计和关系数据库发展不同步的一个中间解决方案,随着面向对象数据库的发展,最终会取代关系数据库,随着面向对象数据库的广泛使用,ORM工具就会自动消亡

面向对象数据库优势

  • 面向对象的建模、操作
  • 多态、继承
  • 崛弃难以理解的过程
  • 简单易用,易理解

关系型数据库优势

  • 大量数据查找、排序
  • 集合数据的连接操作、映射
  • 数据库访问的并发、事务
  • 数据库的约束、隔离

面对这种面向对象的程序设计语言与关系型数据库系统并存的局面,采用ORM就变成一种必然。采用ORM框架之后,应用程序不再直接访问底层数据库,而是以面向对象的方式来操作持久化对象)(例如创建、修改、删除等),ORM框架则将这些面向对象的操作转换成底层的SQL(结构化查询语言)操作。
第二章 MyBatis的基础用法_第1张图片

ORM的映射方式

ORM全称是对象/关系 映射,ORM工具提供了持久化类和数据表之间的映射关系,通过这种映射关系的过度,程序员可以很方便地通过持久化实现对数据表的操作
ORM基本映射有如下几种映射关系

  • 数据表映射类:持久化类被映射到一个数据表。当程序使用这个持久化类来创建实例、修改属性、删除实例时,系统会自动转换为对这个表进行CRUD操作。
  • 数据表的行映射对象(即实例):持久化类会生成很多实例,每个实例都对应数据表的一行记录。当程序在应用中修改持久化类的某个实例时,ORM工具将会转换成对对应数据表中特定行的操作。每个持久化对象对应数据表的一行记录
  • 数据表的列(字段)映射对象的属性:当程序修改某个持久化对象的指定属性时(持久化实例映射到数据行),ORM将会转换成对对应数据表中指定数据行、指定列的操作

下面将通过三张图来演示这三个映射关系大意

数据表映射类

第二章 MyBatis的基础用法_第2张图片

数据表的行映射对象(即实例)

第二章 MyBatis的基础用法_第3张图片

数据表的列(字段)映射对象的属性

第二章 MyBatis的基础用法_第4张图片
也就是说,可以总结为如下关系

数据表 -> 持久化类
数据表的字段 -> 持久化类的对象属性
数据表的字段的记录 -> 持久化类的对象属性的值

数据表和Java持久化类互相映射
数据表的字段和Java持久化类的属性互相映射
数据表的记录和Java持久化类属性的变量值内容互相映射

ORM工具的最大意义就是让开发者用面向对象的思维去操作关系数据库

MyBatis的映射方式

严格来说,MyBatis并不是真正的ORM框架,它只是一个ResultSet映射框架
MyBatis与ORM框架不同,MyBatis并不会将表映射到持久化类。实际上,MyBatis完全没有“持久化类”的概念
MyBatis负责将JDBC查询得到的ResultSet映射成实体对象——同一条查询语句,在不同地方完全可以映射成不同的实体
下图可以看出,MyBatis先使用JDBC(默认使用PreparedStatement)执行查询,查询结果通常会返回一个ResultSet
ResultSet相当于一个包含多行、多列的数据表,ResultSet的每列都有列名,JDBC可通过ResultSetMetaData获取ResultSet包含的列信息,如各列的名称、类型等

开发者通过resultType或resultMap属性来指定转换Java对象
第二章 MyBatis的基础用法_第5张图片
从上面的介绍不难看出,即使是一个ResultSet,程序也完全可以通过不同的resultType或resultMap属性值让MyBatis将它转换成不同的Java对象——这就是MyBatis令人着迷的魅力
第二章 MyBatis的基础用法_第6张图片
虽然MyBatis很灵活,但是每次都需要烦琐的去设置sql语句和返回类型…否则MyBatis就会报错
在指定了resultType或resultMap属性之后,MyBatis就知道将每行记录转换为哪个Java类的实例了。接下来的问题是:MyBatis如何将各列的值传给Java对象的属性呢?MyBatis有两种方式

  • 列名(或列别名)和属性名相同:这种方式底层利用反射来进行赋值,ResultSet有几个列和Java对象的属性同名,MyBatis就为Java对象的几个属性设置值
  • 显式指定:不可避免地,ResultSet的列名并不能总是与Java对象的属性名保持相同,MyBatis提供了result元素(或@Result注解)来指定列名与对象属性之间的对应关系

使用MyBatis执行select查询才会返回ResultSet,此时才需要将ResultSet映射成Java对象。如果使用MyBatis执行增删改,则大致等同于使用PreparedStatement执行insert、update、delete语句,并不需要执行任何映射
通过上面的介绍不难发现,MyBatis的映射方式与ORM框架(如Hibernate)的映射方式完全不同,因此MyBatis并不是严格意义上的ORM框架。实际上,MYBatis官方也成MyBatis是基于SQL的数据映射工具(SQL based data mapping solution),反倒是国内有些初、中级开发者喜欢似是而非地将MyBatis称为ORM框架。
MyBatis的本质是封装了JDBC,使用JDBC的PreparedStatement(默认)进行增删改查,在查询时会有结果集返回,MyBatis通过开发者指定返回类型去决定结果集要映射的实例类型,而在属性方面如果属性名相同,则有多少相同映射多少,如果不同则需要通过result元素或@Result注解来指定列名和对象属性之间的对应关系,而就是说MyBatis并不是ORM框架,MyBatis是基于SQL的数据映射工具,SQLValue -> JavaValue

2-2MyBatis入门

下载完3.5.2的MyBatis后,看如下文件结构

  • lib:该路径下存放了编译和运行MyBatis所依赖的第三方类库
  • mybatis-3.5.2.jar:MyBatis的核心JAR包
  • mybatis-3.5.2.pdf:MyBatis的官方手册,这是学习MyBatis的基础文档。本书的内容比MyBatis官方文档更丰富
  • LICENSE、NOTICE等说明性文档

MyBatis底层依然是基于JDBC的,因此在应用程序中使用MyBatis执行持久化时同样少不了JDBC驱动

MyBatis的数据库操作

前面已经说了,MyBatis默认使用PreparedStatement来执行SQQL语句,因此,MyBatis同样需要先获取数据库连接(Connection)。但如果MyBatis还需要让程序自己去获取数据库连接(虽然MyBatis允许这么做),那MyBatis就算不上一个框架了
与所有持久层框架类似,MyBatis也采用了XML文件来配置必要的数据库连接信息。下面是本例所用的MyBatis配置文件
mybatis-config


DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	
  <environments default="mysql">
  	
    <environment id="mysql">
    
      <transactionManager type="JDBC"/>
      
      <dataSource type="POOLED">
      
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?serverTimezone=UTC"/>
        <property name="username" value="root"/>
        <property name="password" value="password"/>
      dataSource>
    environment>
  environments>
  <mappers>
  
    <mapper resource=""/>
  mappers>
configuration>

上面的配置文件大致可分为两部分,这两部分也是MyBatis核心配置文件的主要部分

  • environments:这部分主要配置MyBatis的数据库环境。数据库环境就是前面所说的连接数据库的驱动、URL、用户名、密码以及事务处理方式等。MyBatis允许配置多个数据库环境,从而保证MyBatis应用可以在不同的数据库之间切换

上面的配置文件使用元素配置了一个名为mysql的数据库环境,该元素中的用于配置事务管理器;而元素则用于配置数据源,该元素中的4个元素的作用很清晰,就是指定连接数据库的驱动、URL、用户名和密码,如配置文件中的四行代码所示
第二章 MyBatis的基础用法_第7张图片
MyBatis提供了内置的数据源来管理数据库连接,因此可以无须第三方数据源。当然,MyBatis也支持切换使用第三方更优秀的数据源(比如C3P0等),后面会介绍使用第三方数据源的示例

数据源是一种提高数据库连接性能的常规手段,数据源会负责维持一个数据连接池,当程序创建数据源实例时,系统会一次性地创建多个数据库连接,并把这些数据库连接保存在连接池中。当程序需要进行数据库访问时,无须重新获取数据库连接,而是从连接池中取出一个空闲的数据库连接。当程序使用数据库连接访问数据库结束后,无须关闭数据库连接,而是将数据库连接归还给连接池即可。通过这种方式,就可避免因频繁获取数据库连接、关闭数据库连接所导致的性能下降

  • mappers:该元素用于配置MyBatis需要加载的Mapper(映射器)

Mapper是MyBatis最核心的东西,Mapper负责管理MyBatis要执行的SQL语句,也负责将ResultSet映射成对象。虽然有些地方会将Mapper翻译成映射器,但实际上Mapper覆盖的范围比映射器更广——Mapper其实相当于Java EE应用的DAO(Data Access Object)组件
MyBatis的核心配置文件,主要就是配置两个东西:数据库环境(包括数据库信息和事务配置)和加载Mapper
接下来看本例的Mapper文件



DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
	<mapper namespace="mybatis-demo/src/main/resources/NewsMapper">
		
		<insert id="savaNews">
			insert into news_inf valuse(null,#{title},#{content})
		insert>
mapper>

这个Mapper文件更是简单,该文件的根元素是,该元素需要指定一个namespace属性,建议将该属性值指定为该文件所在的包+文件名。namespace属性值指定了该Mapper的命名空间——相当于该Mapper的唯一标识
元素内包含了一个元素,这个元素的作用更简单:定义insert SQL语句,并通过id属性指定这条insert语句的名称为saveNews
上面Mapper文件中定义的insert语句与标准的insert SQL语句略有差异:

insert into news_inf values(null,#{title},#{content})

其差异就体现在#{title}、#{content}这两个“像占位符”一样的东西上。这到底是什么意思呢?别忘了MyBatis默认是使用PreparedStatement来执行SQL语句的,因此MyBatis会将SQL语句中的#{}部分都喜欢成问号(?),这意味着上面的SQL语句就变成了以下形式

insert into news_inf values(null,?,?)

这条SQL语句是通过JDBC Connection创建PreparedStatement时传入参数的
由于创建PreparedStatement时传入的SQL语句中有两个问号(?),PreparedStatement自然就需要为这两个问号设置参数值——现在这个过程由MyBatis来完成,MyBatis会将#{title}的值传给第一个问号,将#{content}的值传给第二个问号
那MyBaatis又如何计算#{title}、#{content}的值呢?MyBatis会基于OGNL表达式来计算#{title}、#{content}的值,不要被OGNL这个名称吓着了,其实MyBatis只用了OGNL的部分规则,因此比较简单
简单来说,MyBatis计算#{title}、#{content}的值有两种方式

  • 命名参数:MyBatis执行SQL语句时传入title、content两个命名参数(命名参数必须使用@Param注解或利用Java8中命名参数的特性),title就作为#{title}的值,content就作为#{content}的值…以此类推
  • 唯一参数:MyBatis执行SQL语句时只传入一个参数,要么该参数是一个Java对象,此时该Java对象的title属性值作为#{title}的值,content属性值作为#{content}的值…依次类推;要么该参数是一个Map对象,此时该Map对象中title对应的value将作为#{title}的值,content对应的value将作为#{content}的值…依次类推

MyBatis并不支持自动建表,因此需要手动创建数据表:

create database mybatis;
use mybatis;
#创建数据表
create table news_inf
(
news_id integer primary key auto_increment,
news_title varchar(255),
news_content varchar(255)
);

现在已为MyBatis提供了两个配置文件

  • mybatis-config.xml文件:核心配置文件,该文件配置了数据库连接环境,并告诉MyBatis要加载哪些Mapper文件
  • XML Mapper文件:该文件负责管理SQL语句,并未SQL语句指定名称

MyBatis底层依然使用JDBC访问数据库,因此同样少不了数据库驱动。此外,为了方便观察MyBatis底层执行的每条SQL语句及参数(这对于MyBatis学习有帮助),本书使用Log4j 2作为MyBatis的日志工具,因此还需要添加Log4j 2的两个JAR包
第二章 MyBatis的基础用法_第8张图片
为了让Log4j 2能正确地显示MyBatis的运行日志,还需要为Log4j 2提供如下配置文件


<Configuration status="WARN">
	<Appenders>
		<Console name="Console" target="SYSTEM_OUT">
			<PatternLayout pattern="%5p [%t] %logger{36} %m%n"/>
		Console>
	Appenders>
	<Loggers>
		
		<Root level="error">
			<AppenderRef ref="Console"/>
		Root>
		
		<Logger name="org.crazyit.app.dao" level="debug"/>
	Loggers>
Configuration>

该日志配置文件中的指定将日志输出到哪些IO节点,此处只配置了将日志输出到控制台;元素用于定义日志的输出级别,指定MyBatis会输出org.razyit.app.dao包下debug级别的运行日志
接下来使用MyBatis来执行Mapper中定义的SQL语句

package lee;

import java.io.InputStream;
import java.util.HashMap;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.logging.log4j2.Log4j2Impl;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class NewsManager {
	private static SqlSessionFactory sqlSessionFactory;
	public static void main(String[] args) throws Exception{
		String resource = "mybatis-config.xml";//文件的路径
		//使用Resources工具从类加载路径下加载指定文件
		InputStream inputStream = Resources.getResourceAsStream(resource);
		//构建SqlSeesionFactory
		sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
		//打开Session
		var sqlSeesion = sqlSessionFactory.openSession();
		insertTest(sqlSeesion);
	}
	public static void insertTest(SqlSession sqlSession) {
		//创建消息实例
		HashMap news = new HashMap<String,String>();
		//设置消息标题和消息内容
		news.put("title","日志测试");
		news.put("content","测试log4j2");
		//调用insert方法执行SQL语句
		int n = sqlSession.insert("org.crazyit.app.dao.NewsMapper.savaNews",news);
		System.out.printf("插入了%d条数据%n",n);
		//提交事务
		sqlSession.commit();
		//关闭资源
		sqlSession.close();
	}
}

上面的持久化操作代码非常简单,程序只需要调用SqlSeesion的insert()方法来执行SQL语句。该insert()方法需要两个参数

  • 第一个参数是它要执行的SQL语句。此处第一个参数为org.crazyit.app.dao.NewsMapper.saveNews,其中org.crazyit.app.dao.NewsMapper是NewsMapper.xml的唯一标识(namespace),saveNews是元素的id属性值。通过org.crazyit.app.dao.NewsMapper.saveNews参数,即可让MyBatis定位到要执行的SQL语句——对应于位于NewsMapper.xml文件中、id为saveNews的insert语句
  • 第二个参数用于为SQL语句传入参数。正如前面所介绍的,saveNews对应的insert语句需要计算#{title}、#{content}的值,故此出第二个参数传入一个Map对象,该Map包含了title、content两个key对应的value,这两个value将会作为#{title}、#{content}的值

可见,SqlSession的insert()方法其实就是执行一条insert SQL语句——MyBatis的insert()方法在底层封装了下面的过程:
一、获取Connection对象(数据库连接)
二、根据insert()方法的第一个参数指定的SQL语句(现将#{}部分替换成问号)创建PreparedStatement
三、根据insert()方法的第二个参数为PreparedStatement设置参数
四、调用PreparedStatement的execute()方法执行SQL语句
这个过程就是MyBatis底层的源码实现,其实实现也非常简单,但对于开发者来说,使用MyBatis操作数据库就变得更简洁了,开发者只要关心两点

  • 定义合适的SQL语句
  • 为SQL语句中的问号占位符(替换#{}而来)指定值

至于如何调用JDBC API来执行SQL语句、返回结果值,这些都交给MyBatis负责搞定

MyBatis底层执行的SQL语句

insert into news_inf values(null,?,?)

这就是在NewsMapper中定义的SQL语句,只是将#{}部分替换成了问号

从上面执行过程可以看出,MyBatis并不是真正的ORM框架,MyBatis只是对JDBC的简单封装。对比MyBatis和JDBC操作数据库的方式,不难发现这种封装至少带来了两种优势

  • 程序只要调用SqlSeesion的方法即可完成数据库操作,避免了繁琐的JDBC API调用
  • MyBatis使用Mapper集中管理SQL语句,这样可以将SQL语法从Java代码中分离出阿里,提高了项目的可维护性

正如前面的示例所示,MyBatis操作数据库的核心API是SqlSeesion,因此在调用MyBatis执行持久化操作之前,必须先获取SqlSeesion。
为了使用MyBatis进行持久化操作,通常有如下操作步骤

①使用Mapper定义SQL语句
②创建SqlSeesionFactoryBuilder
③调用SqlSessionFactoryBuilder对象的build()方法创建SqlSeesionFactory
④获取SqlSeesion(同时会打开事务)
⑤调用SqlSeesion的方法执行Mapper中的SQL语句(或通过Mapper对象来操作数据库)
⑥关闭事务,关闭SqlSession

使用MyBatis执行CRUD

如果需要对数据表中的记录进行更新,同样只需按上面步骤进行。上面步骤可以简化为两个核心步骤:
①在Mapper中定义SQL语句
②使用SqlSeesion执行SQL语句
如果要对数据表中的记录进行更新,则需要先在Mapper中定义如下SQL语句


		<update id="updateNews">
			update news_inf set news_title = #{title},news_content=#{content}
			where news_id=#{id}
		update>

其实Mapper文件中的还有后面的元素的功能差不多,只是处于可读性考虑,才建议用定义insert语句,用定义update语句,用定义delete语句。事实上,你完全可以用定义delete语句,这样元素实际指向时会删除记录

接下来在NewsManager中使用如下方法来调用updateNews对应的SQL语句

public static void updateTest(SqlSession sqlSeesion) {
		//创建消息实例
		var news = new HashMap<String,String>();
		//设置消息标题和消息内容
		news.put("id","1");
		news.put("title","很奇怪的bug?");
		news.put("content","log日志并没有输出出来");
		//调用update方法执行SQL语句
		var n = sqlSeesion.update("org.crazyit.app.dao.NewsMapper.updateNews",news);
		System.out.printf("更新了%d条数据%n", n);
		//提交事务
		sqlSeesion.commit();
		//关闭资源
		sqlSeesion.close();
	}

该方法与前面的insertTest()没什么区别,只是改成了使用SqlSession的update()方法执行Mapper中定义的update语句

如果还记得前面讲解的关于MyBatis底层原理的话,其实此处使用SqlSession同样可用insert()方法,甚至delete()方法来执行Mapper中定义的update语句——因为MyBatis底层用的是PreparedStatement,而PreparedStatement执行DML语句都用execute()方法

重复上面先定义SQL语句,再执行SQL语句的步骤,即可为该示例添加删除记录的方法,同样是先在Mapper中定义如下SQL语句


<delete id="delteNews">
			delete from news_inf 
			where news_id = #{id}
		delete>
		<insert id="insertButDeleteNews">
			delete from news_inf
			where news_id = #{id}
		insert>

上面代码使用元素定义了一条delete语句——这影响MyBatis的功能吗?当然不影响,MyBatis只管帮你执行你定义的SQL语句

接下来在NewManager中去时候用如下方法来调用deleteNews对应的SQL语句

	public static void deleteTest1(SqlSession sqlSeesion) {
		//故意调用insert方法执行delete SQL语句
		//证明SqlSession的insert、update、delete方法的功能几乎相同
		var n = sqlSeesion.insert("org.crazyit.app.dao.NewsMapper.delteNews",21);
		System.out.printf("删除了%d条数据%n", n);
		//提交事务
		sqlSeesion.commit();
		//关闭资源
		sqlSeesion.close();
	}
	public static void deleteTest2(SqlSession sqlSeesion) {
		//故意调用insert方法执行delete SQL语句
		//证明SqlSession的insert、update、delete方法的功能几乎相同
		var n = sqlSeesion.delete("org.crazyit.app.dao.NewsMapper.insertButDeleteNews",2);
		System.out.printf("删除了%d条数据%n", n);
		//提交事务
		sqlSeesion.commit();
		//关闭资源
		sqlSeesion.close();
	}

不过貌似一次只能执行一个方法,因为提交了事务和关闭了资源,不过将第一个的sqlSession.close()注释掉就可以同时运行两个方法
上面仍然成功运行了,并没有影响到MyBatis的功能,MyBatis的底层就是使用PreparedStatement来执行Mapper中定义的SQL语句的
此处通过insert()方法给SQL语句只传入唯一的参数:1,此时MyBatis如何处理SQL语句中的#{id}呢?如果传给SQL语句的参数是唯一参数,且该属性是标量类型(String、8个基本类型或其包装类),MyBatis会直接将这个参数本身传给SQL语句中的#{id},而且花括号中的名字可以随便写
也就是说,当SQL语句中只有一个#{}占位符,且程序给SQL语句传入唯一的、标量类型的参数时,花括号的名字可以随便写。这意味着将上面的元素改为如下形式也是可以的

<insert id="deleteNews">
	delete from news_inf
	where news_id=#{abc}
insert>

CRUD流程式总结

我们想进行上面的完整操作需要先创建一个项目,并且按照书上的包创建出来,然后添加4个JAR包,MyBatis、JDBC、Log4j2
不过本次不知道什么原因,Log4j2日志并没有效果
在部署完环境开发后,首先创建MyBatis的核心配置文件-mybatis-config,该文件有2个大内容,数据源配置和映射文件
第二章 MyBatis的基础用法_第9张图片
其次再创建Mapper映射文件,该文件是MyBatis的核心,通过该文件去管理SQL语句,达到SQL从程序中分离出来,该文件首先是创建一个mapper,在命名空间中输入名称,该名称用来配合标签元素id来达到唯一标识的效果,在调用MyBatis的API时,通过命名空间+元素id,来找到该标签的SQL语句,并且填充占位符参数,来达到执行的效果
第二章 MyBatis的基础用法_第10张图片
因为此处我的日志并没有效果,所以现在暂时不在赘述
第二章 MyBatis的基础用法_第11张图片
在测试类中,我们首先创建成员变量
sqlSession对象
然后在主方法中进行获取sqlSession对象的操作
创建sqlSessionFactoryBuilder和sqlSessionFactory对象
首先通过Resources.getResourceAsStream(“mybatis-config.xml”);来获取我们核心配置文件的输入流
其次通过匿名对象new SqlSessionFactoryBuilder().build(is);来加载并获取到sqlSessionFactory对象实例
我们再通过sqlSessionFactory的openSession来实例化sqlSession对象
本书通过形参的方法,将实际执行方法都抽到了方法中
public static void insertTest(SqlSession sqlSession){}
在插入方法中,首先创建一个HashMap数据,在MyBatis的赋值规则中,传入Map对象的话,Map对象的value将作为值,所以在Map中我们声明和占位符{#title}
hashMap.put(“title”,“标题”)
在实际运行中,就会将title对应的值映射{#title}上,在完成占位符的填充后,我们调用sqlSession的insert()…等等方法,首先传入Mapper文件的命名空间+id来唯一标识该SQL语句配置,然后再后面传入hashMap对象作为运行参数
增删改的返回值是int,表示着影响了几行的意思
我们获取返回值,然后通过printf来输出影响行数
由于MyBatis底层使用的是PreparedStatement运行的语句,但是PreparedStatement对于增删改都可以用execute()方法,所以此处尽管是使用update标签或方法来调用增删改的任意一个,仍然不会影响MyBatis的功能
在完成数据库操作后,一定要提交事务,因为MyBatis会自动打开事务,提交事务后并关闭资源
sqlSession.commit();sqlSession.close();

使用MyBatis查询数据

如果使用MyBatis查询数据,同样还是执行上面两步,先在Mapper中定义查询的SQL语句


		<select id="getNews" resultType="map">
			select*from news_inf where news_id = #{id}
		select>

元素用于定义select语句,由于select语句要返回ResultSet,因此程序需要告诉MyBatis将ResultSet的每行记录映射成哪个Java对象。这需要通过的resultType或resultMap属性来指定,此处将resultType指定为map(map是MyBatis的内置别名,代表Map接口),就是告诉MyBatis将每行记录映射成Map
下面程序将会调用SqlSession的selectOne()方法来执行该select语句,该方法的代码如下

public static void selectTest(SqlSession sqlSession) {
		//调用selectOne方法执行select SQL语句
		Map map  = sqlSession.selectOne("org.crazyit.app.dao.NewsMapper.getNews",7 );
		System.out.println("查询到的记录为:"+map);
		//提交事务
		sqlSession.commit();
		//关闭资源
		sqlSession.close();
	}

第二章 MyBatis的基础用法_第12张图片
SqlSession提供了selectOne()、selectList()两个常用的方法来执行查询,正如它们的名称所暗示的,当select语句查询结果集只有一行时,使用selectOne()来获取查询结果会更方便(不需要处理集合);当select语句查询的结果集可能多余一行(包括一行)时,则建议使用selectList()来获取查询结果,此时MyBatis会将每行记录封装成一个Java对象,再将多个Java对象封装成List集合后返回。
与执行DML语句略有不同的是,MyBatis执行select语句会返回ResultSet,当MyBatis拿到ResultSet之后,MyBatis接下来需要执行ResultSet映射
MyBatis处理映射的底层过程如下:

①开发者需要在XxxMapper.xml中为元素指定resultType或resultMap属性。其中,resultType指定ResultSet的每行记录要映射的类型;resultMap对应一个元素,该元素的type元素同样指定了ResultSet的每行记录要映射的类型——只不过resultMap支持更具体的映射——它可指定ResultSet的列名和Java对象属性名的对应关系
②MyBatis通过反射为结果集的每行记录创建一个resultType(或resultMap)所指定的Java对象
③MyBatis根据列名(列别名)与Java对象属性名的对应关系将每行记录的数据设置为Java对象的属性
④整个ResultSet被依次转换成多个Java对象,多个Java对象被封装成List集合后返回。
运行上面的程序(保证news_inf表中id拥有)

利用Mapper对象

前面的程序都是利用SqlSession的方法来执行SQL语句的,这些例子虽然上手简单,但它们至少存在如下两点不同:

  • 对数据库的CRUD操作都是通过SqlSession的方法来执行的,这种方式的代码不够安全(引用SQL语句时命名空间和id都可能写错),也不能利用Java的类型检查(编译器无法检查selectOne()方法的返回值类型)
  • ResultSet只能被映射成Map,而不是更有业务意义的领域对象

接下来,对上一个实例针对这两点进行改进。首先为项目中的每一个领域对象都创建对应的类,而不是使用Map来处理它们。因此,本例应该增加一个News类,该类的代码如下。

public class News {
	private Integer id;
	private String title;
	private String content;
	public News() {
		
	}
	public News(Integer id,String title,String content) {
		this.id=id;
		this.title=title;
		this.content=content;
	}
	//省略各成员变量的getter、setter方法
	...
}	

上面News类的代码也很简单,该类只需为数据表中的各列定义相应的成员变量,并为各成员变量定义getter、setter方法即可。有了该News类之后,接下来程序就无须将ResultSet的记录映射成Map了,而是映射成News

接下来对Mapper进行改进,改进后的Mapper不再是简单地管理SQL语句,而是会编程DAO组件。这种改进非常简单,开发者只需为Mapper增加一个接口即可。建议该接口遵循如下约定:

  • Mapper接口的接口名应该与对应的XML文件同名
  • Mapper接口的源文件应该与对应的XML文件放在相同的包下
  • Mapper接口中抽象方法的方法名与XML Mapper中SQL语句的id相同

现在为上一个示例的Mapper增加如下接口

//定义Mapper接口(DAO)接口,该接口由MyBatis负责提供实现类
public interface NewsMapper {
	//下面这些方法的方法名必须与NewsMapper.xml文件中SQL语句的id对应
	//下面这些方法的参数需要与NewsMapper.xml文件中SQL语句的参数对应
	int saveNews(News news);
	
	int updateNews(News news);
	
	int deleteNews(int a);
	
	News getNews(int a);

}

开发者只需要定义Mapper接口,无须为该接口提供实现类,MyBatis会负责为这些Mapper接口提供实现类,
留意到上面NewsMapper接口中getNews()方法的返回值是News,这表明还需要对上一个示例的NewsMapp,xml文件中id为getNews的SQL语句的resultType略做修改,修改后的元素如下


		<select id="getNews" resultType="org.crazyit.app/domain.News">
			select*from news_inf news_id id,news_title title,news_content content
			from news_inf where news_id = #{id}
		select>

先将resultType由原来的map改为News,这意味着MyBatis会将ResultSet的每行记录映射成News对象
接下来看到元素中的select语句也发生了改变,主要是为news_id列指定别名id,为news_title指定别名title,为news_content指定别名content。为什么这么做呢?这是由MyBatis的映射方式决定的——MyBatis默认根据列名(列别名)和属性名的对应关系来完成映射。News对象包含的三个属性分别为id、title、content,因此需要将查询的ResultSet的三列重命名为id、title、content
定义完上面的Mapper组件之后,接下来程序即可通过Mapper组件来操作底层数据库了。下面是本例操作数据库的代码

public class NewsManager{
	public static SqlSession sqlSession;
	public static void main(String[] args)throws Exception {
		String path = "mybatis-config.xml";
		InputStream is = Resources.getResourceAsStream(path);
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
		sqlSession = sqlSessionFactory.openSession();
//		insertTest(sqlSession);
//		updateTest(sqlSession);
//		deleteTest(sqlSession);
		selectTest(sqlSession);
	}
	public static void insertTest(SqlSession sqlSession){
		//创建消息实例
		var news = new News();
		//设置消息标题和消息内容
		news.setTitle("长安汽车");
		news.setContent("长安UNI-V");
		//获取Mapper对象
		var newsMapper = sqlSession.getMapper(NewsMapper.class);
		//调用Mapper对象的方法执行持久化操作
		var n = newsMapper.saveNews(news);
		System.out.printf("插入了%d条数据%n",n);
		//提交事务
		sqlSession.commit();
		//关闭资源
		sqlSession.close();
	}
	public static void updateTest(SqlSession sqlSession) {
		//创建消息实例
		var news = new News();
		//设置消息标题和消息内容
		news.setId(39);
		news.setTitle("13.99W落地");
		news.setContent("电动尾翼+2.0T");
		//获取Mapper对象
		var newsMapper = sqlSession.getMapper(NewsMapper.class);
		//调用Mapper对象的方法执行持久化操作
		var n = newsMapper.updateNews(news);
		System.out.printf("更新了%d条数据%n",n);
		//提交事务
		sqlSession.commit();
		//关闭资源
		sqlSession.close();
	}
	public static void deleteTest(SqlSession sqlSession) {
		//获取Mapper对象
		var newsMapper = sqlSession.getMapper(NewsMapper.class);
		//调用Mapper对象的方法执行持久化操作
		var n = newsMapper.deleteNews(3);
		System.out.printf("删除了%d条数据%n",n);
		//提交事务
		sqlSession.commit();
		//关闭资源
		sqlSession.close();
	}
	public static void selectTest(SqlSession sqlSession) {
		//获取Mapper对象
		var newsMapper = sqlSession.getMapper(NewsMapper.class);
		//调用Mapper对象的方法执行持久化操作
		var news = newsMapper.getNews(38);
		System.out.println("查询得到的记录为:"+news);
		//提交事务
		sqlSession.commit();
		//关闭事务
		sqlSession.close();
	}
}

上面程序中4个方法都有一行

var newsMapper = sqlSession.getMapper(NewsMapper.class);

用于获取Mapper组件(相当于DAO组件),下一行则是调用该接口的概念实现类来运行接口的方法,调用Mapper组件的方法来操作数据库。上面程序中后4个方法的测试效果与上一个示例效果相同,此处不再赘述
对于这种采用Mapper操作数据库的方式,最让初学者苦恼的地方就是:为何这些Mapper组件只有接口也能使用?难道接口能创建对象吗?
很明显,接口不能创建对象,只有实现类才能创建对象,这是Java的语法规定中。那么,SqlSession的getMapper()方法返回的Mapper组件是什么呢?
在上面程序中后4个方法中添加如下一行代码,打印Mapper组件的实现类:

System.out.println(newsMapper.getClass());

运行上面代码,可以看到如下输出

class com.sun.proxy.$Proxy0

这说明Mapper组件确实有自己的实现类,而且这个实现类不需要开发者提供实现,而是MyBatis自动提供实现,MyBatis通过反射生成动态代理类来实现操作
JDK的动态代理可以为借口动态生成实现类,这个实现类的class文件在内存中

由此可见,开发者只需要为Mapper组件定义接口,MyBatis最神奇的地方就是通过反射为Mapper组件生成动态代理,那么MyBatis是怎么知道如何实现这些抽象方法的呢?
MyBatis对这些方法的实现非常简单,每个Mapper组件都持有一个SqlSession对象,Mapper组件的方法主要就是以下伪码:

Class clz = 获取Mapper接口的类;
String statementId = clz.getName() + 当前方法名;
//执行不同元素定义的SQL语句用对应的方法,如执行元素定义的SQL语句用insert()方法
return sqlSession.xxx(statementId,参数);

使用Mapper方法(MyBatis推荐这种方式)操作数据库时,MyBatis在SqlSession上又包装了一层,这使得开发者能以更面向对象的方式来操作数据库,而且代码更加安全
由于Mapper组件直接就能充当DAO组件,这一点比Hibernate等ORM框架更为方便——Hibernate等ORM框架完成持久化类与数据表的映射之后,还需要另外开发DAO组件,但如果使用MyBatis,Mapper组件开发完成之后(而且只需要定义接口),就已经完成了DAO组件的开发,这一点是MyBatis的优势
使用Mapper组件操作数据库,使用领域对象(而不是Map)映射ResultSet的方式,才是MyBatis推荐的方式。采用这种方式操作数据库的核心步骤如下:
①根据查询结果定义Java类(充当领域对象)
②开发Mapper组件,关于Mapper组件有如下公式
Mapper组件=Mapper接口+XML文档(或注释)
③调用SqlSession的getMapper()方法获取Mapper组件
④调用Mapper组件的方法操作数据库

在Eclipse中使用MyBatis

正如前面所提到的,在使用任何IDE工具辅助开发之前,开发者都应该很清楚不使用工具如何使用该技术。IDE工具1仅用于辅助开发,提供开发效率,绝对无法弥补开发者的知识缺陷。

MyBatis为Eclipse提供了一个MyBatipse插件,该插件为XML Mapper编辑器提供了如下常用工具

  • 自动完成:与大部分插件类似,MyBatipse插件为命名空间、映射类型、属性、SQL语句ID等提供了自动完成功能,安装该插件后,按下“Alt+/”快捷键即可体验到该功能。
  • 有效性验证:MyBatipse插件可对类型别名、属性名、SQL语句ID等有效性进行检查,如果发现XML Mapper中引用的类型、属性名、SQL语句ID有错,该插件会立即报错
  • Mapper声明视图:该视图能让开发者以更直观的方式查看XMLMapper文件的关键内容

MyBatipse也为Java编辑器提供了如下常用功能

  • 自动完成:MyBatipse为Java编辑器提供了大量与MyBatis相关的自动完成功能,安装该插件后,按下“Alt+/”快捷键即可体验到该功能
  • 快速辅助:该功能能让开发者更方便地使用映射注解

前面介绍的例子都是使用XML Mapper定义SQL语句、映射关系的,实际上MyBatis同样允许不使用XML Mapper,而是直接在Mapper接口中使用注解来定义SQL语句、映射关系,本书后面的示例都会同时使用XML Mapper和注解两种方式

  • 有效性验证:MyBatipse插件也可对映射注解中 resultMap ID等属性进行验证。

此外,MyBatipse插件元允许使用向导式的方式来创建XML Mapper,也允许在XML Mapper或Java编辑器面对MyBatis元素进行重命名,熟悉这些功能可以加速MyBatis的开发

为Eclipse安装MyBatipse插件

第二章 MyBatis的基础用法_第13张图片
第二章 MyBatis的基础用法_第14张图片
第二章 MyBatis的基础用法_第15张图片
第二章 MyBatis的基础用法_第16张图片
第二章 MyBatis的基础用法_第17张图片
第二章 MyBatis的基础用法_第18张图片
第二章 MyBatis的基础用法_第19张图片
第二章 MyBatis的基础用法_第20张图片
第二章 MyBatis的基础用法_第21张图片
第二章 MyBatis的基础用法_第22张图片
重复上述操作将MyBatis、Log4j_2和MySQL添加上
完成效果
第二章 MyBatis的基础用法_第23张图片
刚才所添加的Eclipse用户库只能保证在Eclipse下编译、运行该程序可找到相应的类库;如果需要发布该应用,则还要将刚刚添加的用户库所引用的JAR文件岁应用一起发布。对于一个Web应用,由于在Eclipse中部署Web应用时不会将用户库的JAR文件复制到WEB-INF/lib路径下,因此需要主动将用户库所引用的JAR文件复制到Web应用的WEB-INF/lib路径下

第二章 MyBatis的基础用法_第24张图片
第二章 MyBatis的基础用法_第25张图片
将该文件放到src下,并将上一个项目的核心配置文件内容复制来,此时就完成了一个MyBatis项目的创建
至此,已经陈宫使用Eclipse创建了一个MyBatis项目,接下来既可按①定义实体类②开发Mapper组件③使用Mapper组件操作数据库的步骤来操作数据库了。具体的开发步骤如下:
①右键点击“MyBatisDemo”节点下的“src”子节点,在弹出的快捷菜单中单击“New”->“Class”菜单项,创建一个位于org.crazyit.app.domain包下的News类,该类的代码与前面实例的News类的代码完全一样
②右键单击“MyBatisDemo”节点下的“src”子节点,在弹出的快捷键菜单中“New”->“Interface”菜单项,创建一个位于org.crazyit.app.dao包下的NewsMapper接口,该接口代码与前面示例的NewsMapper接口的代码完全相同
③右键点击“MyBatisDemo”节点下的“src”子节点,在弹出的快捷菜单中单击“New” -> “Others…”菜单项,将出现新建项目或文件对话框,单击该对话框中的“MyBatis”节点(这就是MyBatipse创建发挥作用了),并选中该节点下的“MyBatis XML Mapper”子节点,然后单击“Next”按钮,将出现下图所示的对话框
第二章 MyBatis的基础用法_第26张图片
④为XML Mapper选择保存位置,输入文件名之后,单击“Finish”按钮,即可成功创建XML Mapper文件。接下来使用Eclipse编译该文件时,可随时通过“Alt+/”快捷键调出MyBatipse插件的辅助功能,例如,在根元素内输入小于号,按下“Alt+/”快捷键,即可看到如下图的所示提示
第二章 MyBatis的基础用法_第27张图片
从上图中可以看出,MyBatipse提示了XML Mapper允许在元素内添加的所有子元素,例如前面见过的等元素都被列出来了,开发者根据需要进行选择即可
加入选择定义元素,MyBatipse就会自动补齐元素的核心部分,当为元素输入id属性时,按下“Alt+/”快捷键,即可看到如下图所示的提示
第二章 MyBatis的基础用法_第28张图片
从上图可以看出,MyBatipse列出了元素的所有有效的id属性值——这些属性值就是对应Mapper接口中的方法名——前面已经说过,Mapper接口中的方法名必须与SQL声明中的id属性值对应
总之,读者随时可通过“Alt+/”快捷键调出MyBatipse的辅助功能,这样在某种程序度上提高开发效率。但最终编辑得到的NewsMapper.xml文件的内容依然与上一个示例完全相同,
这里还是要忠告开发者:不要过度依赖MyBatipse的提示功能!毕竟开发人员利用工具,不是工具利用开发人员。真正熟练的开发人员其实基本不靠这种插件提示——这样效率太低了!它们通常都是先赋值之前定义的内容,然后根据需要进行修改的
开发玩Mapper组件(Mapper+XML Mapper)后,接下来既可调用Mapper组件的方法来操作数据库了。这些代码很简单,此处不再赘述

2-3MyBatis核心API及作用域

从前面的程序中可以看到,在实现了Mapper组件之后,主程序要用到SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession等核心API。下面详细介绍这些核心API

SqlSessionFactoryBuilder的作用域

SqlSessionFactoryBuilder甚至算不上核心API,从它的源码来看,它甚至没有专门定义构造器(Java会提供默认的、无参的构造器),它主要提供了如下方法

SqlSessionFactory build(Reader|InputStream reader,String environment,Properties properties)

实际上,该方法有多个重载版本。其中,第一个reader参数即可使用字符流,也可使用字节流(本质是一样的),都指定告诉SqlSessionFactoryBuilder到哪里去加载MyBatis核心配置文件
第二个environment参数引用了MyBatis配置文件中元素的id属性——也就是指定使用MyBatis配置文件中哪个元素的数据库配置。在重载的build()方法中,可以不指定environment参数,如果不指定该参数,则使用默认的数据库配置——相当于为该参数传入了元素的default属性值
第三个properties参数用于为MyBatis配置文件传入额外的属性配置。在重载的build()方法中,也可以不指定properties参数,表明不传入额外的属性配置

由此可见,SqlSessionFactoryBuilder的唯一作用就是创建SqlSessionFactory,一旦SqlSessionFactory对象创建成功,就再也不需要SqlSessionFactoryBuilder了。因此,建议在应用启动时,使用局部变量的SqlSessionFactoryBuilder来创建SqlSessionFactory,这样能保证该对象被尽快销毁,从而释放它底层用到的XMLConfigBuilder及XML资源。
以作者的经验来看,SqlSessionFactoryBuilder其实根本没有必要创建实例,Clinton(MyBatis的作者)在设置该类时将build()方法定义成static会更加简洁,可是他没没有这么做

SqlSessionFactory的作用域

SqlSessionFactory由SqlSessionFactoryBuilder创建,做SqlSessionFactoryBuilder的源代码中可以看到如下方法:

public SqlSessionFactory build(Configuration config){
	return new DefaultSqlSessionFactory(config);
}

SqlSessionFactoryBuilder中其他重载的build()方法始终都是调用该build()方法来创建SqlSessionFactory的,实际上返回的是DefaultSqlSeesionFactory实例的
在创建SqlSessionFactory时,程序需要传入一个Configuration参数,Configuration对象代表了MyBatis的核心配置文件,因此它提供了大量方法来获取或修改MyBatis的核心配置文件

SqlSessionFactory提供了一个getConfiguration()方法来获取底层的Configuration对象
SqlSessionFactory的主要作用就是获取SqlSession,因此它提供了如下几个重载的openSession()方法来获取SqlSession

  • SqlSession openSession()
  • SqlSession openSession(boolean autoCommit)
  • SqlSession openSession(boolean autoCommit)
  • SqlSession openSession(ExecutorType execType,TransactionIsolationLevel level)
  • SqlSession openSession(ExecutorType execType)
  • SqlSession openSession(ExecutorType execType,boolean autoCommit)
  • SqlSession openSession(ExecutoryType execType,Connection connection)

上面这些方法允许在获取SqlSession时进行以下控制

  • 事务处理:由autoCommit参数控制,如果将该参数设为false,则意味着不使用自动提交,而是使用显式的事务控制(这也是MyBatis推荐的方式)
  • 连接对象:由connection参数控制,如果显式传入connection参数,则意味着SqlSession底层使用用户提供的JDBC Connection对象;否则,MyBatis会使用底层配置的数据源来获取数据库连接
  • 执行语句的类型:由execType参数控制,它可控制MyBatis底层是否复用PreparedStatement,是否使用JDBC的批量更新(包括插入或删除)
  • 隔离级别:由level参数控制

如果调用openSession()方法时不传入任何参数(前面的示例就是这么做的),那么该方法将会打开一个具有如下特征的SqlSession

  • 不自动提交,相当于为autoCommit参数传入false
  • 使用底层配置的数据源来获取数据库连接
  • 使用最简单的PreparedStatement(既不复用,也不启用批量更新),相当于为execType参数传入ExecutorType,SIMPLE值
  • 事务隔离级别将会使用驱动或数据库的默认级别

execType参数支持以下三个枚举值

  • ExecutorType.SIMPLE:它意味着MyBatis每次执行时都创建新的PreparedStatement,既不会复用PreparedStatement,也不会启用批量更新
  • ExecutorType.REUSE:复用PreparedStatement
  • ExecutorType.BATCH:启用批量更新

SqlSessionFactory底层封装了数据库配置环境(包括Datasource和事务配置),SqlSessionFactory应该是整个应用相关的,因此,建议在应用启动时创建SqlSessionFactory,整个应用在运行期间它应该一致存在,没有任何理由丢弃它或重新创建另一个实例
SqlSessionFactory的最佳作用域就是应用作用域,例如,使用单例模式或静态单例模式即可实现这一点。简单来说,应用不退出,SqlSessionFactory就不退出
SqlSessionFactory被设计成线程安全的实例,因此,它可以被多个线程共享

SqlSession及其作用域

SqlSession是MyBatis执行数据库操作最核心的API,它提供了如下常用方法来执行SQL语句

  • T selectOne(String statement,Object parameter):执行select查询语句,返回单条记录封装对象
  • ListselectList(String statement,Object parameter):执行select查询语句,返回一条或多条记录封装的对象所组成的List集合
  • CursorselectCursor(String statement,Object parameter):该方法的功能与selectList()方法大致相同,只是该方法不返回List集合,而是返回Cursor对象,该对象可延迟获取实际的记录
  • int insert(String statement,Object parameter):通常用于执行insert语句
  • int update(String statement,Object parameter):通常用于执行update语句
  • int delete(String statement,Object parameter):通常用于执行delete语句

正如前面所介绍的,上面的insert()、update()、delete()方法都可用于执行DML语句,它们只是语义上的区别,在功能上基本相似。这些方法在本章已经有过示例示范,实际上MyBatis不再推荐直接使用这些方法执行数据库操作

SqlSession还提供了如下方法用于控制事务

  • commit():提交事务
  • commit(boolean force):提交事务,其中force参数指定是否强制提交
  • rollback():回滚事务
  • rollback(boolean force):回滚事务,其中force参数指定是否前置回滚

SqlSession提供了如下方法来支持批量更新

  • List<.BatchResult>flushStatements():该方法用于执行存储在JDBC驱动中的批量更新语句。

如果要使用MyBatis执行批量更新,则需要在获取SqlSession时将ExecutorType参数设置为ExecutorType.BATCH
SqlSession底层封装了Connection,实际上SqlSession提供了一个getConnection()方法是返回预JDBC Connection的用法类似,SqlSession的最佳作用域是方法作用域(将SqlSession定义成局部变量)或请求域(对于Web应用)
不要将SqlSession定义成成员变量)(实例变量或类变量也不行),因为这样可能导致SqlSession被多个线程共享;更不能将SqlSession放入Web应用的HttpSession或ServletContext作用域;也不能将SqlSession设为Spring容器中的singleton行为
如果在Java项目中使用MyBatis,那么每个方法用SqlSession操作完数据库之后都应该立即关闭它,正如前面每个方法的最后一行都关闭了SqlSession,如果SqlSession没有被成功关闭,就可能引起物理资源泄露
在实际项目中,为了保证SqlSession一定被成功关闭,建议使用finally块来关闭它。例如,如下代码片段

var session = SqlSessionFactory.openSession();
try{
	//你的应用逻辑代码
	...
}finally{
	session.close();
}

如果在Java Web应用中使用MyBatis,则可以考虑将SqlSession放在一个与HTTP request对象相似的作用域中,这样可以让整个HTTP请求都使用同一个SqlSession——每次应用接收到用于请求后,都可以打开一个SqlSession,在成功生成服务器响应数据之后,程序同样应该使用finally块来关闭它

MySQL等数据库本身支持批量插入,它们支持在insert语句的values后用逗号隔开多条记录,这样即可使用一条insert语句来插入多条数据,这种来自数据库底层支持的批量插入肯定具有更好的性能。因此,如果数据库本身支持批量插入(使用一条insert语句可插入多条记录),则建议使用MyBatis的foreach元素来利用该特性

Mapper组件的作用域

SqlSession提供了如下方法来获取Mapper组件

  • getMapper(Classtype):根据Mapper接口获取Mapper组件

正如前面所介绍的,MyBatis通常会使用JDK动态代理为Mapper组件生成实例,由于Mapper组件由SqlSession创建而来,因此Mapper组件的作用域不应该超过创建它的SqlSession的作用域
一般来说,建议使用局部变量保存Mapper组件。换句话说,程序每次需要执行数据库操作时,都应该先获取Mapper组件,用完后立即丢弃它。例如,如下代码片段示范了Mapper组件的最佳实践

var session = sqlSessionFactory.openSession();
try{
	var newsMapper = session.getMapper(NewsMapper.class);
	//你的应用逻辑代码
}finally{
	session.close();
}

2-4MyBatis配置详解

MyBatis使用核心配置文件来管理相关配置信息,下面就来详细介绍这些配置的作用和用法。

属性配置

MyBatis可以在核心配置文件中使用元素配置属性——所谓属性就是一个一个的key-value对,接下来既可在核心配置文件中通过属性名(key)来引用属性值(value)
通过使用属性,可以将核心配置文件中的部分信息(比如数据库连接信息)提取到属性文件中集中管理,或者以参数形式传给SqlSessionFactory的build()方法
总结来说,MyBatis允许在三个地方配置属性

  • 使用额外的属性文件配置,再使用元素加载该属性文件
  • 元素中使用子元素配置,每个子元素配置一个属性
  • 在SqlSessionFactory的build()方法中传入Properties参数

当多出配置存在同名的属性时,优先级高的属性会覆盖优先级低的属性。上述三种方式所配置的属性优先级如下

build()方法>额外的属性文件<property.../>子元素

下面示例使用如下属性文件来管理数据库连接信息

#故意将driver配错
driver=crazyit
url=jdbc:mysql://127.0.0.1:3306/mybatis?serverTimezone=UTC
user=root

提供上面的属性文件之后,接下来既可在MyBatis核心配置文件中加载并使用该属性。下面的核心配置文件示范了这种用法



DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	
	<properties resource="db.properties">
		
		<property name="user" value="root"/>
		<property name="password" value="password"/>
	properties>
	
	<environments default="mysql">
		
		<environment id="mysql">
			
			<transactionManager type="JDBC"/>
			
			<dataSource type="POOLED">
				
				<property name="driver" value="${driver}" />
				<property name="url" value="${url}" />
				<property name="username" value="${user}" />
				<property name="password" value="${password}" />
			dataSource>
		environment>
	environments>
	<mappers>
		
		<mapper resource="org/crazyit/app/dao/NewsMapper.xml"/>
	mappers>
configuration>

上面配置文件中前面定义了元素,该元素的resource属性值指定加载哪个属性文件,除使用resource属性之外,也可以使用url属性来指定加载哪个属性文件
resource属性与url属性的区别如下

  • resource:指定从应用的类加载路径下搜索属性文件。例如,此时将db.properties文件放在src目录下(编译时会赋值到classes目录下),因此直接将resource属性指定为db.properties
  • url:指定加载URL对应的属性文件。例如,根据HTTP协议加载网络上的资源文件,可将url指定为http://domain/资源文件;也可加载指定磁盘路径下的文件,如将url指定为file:///g:/abc/db.properties,这意味着加载G:/abc目录下的db.properties文件

一般来说,通过resource属性指定要加载的属性文件中定义的属性。不难看出,引用属性值的语法格式如下

${属性名}

为了更好地演示MyBatis属性配置的加载机制,本例的属性配置略微有点复杂

  • 在db.properties文件中已有名为user的属性,在元素也配置了名为user的属性,但该属性配置是错误的。由于db.properties中的属性的优先级更高,因此它会覆盖中配置的user属性值
  • 在db.properties文件中没有配置名为password的属性,因此在中配置的password属性值会发挥作用
  • 在db.properties文件中配置了名为driver的属性,但该属性配置是错误的,接下来在build()方法中传入Properties对象会覆盖该属性

下面是主程序代码,该程序会在使用build()方法时传入Properties参数

public class NewsManager {
	private static SqlSessionFactory sqlSessionFactory;
	public static void main(String[] args)throws Exception {
		var resource = "mybatis-config.xml";
		//使用Resorces工具从类加载路径下加载指定文件
		var inputStream = Resources.getResourceAsStream(resource);
		//创建Properties对象
		var props = new Properties();
		props.setProperty("driver","com.mysql.cj.jdbc.Driver");
		//传入Properties作为参数构建SqlSessionFactory
		sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream,props);
		//打开Session
		var sqlSession = sqlSessionFactory.openSession();
		insertTest(sqlSession);
	
	}
	public static void insertTest(SqlSession sqlSession) {
		News news = new News(null,"测试","测试内容");
		NewsMapper mapper = sqlSession.getMapper(NewsMapper.class);
		int n = mapper.saveNews(news);
		System.out.printf("插入了%d条数据%n", n);
		sqlSession.commit();
		sqlSession.close();
	}
}

在我没有添加log4j2的配置文件后,控制台居然报错了,我复制了上一个实例的log4j2配置文件后,居然又成功了
在这里插入图片描述
上面的SqlSessionFactory创建的build()方法传入了Properties参数,build()方法传入属性的优先级最高,此处设置了名为driver的属性,该属性会覆盖db.properties文件中的driver属性
运行该示例,将看到程序完全可以正常连接数据库,这充分说明了MyBatis的三种属性配置方式及其优先级
上面示例演示了再三个地方配置属性,但实际项目中通常不需要搞得这么复杂,一般来说,将某种特定的信息(比如数据库连接信息)集中放在属性文件中配置,然后使用元素的resource或url属性加载该属性文件即可
当使用${属性名}来引用属性值时,可能存在属性值并不存在的情况,MyBatis还允许在引用属性值时指定默认值
为引用的属性值指定默认值的语法格式如下:

${属性名:默认值}

从上面的语法可以看出,只要将默认值放在属性名之后,并以英文冒号隔开即可
需要说明的是,在引用属性时指定默认值默认是关闭的,如果需要启用该特性,则需要在中增加如下属性

<property name="org.apche.ibatis.parsing.PropertyParser.enable-default-value" value="true"/>

在默认情况下,被引用属性的默认值与属性名之间的分隔符是英文冒号(,MyBatis同样允许改变这个分隔符,可以通过在中增加如下属性来改变分隔符

<!-- 将被引用属性的默认值与属性名之间的分隔符改位两个减号 -->
<property name="org.apache,ibatis.parsing.PropertyParser,default-value-separator" value="--" />

下面示例的db.properties文件只配置了两个属性

url=jdbc:mysql://127.0.0.1:3306/mybatis?serverTimezone=UTC
user=root

接下来mybatis-config.xml文件示范了为属性配置默认值



DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
	<configuration>
	
	<properties resource="db.properties">
		
		<property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/>
		
		<property name="org.apache.ibatis.parsing.PropertyParser.default-value-separator" value="--"/>
		properties>
		
		<environments default="mysql">
			
			<environment id="mysql">
				
				<transactionManager type="JDBC"/>
				
				<dataSource type="POOLED">
					<property name="driver" value="${driver--com.mysql.cj.jdbc.Driver}"/>
					<property name="url" value="${url}"/>
					<property name="username" value="${user}"/>
					
					<property name="password" value="${password--password}"/>
				dataSource>
			environment>
		environments>
	<mappers>
		
		<mapper resource="org/crazyit/app/dao/NewsMapper.xml" />
	mappers>
configuration>

设置配置

MyBatis有一些全局行为需要设置,比如是否使用缓存、日志设置等,这些设置(settings)都被放在元素中,每个子元素配置一个设置

例如,如下设置启用MyBatis的延迟加载功能

<settings>
	
	<setting name="lazyLoadingEnabled" value="true" />
settings>

从上面的配置可以看出,元素的name属性指定名称,value用于配置合适的值

正如从前面示例中看到的,很多时候项目可能并不需要为MyBatis配置这些设置,因为这些设置都有一个比较合理的默认值,通常使用默认值就能让MyBatis具有较好的行为。MyBatis支持的设置及其有效值、默认值如下表所示
第二章 MyBatis的基础用法_第29张图片
第二章 MyBatis的基础用法_第30张图片
在这里插入图片描述

为类型配置别名

在MyBatis的Mapper配置中必然涉及大量的类,在默认情况下,开发者必须为每个类都指定全限定类名,这样的代码不仅冗长,而且十分烦琐
MyBatis允许在元素内通过如下两个元素为Java类指定别名

  • :为单个Java类指定别名
  • :为指定包下的所有Java类集中指定别名

为了简化开发,MyBatis默认已经为常见的Java类型提供了别名。常见的Java类型及其别名如下表所示

第二章 MyBatis的基础用法_第31张图片
第二章 MyBatis的基础用法_第32张图片
除了上面这些内置别名,如果希望在MyBatis中使用其他别名,则需要开发者自己配置。
下面介绍通过为单个Java类型指定别名的用法。先在MyBatis核心配置文件中添加如下配置

<typeAliases>
		
			<typeAlias alias="news" type="org,crazyit.app.domain.News"/>
		typeAliases>

上面的配置应该写在核心配置文件的properties元素后,这是MyBatis的约定,上面的配置文件中org,crazyit.app.domain.News类指定了别名:news,接下来既可在Mapper中需要用到org.crazyit.app.domain.News类的地方使用news别名

例如,下面XML Mapper文件为元素指定一个resultType属性,该属性指定该SQL语句时传入的参数类型,下面是该Mapper文件的代码

<select id="getNews" resultType="news">
		select news_id , news_title title,news_content content
		from news_inf where news_id = #{id}	
	select>

正如上面的resultType="news"所示,此时resultType属性被指定为news别名,无须使用org.crazyit.app.domain.News这样的全限定类名,因此配置文件就简洁多了
如果希望为指定包下的所有类指定别名,则可通过元素来指定,该元素的name属性指定MyBatis自动为哪个包下的所有类指定别名

在没有注解的情况下,MyBatis会自动将所有类的首字母小写来作为别名。此外每页可以在类上使用@Alias注解显式指定别名
下面示例演示了核心配置文件中通过元素为指定包下的所有类指定别名

<typeAliases>
	
	<package name="org,crazyit.app.domain"/>
typeAliases>

上面配置文件中为org.crazyit.app.domain包下的所有类指定别名。在默认情况下,如果程序在该包下有News类,那么该类对应的别名为news。如果不希望使用这个news别名,则可通过News类增加@Alias注解来指定别名。例如如下代码

@Alias("fkNews")
public class News
{
	//省略该类的其他代码
	...
}

在该News类上定义了@Alias(“fkNews”)注解,这意味着该News类的别名是fkNews,接下来既可在XML Mapper中使用fkNews别名来代表News类的别名是fkNews,接下来既可在XML Mapper中使用fkNews别名来代表News类的全限定类名,如下面的代码所示


!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.crazyit.app.dao.NewsMapper">

	<select id = "getNews" resultType="fkNews">
		select news_id id,news_title title,news_content content
		from news_inf where news_id = #{id}
	select>
mapper>	

对象工厂

当MyBatis将ResultSet映射成对象时,MyBatis为每行记录创建一个对象,这个对象就由对象工厂(Object Factory)负责创建
读者不必担心,MyBatis其实已经内置了工作良好的DefaultObjectFactory(实现了ObjectFactory接口),因此,MyBatis用户大部分时候并不需要自己实现对象工厂。这就是为什么前面实例都没有提供对象工厂,但程序依然良好的原因
在某些极端的情况下,开发者可能希望实现自己的对象工厂来执行某些定制行为,这时可通过开发自定义对象工厂来实现

对象工厂的本质属于对MyBatis的扩展,这并不是MyBatis用户日常开发的工作

为MyBatis开发自定义对象工厂只需两步
①定义一个实现ObjectFactory接口的类,该类可作为对象工厂
②在MyBatis核心配置文件中使用元素注册对象工厂

1、开发自定义工厂
自定义工厂需要实现ObjectFactory接口,但在实际开发时往往不是从头开始,而是基于MyBatis默认的对象工厂开发的,因此,通常建议继承DefaultObjectFactory,这样开发起来更加简洁
如果实现ObjectFactory接口,则需要实现以下4个方法;如果继承DefaultObjectFactory基类,也可重写以下4个方法

  • T create(Classtype):该方法负责创建对象。当MyBatis通过无参数的构造器创建对象时,实际上由对象工厂调用该方法来创建对象
  • T create(Classtype,List>constructorArgTypes,ListconstructorArgs):该方法负责创建对象。当MyBatis通过带参数的构造器创建对象时,实际上由对象工厂负责创建对象。当MyBatis通过带参数的构造器创建对象时,实际上由对象工厂调用该方法来创建对象。该方法比上一个参数多了两个参数,其中constructorArgTypes代表多个构造器参数的类型,constructorArgs则代表多个构造器参数的值
  • boolean isCollection(Classtype):该方法返回true,代表创建的对象是集合
  • void setProperties(Properties properties):该方法负责将配置对象工厂时配置多个属性以Properties整体传入

本示例将开发一个自定义对象工厂,该对象工厂会对生成News对象进行额外处理——为该对象添加author和queryDate两个元数据。下面是该对象工厂的实现类

package org.crazyit.mybatis;

import java.util.*;

import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.crazyit.app.domain.News;

//继承DefaultObjectFactory创建自定义对象工厂
public class FkObjectFactory extends DefaultObjectFactory{
	private String author;
	//使用无参的构造器创建对象时,调用该方法
	public Object create(Class type) {
		System.out.println("无参数的构造器创建:"+type);
		var obj = super.create(type);
		return processObject(obj);
	}
	//该方法负责将objectFactory元素内配置的属性传入该对象
	public void setProperties(Properties properties) {
		super.setProperties(properties);
		System.out.println("设置属性值"+properties);
		this.author = properties.getProperty("author");
	}
	public <T>boolean isCollection(Class<T>type){
		System.out.println("==isCollection==");
		//直接调用父类的方法
		return super.isCollection(type);
	}
	
	
	
	private Object processObject(Object obj) {
		//如果type是News的子类或本身
		if(News.class.isAssignableFrom(obj.getClass())) {
			var news = (News)obj;
			//为news放入额外的信息
			news.getMeta().put("author",this.author);
			news.getMeta().put("queryDate",new Date());
		}
		return obj;
	}
}

上面程序的关键就是重写了两个create()方法,这两个方法都是先调用父类(DefaultObjectFactory)的方法来创建对象的,然后调用processObject()方法对继承DefaultObjectFactory创建的News对象进行额外处理——添加元数据

上面程序调用了News的getMeta()方法,在News类中增加HashMap变量和getting方法

该自定义工厂类还重写了setProperties()方法,该方法将用于在元素内配置所有属性以Properties参数传入

配置自定义工厂

在核心配置文件中可以使用元素配置自定义工厂类,在该元素内还可以使用元素配置属性——无论配置多少个属性,它们都会被整体封装成一个Properties参数传给对象工厂的setProperties()方法

在下面的mybatis-config.xml文件中配置了自定义对象工厂

<typeAliases>
			
			<package name="org.crazyit.app.domain"/>
		typeAliases>
		
		<objectFactory type="org.crazyit.app.mybatis.FkObjectFactory">
			
			<property name="author" value="crazyit"/>
		objectFactory>

经过上面两部,MyBatis就会改为使用自定义工厂来创建对象。例如,在主程序中查询id为3的News对象,将会看到查询出来New对象会携带meta属性,该属性保存了author(由配置属性传入)和queryDate(代替查询时间)两个数据

加载Mapper

正如从前面示例中所看到的,在开发玩完Mapper组件之后,还需要在核心配置文件中进行配置,用于告诉MyBatis加载这些Mapper
前面在加载Mapper时使用了类似于如下的配置片段

<mappers>
	
	<mapper resource="org/crazyit/app/dao/NewsMapper.xml"/>
mappers>

那么问题来了,MyBatis是否还支持加载Mapper的其他配置方式?答案是肯定的。MyBatis共支持4种加载Mapper的配置方式

  • 指定resource属性加载Mapper:这种方式会基于类加载路径来定位XML Mapper文件
  • 指定url属性加载Mapper:这种方式根据URL来定位XML Mapper文件。通过这种方式,可以使用f://协议加载指定磁盘路径下的Mapper。例如如下配置片段:
<mappers>
	<mapper url="file://G:/abc/NewsMapper.xml"/>
mappers>
  • 指定class属性加载Mapper:这种方式的class属性值为Mapper接口。例如如下配置片段:
<mappers>
	<mapper class="org.crazyit.app.dao.NewsMapper" />
mappers>
  • 使用元素加载指定包下的所有Mapper:这种方式可以加载指定包下的所有Mapper

虽然MyBatis提供了4种配置方式来加载Mapper,但前三种方式大同小异,每个元素只能加载一个Mapper,尤其是为指定url属性的方式,其实这是一种极为少用的方式——很少有开发者会根据磁盘路径加载Mapper,因为这样可移植性太差了
使用元素加载指定包下的所有Mapper的方式,在实际项目开发中会比较常用,开发者只需要告诉MyBatis去哪些包下加载Mapper,然后将所有的Mapper组件放在这些包下,即可避免对每个Mapper都需要配置一次

下面示例示范了使用元素来加载Mapper



DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
	<configuration>
<configuration>
<mappers>
		
		<package name="org.crazyit.app.dao" />
mappers>
configuration>

上面的package代码配置了MyBatis自动加载org.crazyit.app.dao包下的所有Mapper

2-5类处理器

MyBatis的功能就是处理Java对象与底层数据库之间的转换,无论是JavaMyBatis为PreparedStatement设置参数时,还是从ResultSet取出记录来封装Java对象时,都需要使用类型处理器(Type Handler)来处理Java类型与数据库类型之间的转换

简单来说,MyBatis的类型处理器类似于其他语言的类型转换器,它负责完成Java类型与JDBC类型之间的转换

类型处理器同样可以在MyBatis核心配置文件中配置,不过类型处理器的内容比较多,故本章将它单独作为一节来介绍

内置的类型处理器

对于绝大部分开发场景而言,MyBatis使用者根本意识不到类型处理器的存在,这是因为MyBatis内置了大量的类型处理器,这些类型处理器基本可以处理日常开发处理的各种类型

MyBatis内置的类型处理器如下表所示

第二章 MyBatis的基础用法_第33张图片
第二章 MyBatis的基础用法_第34张图片

从上面的类型处理器可以看出,MyBatis已能支持Java8新增的日期、时间类型,MyBatis从3.4.5版本开始提供了内置的类型处理器来支持这些新增的日期、时间类型

自定义类型处理器

对于一些特殊类型,MyBatis没有提供相应的类型处理器你,此时可以通过自定义类型处理器进行转换

与开发自定义对象工厂一样,开发自定义类型处理器同样只需要两部。
1、开发自定义类型处理类
2、在核心配置文件中配置类型处理器

1、开发自定义类型处理器类

自定义类型处理器类需要实现TypeHandler接口,实际上,通常会通过继承BaseTypeHandler基类来实现

MyBatis内置的类型处理器,也是通过继承BaseTypeHandler基类来实现的

通过继承BaseTypeHandler基类开发自定义类型处理器,只要实现以下4个抽象方法即可

  • void setNonNullParameter(PreparedStatement ps,int i,T parameter,JdbcType):该方法负责将Java对象转换成合适的PreparedStatement能接收的类型,并为PreparedStatement设置参数值
  • T getNullableResult(CallableStatement cs,int columnIndex):该方法负责将从数据库查询得到的数据转换成目标Java类型的实例
  • T getNullableResult(ResultSet rsmint columnIndex):该方法负责将从数据库查询得到的数据转换成目标Java类型的实例
  • T getNullableResult(ResultSet rs,String cloumnName):该方法负责将从数据库查询得到的数据转换成目标Java类型的实例

上面列出了4个方法,但实际上只有两个,其中setNonNullParameter负责将Java类型的实例转换成数据库类型;而getNullableResult则负责将数据库类型的值转换成Java类型的实例

下图显示了这两个方法的功能示意图

第二章 MyBatis的基础用法_第35张图片

本例将会示范把Name类型的属性值传入底层数据库的VARCHAR例。本例用到一个User类,该类包含一个Name类型属性。下面代码是该User类的代码

public class User{
	//标识属性
	private int id;
	//名字
	private Name name;
	//年龄
	private int age;
	//省略该类的构造器和setter、getter方法
	...
}

上面User类中的name属性是Name类型的,Name是一个复合类

public class Name{
	private String first;
	private String last;
	//省略Name的构造器和setter、getter方法
	...
}

下面的类继承了BaseTypeHandler基类,并实现了该抽象基类中的4个抽象方法,这4个方法负责完成Name对象与VARCHAR(对应Java的String类型)之间的相互转换,这样就实现了一个类型处理器

@MappedJdbcTypes(JdbcType.VARCHAR)//声明该类型处理器处理哪些JDBC类型
@MappedTypes(Name.class)//声明该类型处理器处理哪些Java类型
public class NameTypeHandler extends BaseTypeHandler<name>{
	//将Java类型转换成JDBC支持的类型
	@Override
	public void setNonNullParameter(PreparedStatement ps,int i,Name param,JdbcType jdbcType)throws SQLException{
		ps.setString(i,param.getFirst()+"-"+param.getLast());
	}
	//以下三个方法都负责将查询得到的数据转换成目标Java类型的实例
	@Override
	public Name getNullableResult(ResultSet rs,String columnName)throws SQLException{
		String[]Tokens = rs.getString(columnName).split("-");
		return new Name(nameTokens,nameTokens[1]);
	}
	@Override
	public Name getNullableResult(ResultSet rs,int cloumnIndex)throws SQLException{
		String[]Tokens = rs.getString(columnName).split("-");
		return new Name(nameTokens,nameTokens[1]);
	}
	@Override
	public Name getNullableResult(CallableStatement cs,int columnIndex)throws SQLException{
		String[]Tokens = rs.getString(columnName).split("-");
		return new Name(nameTokens,nameTokens[1]);
	}
}

上面的类型处理器还使用了@MappedJdbcTypes和@MappedTypes两个注解修饰,这两个注解的作用如下

  • @MappedJdbcTypes:该注解声明该类型处理器负责处理哪些JDBC类型
  • @MappedTypes:该注解声明该类型处理器负责处理哪些Java类型

其实这两个注解是可选的,这意味着即使不添加这两个注解,MyBatis也可通过反射识别该类型处理器负责处理哪些JDBC类型与Java类型之间的转换。出于准确性考虑,建议添加这两个注解(至少添加@MappedJdbcTypes)

2、配置自定义类型处理器

在实现了自定义类型处理器之后,接下来还要在核心配置文件中使用配置它。使用元素时同样可指定两个属性

  • javaType:指定该类型处理器负责处理的Java类型
  • jdbcType:指定该类型处理器负责处理的JDBC类型

正如大家所想的,这两个属性与@MappedTypes和@MappedJdbcTypes两个注解的功能是类似的。因此,如果在定义类型处理器时已经添加了@MappedTypes和@MappedJdbcTypes两个注解,那么在配置类型处理器时可以不用指定javaType和jdbcType属性
需要指出的是,在元素中指定的javaType会覆盖@MappedTypes注解的值。也就是说,如果为元素指定了javaType属性值,那么@MappedTypes注解就会被忽略;类似的,如果为元素指定了jdbcType属性值,那么@MappedJdbcTypes注解也会被忽略

下面配置文件中配置了上面开发的自定义类型处理器

<configuration>
	<typeAliases>
		
		<package name="org.crazyit.app.domain"/>
	typeAliases>
	<typeHandlers>
		<typeHandler handler="org.crazyit.app.mybatis.NameTypeHandler" />
	typeHandlers>
	
	...
configuration>

经过上面两步,NameTypeHandler就可以在MyBatis中发挥作用了,它会负责完成Name类型与VARCHAR之间的相互转换
接下来在XML Mapper中定义SQL语句时,程序可以直接将Name类型的属性值插入VARCHAR类型的数据列中,也可以将从底层查询得到的VARCHAR类型的值转换成Name类型的属性值,例如如下代码

<mapper namespace="org.crazyit.app.dao.UserMapper">
	
	<delete id="insertUser" parameterType="user">
		insert into user_inf values(null,#{name},#{age})
	delete>
	
	<select id="selectUser" paramterType="int" resultType="user">
		select user_id id,user_name name,user_age age from user_inf where user_id=#{a}
	select>
mapper>

上面配置中的第一行插入语句代码将User对象的name属性插入user_inf表的user_name数据列中,虽然User的name属性是Name类型的,而user_name列是VARCHAR类型的,但是这完全没有问题,因为NameTypeHandler会负责将Name对象转换成String类型(对应VARCHAR)
上面配置中第二行查询语句代码选出user_name列(别名是name)将会被映射到User对象的name属性,虽然查询得到的user_name列是VARCHAR类型的,而User对象的name属性是Name类型的,但这完全没有问题,因为NameTypeHandler会负责将String类型(对应VARCHAR)转换成Name对象

枚举的类型处理器

MyBatis为枚举类型提供了如下两个类型处理器

  • EnumTypeHandler:该类型处理器将枚举值转换成对应的名称(字符串)
  • EnumOrdinalTypeHandler:该类型处理器将枚举值转换成对应的序号(整数)
    EnumTypeHandler和EnumOrdinalTypeHandler是比较特别的,其他类型处理器通常专门处理某个特定的类型,但EnumTypeHandler和EnumOrdinalTypeHandler能处理的枚举类型——只要该类型是Enum的子类,它们都可以处理
    为什么EnumTypeHandler和EnumOrdinalTypeHandler与众不同呢?其他也没啥特别的,主要是因为它们都是泛型类型处理器。如果打开EnumTypeHandler的源代码,就会发现如下代码:
public class EnumTypeHandler<E extends Enum<E>>extends BaseTypeHandler<E>{
	//实现类型处理器的4个方法
	...
}

从上面的类声明可以看出,EnumTypeHandler类型处理器可以处理所有Enum的子类。EnumOrdinalTypeHandler类的定义与此类似
实际上,MyBatis也允许开发自定义泛型类型处理器,只要按如下方式定义类型处理器即可。

public class BaseTypeHandler<E extends Base>extends BaseTypeHandler<E>{
	private final Class<E>type;
	public BaseTypeHandler(Class<E>type){
		if(type==null)throw new IllegalArgumentException("type参数不允许为null!");
	}
	...
	//实现类型处理器的4个方法
	...
}

只要定义上面所示的类型处理器,那么该类型处理器就可处理所有Base的子类
在默认情况下,MyBatis使用EnumTypeHandler处理枚举,而且该类型处理器已由MyBatis配置完成,因此可以直接使用
例如,下面示例为News类增加了一个happenSeasion属性,该属性是Season枚举类型的。该News类的代码如下

public class News{
	private Integer id;
	private String title;
	private String Content;
	private Season happenSeason;
	//省略构造器和setter、getter方法
	...
}

上面News类的happenSeason属性是Season枚举类型的,下面是Season枚举类的代码

public enum Season
{
	SPRING,SUMMER,FALL,WINTER;
}

虽然News类包含了Season枚举类的属性,但是MyBatis用户几乎不需要对它进行任何额外的处理,这是由于EnumTypeHandler会自动发挥作用

在XML Mapper中配置对News对象的操作,与上面一个示例相对比几乎没有任何变化,如下面的代码所示。

<mapper namespace="org.crazyit.app.dao.NewsMapper">
	
	<insert id="saveNews">
		insert into news_inf values(null,#{title},#{content},#{happenSeason})
	insert>
	<select id="getNews" resultType="news">
		select news_id id,news_title title,
		news_content content,happen_season happenSeason
		from news_inf where news_id = #{id}
	select>	
mapper>

从上面的XML Mapper可以看出,程序只需要照常将happenSeason属性插入表中,EnumTypeHandler就会负责将枚举值转换为它的名称(String类型),这样MyBatis就可正常地将枚举值存入底层数据库了

存储枚举值的序号

MyBatis默认使用EnumTypeHandler来处理枚举类型,因此默认保存枚举值的名称。如果需要保存枚举值的序号,则需要开发者自行将EnumOrdinalTypeHandler注册成枚举类型处理器

Java的所有枚举值都有默认的序号,Java可通过枚举值的ordinal()方法获取它的序号

现对上一个示例略微修改,只要修改MyBatis核心配置文件即可,在核心配置文件中增加类型处理器的配置部分。下面是修改后的配置文件

<configuration>
	<typeAliases>
		
		<package name="org.crazyit.app.domain" />
	typeAliases>
	<typeHandlers>
		
	typeHandlers>
	
	...
configuration>	

上面配置的typeHandler告诉MyBatis使用EnumOrdinalTypeHandler负责处理Season枚举类型

虽然news_inf表的happen_season列依然是VARCHAR类型的,但这并不影响MyBatis的正常工作,这是因为MyBatis非常智能,它会将int类型的序号转换成字符串后存入底层数据表中
当然,处于程序性能和一致性考虑,当程序使用EnumOrdinalTypeHandler将枚举值保存为它的序号时,还是建议将它对应的数据列定义为INTEGER类型

同时存储枚举值的名称和序号

在极端情况下,项目需要对同一个枚举类型分别存储名称和序号,假如还是上面的News类,不过需要为它增加一个recordSeason属性,该属性同样属于Season类型,但是要求很奇葩:用happenSeason和recordSeason属性存储枚举值序号,而用recoredSeason属性存储枚举值名称
如果直接为上一个示例的News类增加一个Season类型的属性,那么由于核心配置文件中配置了EnumOrdinalTypeHandler作为Name的类型处理器,因此程序在保存News对象时,它的happenSeason和recordSeason属性都会被保存为枚举值序号
如果需要对同一个枚举类型分别存储名称和序号,则需要一种临时指定类型处理器的方式,MyBatis允许在Mapper中为特定属性指定类型处理器

在MyBatis的核心配置文件中配置的类型处理器是全局性的,它会自动处理所有对应类型的属性;在Mapper中为特定属性指定的类型处理器是局部性的,它只对特定属性起作用

在Mapper中为特定属性指定类型处理器的方式有两种:

  • 元素或@Result注解中通过typeHandler属性指定类型处理器
  • 在#{}中通过typeHandler属性指定类型处理器

本示例先为上一个示例的News类增加一个recordSeason属性,该News类的代码如下。

public class News{
	private Integer id;
	private String title;
	private String content;
	private Season happenSeason;
	private Season recordSeason;
	//省略构造器和setter、getter方法
	...
}

上面News类中包含了两个Season类型的属性,项目需要将happenSeason属性保存为枚举值序号,这一点可以得到保证:在核心配置文件中配置的EnumOrdinalTypeHandler会发挥作用;项目还需要将recordSeason属性保存为枚举值的名称,这就需要在Mapper中进行配置了
下面是XML Mapper中的配置

<mapper namespace="org.crazyit.app.dao.NewsMapper">
	
	 <insert id="saveNews">
	 	insert into news_inf values
	 	(null,#{title},#{content},#{happenSeason},#{recordSeason,typeHandler=org.apche.ibatis.type.EnumTypeHandler})
	 insert>
	 
	 <select id="getNews" resultMap="newsMap">
	 	select news_id id,news_title title,
	 	news_content content,happen_season happenSeason,
	 	record_season
	 	from news_inf where news_id = #{id}
	 select>
	 <resultMap id="newsMap" type="news">
	 
 	<result column="record_season" property="recordSeason" 
	typeHandler="org.apache.ibatis.type.EnumTypeHandler" />
	resultMap>
mapper>

上面代码中在insert语句中使用了如下代码:

#{recordSeason,typeHandler=org.apache.ibatis.type.EnumTypeHandler}

这个配置与前面配置的差别就在于多了typeHandler属性,该属性指定使用EnumTypeHandler类型处理器来处理recordSeason属性,这样就可保证将recordSeason属性存储为枚举值名称
元素时指定了resultMap属性,这是一个比较高级的属性,本书后面还会详细介绍它,此处先简单介绍一下:对于元素而言,它总是定义一条查询语句,因此MyBatis必须将它查询得到的ResultSet中的每行记录封装成对象。所以,要么为指定resultType属性,要么指定resultMap属性。它们的区别如下

  • resultType:该属性值只能指定一个类型,因此MyBatis只能根据“同名映射”规则来进行映射——也就是要求属性名和列名(或列名)同名
  • resultMap:该属性值为一个元素的id,这样即可在元素中详细定义数据列与属性之间的对应关系

根据上面介绍不难发现,指定resultType时配置文件更加简洁,但它的功能比较若,它只能根据“同名映射”规则进行映射;指定resultMap属性时则比较复杂(需要额外配置元素,但它的功能较强)
由于此处需要将record_season列映射成recordSeason属性,而且需要为该属性指定类型处理器,因此程序必须使用功能强大的元素进行配置。上面配置中最后两行代码完成了从record_season列到recoredSeason属性的映射,并通过typeHandler属性指定了类型处理器
经过在Mapper中如上所示的配置,MyBatis就知道了EnumType来处理recordSeason属性

这种在Mapper中指定类型处理器的方式,既使用与在XML Mapper中指定,也适用于使用注解指定。使用注解指定类型处理器时,对于#{}的形式,同样在花括号中用typeHandler指定类型处理器,这与XML Mapper的形式并没有什么区别;至于XML Mapper中的元素,MyBatis则提供了对应的@Result注解

将上面一个示例中的NewsMapper.xml文件删除,然后在NewsMapper.java文件中使用注解来管理SQL语句,配置映射。下面是NewsMapper.java接口的代码

//定义Mapper接口(DAO接口),该接口由MyBatis负责提供实现类
public interface NewsMapper{
	@Insert("insert into news_inf values"+
	"(null,#{title},#{content},#{happenSeason},#{recordSeason,"+
	"typeHandler=org.apache.ibatis.typeEnumTypeHandler})")
	int saveNews(News news);
	@Select("select news_id id,news_title title"+
	"news_content content,happen_season happenSeason,record_season"+
	"from news_inf where news_id = #{id}")
	@Results({
		@Result(property="recordSeason",column="record_season"
		,typeHandler=EnumTypeHandler.class)
	})		

	)
	News getNews(int id);
}

从上面第一段代码可以看出,@Insert注解与元素的用法基本相同,区别只是形式不同而已——元素将SQL语句放在元素里定义;而@Insert注解则将SQL语句定义为该注解的value属性
第二段代码则使用@Result定义了record_season列与recordSeason属性之间的对应关系,并通过typeHandler指定类型处理器。通过这段代码也可以看出,@Result注解与元素的用法基本相同,它们同样都指定了column、property和typeHandler属性

很多Java框架都同时支持XML配置和注解两种方式,对于真正掌握了框架的开发者而言,无论是XML配置还是注解,用起来都没有太大的区别,因为它们的本质其实完全一样,它们需要提供的配置信息也是完全相同的,区别只是提供信息的载体不同而已

2-6数据库环境配置

在本章第一个示例的MyBatis核心配置文件中,元素下只有两个子元素,前面已经详细介绍了使用元素加载Mapper的四种方式:而元素就是MyBatis配置文件的重点,下面将详细介绍有关元素的配置

环境配置与默认环境配置

元素正如它的名称所按时的,它的主要作用就是包含多个元素,每个元素定义一个数据库环境
由此可见,MyBatis允许在配置文件中配置多个数据库环境,这样可以为MyBatis项目的开发、测试或生产环境提供不同配置,也可在相同环境下分别使用不同的数据库。总之,每个环境对应一个数据库配置
使用元素时可指定一个id属性,该属性值就是该配置环境唯一标识
使用元素时可唯一指定的属性是default,该属性必须引用一个已有的元素的id属性,default属性指定默认的数据库环境配置

例如如下配置片段


<environments default="mysql">
	
	<environment id="mysql">
		
		<transactionManager type="JDBC" />
		
		<dataSource type="POOLED">
			
			<property name="driver" value="com.mysql.cj.jdbc.Driver" />
			<property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?serverTimezone=UTC" />
			<property name="username" value="root" />
			<property name="password" value="password">
		dataSource>
	environment>
environments>

上面片段中定义了一个id为mysql(该名字任意)的元素,字符串mysql就是这个配置环境的唯一标识
元素中的default属性值为mysql,这个mysql代表将id为mysql的数据库环境设为默认值。这个默认值有什么用呢?

回忆一下SqlSessionFactoryBuilder中build()方法的参数:

  • build(InputStream inputStream,String environment)
  • build(Reader reader,String environment)

这两个build()方法都可传入一个environment参数——其实将该参数名定义成environmentId会更合适,该参数不能随便填写,它也对应于引用元素的唯一标识(id属性值)
假如有下配置片段:

<environments default="env2">
	<environment id="env1">
		...
	environment>
	<environment id="env2">
		...
	environment>
	<environment id="env3">
		...
	environment>
environments>

上面配置片段中配置了三个数据环境,它们的id分别为env1、env2、env3,这意味着当程序调用SqlSessionFactoryBuilder的build()方法时,传给environment参数的值只能是env1或env2或env3,否则程序就会报错
在调用build()方法时为environment参数传入哪个元素的id,MyBatis就会使用这个元素定义的数据库环境来创建数据源、处理事务管理
如果在调用build()方法时没有传入environment参数,那么元素的default属性不会起作用;只有当不传入environment参数时,default属性才会起作用
虽然MyBatis可以配置多个数据库环境,但每个SqlSessionFactory实例只能选择一个环境:要么根据build()方法的environment参数选择,要么根据的default属性选择
MyBatis项目底层也可支持多个数据库,如果想连接两个数据库,就需要创建两个SqlSessionFactory实例,依此类推

每个数据库对应一个SqlSessionFactory实例

元素除可通过id指定唯一标识之外,还可包含如下两个有序的子元素。

  • transactionManager:配置事务管理器
  • dataSource:配置数据源

下面详细介绍事务管理器和数据源配置

事务管理器

配置事务管理器使用元素,该元素唯一可指定的属性为

  • type:指定事务管理器的类型

该type指定事务管理器的实现类,此处同样既可使用全限定类名,也可使用预定义的类别名
MyBatis默认提供了两个事务管理器的实现类

  • org.apache.ibatis.transaction.jdbc.Jdbc.TransactionFactory:Gaia实现类直接使用JDBC自带的事务提交和回滚,它依赖于从数据源得到的数据库连接(Connection)来管理事务,该实现类预定义的类别名是JDBC,因此在前面配置文件中都将type属性设为JDBC
  • org.apache.ibatis.transaction.managed.ManagedTransactionFactory:该实现类对事务控制什么都不做,它既不会提交事务,也不会回滚事务,而是直接让容器(比如JEE应用服务器的事务上下文)来管理事务生命周期。该实现类预定义的类别名是MANAGED,因此一般将type属性设置为MANAGED即可

在默认情况下,ManagedTransactionFactory事务管理工厂会自动关闭数据库连接,但有些容器却希望自己来处理数据库连接,不希望MyBatis自动关闭数据库连接,因此可通过将closeConnection属性设为false来阻止它默认关闭行为。例如如下配置片段

<transactionManager type="MANAGED">
	<property name="closeConnection" value="false">
transactionManager>

需要说明的是,如果使用Srping+MyBatis整合开发,Spring的事务管理器会接管底层的事务,因此不再需要配置事务管理器
MyBatis完全允许开发者使用自定义的事务管理器,开发者只要提供一个TransactionFactory接口的实现类(JdbcTransactionFactory、ManagedTransactionFactory都实现了该接口),接下来既可将该实现类的全限定类名或类别名设为type属性值,这样MyBatis就会使用自定义的事务管理器了。

TransactionFactory接口内有三个抽象方法

  • void setProperties(Properties props):该方法负责将元素内配置的所有属性以Properties对象传入
  • Transaction newTransaction(DataSource dataSource,TransactionIsolationLevel level,boolean autoCommit):该方法使用指定的事务隔离、自动提交属性开启一个新事务

由于两个newTransaction()方法都必须返回一个Transaction对象,因此开发自定义事务工产还必须提供
自定义的Transaction实现类,其中Transaction接口的代码如下:

public interface Transaction{
	Connection getConnection()throws SQLException;
	void commit()throws SQLException;
	void rollback()throws SQLException;
	void close()throws SQLException;
	Integer getTimeout()throws SQLException;
}

通过为TransactionFactory、Transaction接口提供实现类,开发者可以使用自定义的事务管理器来代替MyBatis的内置事务管理器
下面示例开发一个基于JDBC数据库连接的事务管理器,该事务管理器的实现类可以代替MyBatis内置的JdbcTransactionFactory实现类,而且该事务管理器允许设置超时时长,因此比JdbcTransactionFactory更实用
下面首先实现一个TransactionFactory接口的实现类,该实现类的代码较为简单,如下所示

//自定义事务工厂,实现TransactionFactory接口
public class FkTransactionFactory implements TransactionFactory{
	private Integer timeout;
	@Override
	public void setProperties(Properties props){
		//将配置的timeout属性转换成整数后赋值给timeout属性
		this.timeout=Integer.parseInt(props.getProperty("timeout"));
	}
	@Override
	public Transaction newTransaction(Connection conn){
		//返回自定义的FkTransaction
		retrun new FkTransaction(timeout,conn);
	}
	@Override
	public Transaction newTransaction(DataSource ds,
	TransactionIsolationLevel level,boolean autoCommit){
		//返回自定义的FkTransaction
		return new FkTransaction(timeout,ds,level,autoCommit);
	}
}

从上面的实现类代码可以看出,FkTransactionFactory实现了setProperties()方法,该方法可获取在事务管理器中配置的timeout属性,该属性将会用于控制超时时长
上面的实现类还实现了两个重载的newTransaction()方法,这两个方法都返回了FkTransaction对象,FkTransaction才是自定义事务管理器的关键实现类,该实现类必须实现Transaction接口中定义的5个抽象方法

public class FkTransaction implements Transaction
{
	private Integer timeout;
	protected Connection connection;
	protected DataSource dataSource;
	protected TransactionIsolationLevel level;
	protected boolean autoCommit;
	public FkTransaction(Integer timeout,DataSource ds,
	TransactionIsolationLevel desiredLevel,boolean desiredAutoCommit){
		this.timeout=timeout;
		dataSource=ds;
		level=desiredLevel;
		autoCommit=desiredAutoCommit;
	}
	public FkTransaction(Integer timeout,Connection connection){
		this.timeout=timeout;
		this.connection=connection;
	}
	@Override
	public Connection getConnection()throws SQLException{
		if(connection==null){
			openConnection();
		}
		return connection;
	}
	@Override
	public void commit()throws SQLException{
		//如果连接不为null,且不自动提交
		if(connection != null && !connection.getAutoCommit()){
			//提交事务
			connection.commit();
		}
	}
	@Override
	public void rollback()throws SQLException{
		//如果连接不为null,且不是自动提交
		if(connection !=null && !connection.getAutoCommit()){
			//回滚事务
			connection.rollback();
		}
	}
	@Override
	public void close()throws SQLException{
		if(connection != null){
			//将自动提交行为恢复为true
			connection.setAutoCommit(true);
			//关闭连接
			connection.close();
		}
	}
	//定义打开数据库连接的方法
	protected void openConnection()throws SQLException{
		connection=dataSource.getConnection();
		//设置事务隔离级别
		if(level != null){
			connection.setAutoCommit(autoCommit);
		}
	}
	@Override
	public Integer getTimeout()throws SQLException{
		return this.timeout;
	}
}

提交事务的行为其实就是依赖Connection对象的commit()方法;回滚事务的行为就是依赖Connection对象的rollback()方法,这说明该事务管理器其实是基于Connection(JDBC数据库连接对象)管理事务的。在Close()中是关闭数据库连接
提供上面两个实现类之后,接下来既可在元素中启用该事务管理器。下面的配置代码即可启用自定义的事务管理器

<configuration>
	<typeAliases>
		
		<package name="org.crazyit.app.domain" />
	typeAliases>
	
	<environments default="mysql">
		
		<environment id="mysql">
			
			<transactionManager type="org.crazyit.app.transaction.FkTransactionFactory">
			<property name="timeout" value="5">
			transactionManager>
			
			<dataSource type="POOLED">
				<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
				<property name="url" value="..."/>
				<property name="username" value="..."/>
				<property name="password" value="..."/>
			dataSource>
		environment>
	environments>
	<mappers>
		
		<package name="org.crazyit.app.dao" />
	mappers>
configuration>

上面的事务管理器使用FkTransactionFactory作为事务管理器,而且程序还为该事务管理器设置了timeout属性,该timeout属性会控制事务的超时时长

数据源配置

MyBatis使用数据源管理数据库连接,这样以便更好地复用数据库的连接来提高程序性能
MyBatis内置了三种数据源实现

  • UNPOOLED:不使用连接池,这种方式每次都会重新打开连接
  • POOLED:使用MyBatis内置连接池的数据源
  • JNDI:使用容器管理的数据源,这种方式需要在提供数据源支持的容器(如Tomcat)中使用

事务管理器类型,此处的数据源配置同样也是别名,上面的UNPOOLED、POLLED和JNDI分别对应于UnpooledDataSourceFactory、PooledDataSourceFactory和JndiDataSourceFactory实现类

当使用UNPOOLED类型的配置时,只需指定如下属性

  • driver:指定JDBC驱动的全限定类名
  • url:指定数据库的URL地址
  • username:指定登录数据库的用户名
  • password:指定登录数据库的密码
  • defaultTransactionIsolationLevel:配置默认的事务隔离级别

此外,如果需要传递额外的属性给数据库驱动,则可选择在属性名之前添加“driver”。例如,要为数据库驱动指定字符串的属性,则可进行如下配置:

driver.encoding=UTF8

由于UNPOOLED并不是基于连接池的,因此它不需要配置与连接池相关的信息;而POOLED采用连接池来管理数据库连接,因此它除可指定上面列出的属性之外,还可指定以下连接池相关的配置信息

  • poolMaximnumActiveConnections:指定最大的活动连接(正在使用的连接)的数量
  • poolMaximnumIdleConnections:指定最大的空闲连接(不是正在使用的连接)的数量
  • poolMaximumCheckoutTime:指定连接池中的连接在强制返回之前,从连接池中被检出(check out)的时间,默认值为20 000毫秒(即20秒)
  • poolTimeToWait:如果获取数据库连接超过该配置指定的时间,连接池打印日志,并重新尝试获取新的连接。默认值为20 000毫秒(即20秒)
  • poolMaximumLocalBadConnectionTolerance:这是一个关于坏连接容忍度的底层配置。当一条线程尝试从连接池获取连接时,如果该连接获取到一个无效的连接,该配置允许该线程尝试重新获取一个新的有效连接,但重新尝试的次数不能超过poolMaximumIdleConnections与poolMaximnumLocalBadConnectionTolerance之和,默认值为3
  • poolPingQuery:指定用于执行查询检查的SQL语句。默认值为“NO PING QUERY SET”,它会在数据库驱动失败时显示合适的错误信息
  • poolPingEnabled:配置是否启用查询检查。如果开启该配置,连接池会周期性地执行poolPingQuery属性指定的查询语句,因此poolPingQuery应该被设为一条可执行的SQL语句(最好耗时非常少),开启该配置会降低连接池的性能,默认值为false
  • poolPingConnectionsNotUsedFor:配置poolPingQuery的频率,建议将该属性设为与数据库连接的超时时长相同,从而避免不必要的检查。默认值为0,该属性仅当poolPingEnabled为true时有效
    如果使用JNDI的连接池配置,则意味着使用第三方容器提供的连接池,因此只要配置容器提供的连接池的JNDI名称即可
  • initial_context:该属性用于在JNDI上下文中获取InitialContext
  • data_source:该属性指定数据源在JDNI上下文的JNDI路径。如果提供initial_context配置,则会使用该配置返回的InitialContext实例的lookup()方法来查找数据源;否则,将直接调用InitialContext类的lookup()方法来查找数据源

此外,如果要将配置属性传给InitialContext,则可通过添加“env.”前缀来实现。例如,下面配置将encoding属性传给InitalContext

env.encoding=UTF8

除MyBatis官方提供上述的三种数据源配置之外,如果开发者需要使用其他数据源,则可通过自定义DataSourceFactory来实现

跳过2-6和2-7的内容

2-7支持不同类型的数据库

2-8Mapper基础

XML Mapper的根元素是,在该元素内只能包含如下几个无序的子元素

  • cache:用于启用当前命名空间的缓存设置
  • cache-ref:用于引用其他命名空间的缓存设置
  • resultMap:用于定义ResultSet与Java对象之间的映射。该元素的功能非常强大,用起来也较为复杂
  • sql:用于定义可复用的SQL语句块
  • insert:通常用于定义insert语句。从功能上看,它完全可用于定义其他DML语句
  • update:用于定义update语句。从功能上看,它完全可用于定义DML语句
  • delete:用于定义delete语句。从功能上看,它完全可用于定义其他DML语句
  • select:用于定义select语句

读者可能发现,MyBatis官方说明文档说上面这些元素应按顺序定义(in the order that they should be defined),但实际上查看元素的DTD定义,会发现如下内容

<! ELEMENT mapper(cache-ref|cache|resultMap*|parameterMap*|sql*|insert*|update*|delete*|select*)+>
<!ATTLIST mapper
namespace CDATA #IMPLIED
>

从该DTD片段可明显看到,元素内的子元素其实是无序的,而且从实际代码的测试来看,子元素的也不要求按顺序排列,可见此处应该也是MyBatis官方的纰漏

MyBatis早期还支持在元素中定义多个子元素,但这个元素现已经被标记为过时,而是该元素现在也没有使用价值了,故本书不再介绍该元素

select的用法

正如前面示例所示的,元素或@Select注解用于定义一条select语句,在使用元素时必须指定id属性,该属性值将作为这条select语句的唯一标识
此外,还可为元素指定如下属性

  • parameterType:指定传给这条SQL语句的参数的类型,该属性值支持全限定类名或别名。实际上该属性是可选的,因此MyBatis的类型处理器能通过反射推断出参数的类型
  • resultType:指定ResultSet的每行记录需要映射的Java对象的类型。可见,当查询返回集合时,该属性值应该是集合元素的类型,而不是集合本身的类型。该属性值支持全限定类名或别名
  • resultMap:指定对元素的引用,该属性值应该是另一个元素的id属性

元素要么使用resultType属性指定ResultSet的每行记录对应的Java类型,要么使用resultMap引用另一个元素,这两个属性不能同时使用

  • flushCache:如果将该属性设置为true,则意味着只要语句被调用,总会清空局部缓存和二级缓存。默认值为false

  • useCache:如果将该属性设置为true,则意味着该语句的执行结果将被二级缓存所缓存。默认值:对select语句为true
    本书第三章将会详细介绍MyBatis缓存的应用

  • timeout:指定查询返回结果集的超时时长,时间单位是秒。默认值为unset(依赖于底层驱动)

  • fetchSize:设置数据库驱动批量抓取的记录数,该属性只是给底层驱动的建议,并不保证效果

  • statementType:指定MyBatis用于SQL语句的Statement的类型。该属性支持STATEMENT(普通Statement)、PREPARED(PreparedStatement)或CALLABLE(CallableStatement,用于执行存储过程)其中之一,默认值为PREPARED

尽量不要将该属性设置为STATEMENT,这意味着强制MyBatis使用普通Statement来执行SQL语句,普通Statement不仅性能差,而且有SQL注入漏洞,当使用指定存储过程时,该属性应被设置为CALLABLE,本书后面会有使用MyBatis执行存储过程的详细示例

  • resultSetType:设置ResultSet的类型,即控制查询返回的ResultSet是否为可滚动的结果集。该属性支持FORWARD_ONLY(不可滚动)、SCROLL_SEN或DEFAULTSITIVE(可滚动,对修改敏感)、SCROLL_INSENSITIVE(可滚动,对修改不敏感)或DEFAULT(等价于unset)其中之一,默认值为unset(依赖于底层驱动)
  • databaseId:设置数据库类型的别名,如果设置了该属性,则意味着这条SQL语句仅对该类型的数据库有效
  • resultOrdered:该属性仅对嵌套结果的select语句使用,如果将该属性设置为true,即认为包含了嵌套结果集或分组,这样当返回一个主结果行的时候,就不会发生对前面结果集进行引用的情况了。默认值为false
  • resultSets:该属性仅对查询返回多结果集(调用存储过程)时有效。该属性用于为多结果集分配名称,各名称之间以英文逗号隔开

本书第三章将会详细介绍多结果及的应用

如果使用@Select注解,该注解仅能指定一个value属性,用于定义SQL语句。如果需要为@Select注解指定类似于元素的额外属性,则需要使用@Options注解

@Options注解支持如下属性

  • int fetchSize:等同于元素的fetchSize属性
  • Options.FlushCachePolicy flushCache:等同于flushCache属性
  • String keyColumn:等同于元素的keyColumn属性
  • String keyProperty:等同于元素的keyProperty属性
  • String resultSets:等同于元素的resultSets属性
  • ResultSetType resultSetType:等同于元素的resultSetType属性
  • StatementType statementType:等同于statementType属性
  • int timeout:等同于timeout属性
  • boolean useCache:等同于useCache
  • boolean useGeneratedKeys:等同于元素的useGeneratedKeys属性

从上面介绍不难看出,@Options注解不仅仅用于为@Select注解提供支持,也用于为@Insert、@Delete、@Update注解提供支持
当使用@Select注解定义select语句时,如果只是基于简单的“同名映射”规则,MyBatis可以通过反射推断出ResultSet的每行记录映射的Java对象的类型,因此无须指定resultType属性

例如,如下示例的Mapper接口使用了@Select注解定义select语句

public interface NewsMapper{
	//基于“同名映射”规则,MyBatis自动推断出ResultSet的每行记录映射的Java对象的类型
	@Select("select news_id id,news_title title,news_content content"+
	"from news_inf where news_id>#{id}")
	List<News>findNews(int id);
}
public static void insertTest(SqlSession sqlSession) {
		News news = new News(null,"奥迪A3","20万内落地,感觉还不错");
		var newsMapper = sqlSession.getMapper(NewsMapper.class);
		List<News> m = newsMapper.findAll();
		m.forEach(System.out::println);
	}
//定义Mapper接口(DAO)接口,该接口由MyBatis负责提供实现类
public interface NewsMapper {
@Select("select*from news_inf")
	@Results({
		@Result(column = "news_id",property="id"),
		@Result(column = "news_title",property="title"),
		@Result(column = "news_content",property="content")
	})
	List<News> findAll();
}

上面程序中的@Select注解定义了select语句,但由于findNews()方法的返回值类型时List,因此MyBatis可推断出ResultSet的每行记录映射的Java对象为News类型
需要说明的是,当使用@Select注解定义select语句时,由于没有指定resultType属性,因此必须将该方法的返回值类型声明为List,这样MyBatis才可推断出ResultSet的每行记录映射的Java对象的类型;如果开发者将该方法的返回值类型声明为简单的List,那么MyBatis就无法推断出ResultSet的每行记录映射的Java对象的类型,程序就会报错。
使用主程序调用上面Mapper组件的findNews()方法,将会看到程序运行完全正常
其实select语句是数据库操作中最复杂的用法之一,这种复杂的用法通常都需要结合resultMap来进行结果集映射,本书第三章将会详细介绍resultMap的内容,故此处不再进一步讲解

insert的用法

元素或@Insert注解用于定义一条insert语句。实际上,元素(或@Update注解)、元素(或@Delete注解)与元素(或@Insert注解)的用法非常相似。它们都可以定义DML语句
在使用元素时同样必须指定id属性,该属性值将作为它们定义的DML语句的唯一标识
此外,这可为这三个元素指定如下属性

  • parameterType:指定传给这条SQL语句的参数的类型,该属性值支持全限定类名或别名
    实际上该属性是可选的,因此MyBatis的类型处理器能通过反射推断出参数的类型
  • flushCache:如果将该属性设置为true,则意味着只要语句被调用,总会清空局部缓存和二级缓存。默认值为true
  • timeout:指定查询返回结果集的超时时长,时间单位是秒。默认值为unset(依赖于底层驱动)
  • statementType:指定MyBatis用于SQL语句的Statement的类型,该属性支持STATEMENT(普通Statement)、PREPARED(PreparedStatement)或CALLABLE(CallableStatement,用于执行存储过程)其中之一,默认值为PREPARED
  • useGeneratedKeys:该属性通常仅对insert语句有效,该属性值指定是否将数据库内部(自增长)生成的主键值传给作为参数的Java对象。默认值为false
  • keyProperty:该属性需要与useGeneratedKeys结合使用,用于告诉MyBatis将数据库内部生成的主键值传给Java对象的哪个属性——主键列对应于key属性。默认值为unset(未设置)。如果希望得到多个列的值,也可指定用逗号分隔的多个属性名的列表
  • keyColumn:该属性指定用于保存keyProperty的列名,该属性可不用指定
  • databaseId:设置数据库类型的别名。如果设置了该属性,则意味着这条SQL语句仅对该类型的数据库有效

当使用@Insert注解来定义insert语句时,如果程序需要为该注解指定额外的属性,则可使用@Options注解来定义这些属性。虽然元素或@Options注解都可以指定上面这些
但实际开发时通常无须指定这些属性,直接使用默认值即可

例如,如下Mapper接口直接使用@Insert注解定义了insert语句

public interface NewsMapper{
	@Insert("insert into news_inf values"
	+"(null,#{title},#{content})")
	int saveNews(News news);
}
News news = new News(null,"奥迪A3","新款1.5T");
int n = mapper.saveNews(news);
System.out.println("插入了%d条记录%n",n);//不要把逗号写成+连接符

上面程序中的@Insert注解定义了insert语句,但由于saveNews()方法的参数类型是News,因此无须使用@Options注解指定parameterType属性
使用主程序调用上面Mapper组件的saveNews()方法,将会看到程序完全允许正常

使用useGeneratedKeys返回自增长的主键值

当为数据库的主键列设置了自增长特性之后,程序在插入记录时会自动生成主键值,如果需要MyBatis将数据库自动生成的主键值传回参数对象,则可将useGeneratedKeys属性设置为true
当将useGeneratedKeys属性设置为true之后,还需要通过keyProperty告诉MyBatis将主键值传给参数对象的哪个属性

例如,如下XML Mapper配置会让MyBatis将数据库自动生成的主键值传给参数对象的id属性

<insert id="saveNews" useGeneratedKeys="true" keyProperty="id" >
		insert into news_inf values
		(null,#{title},#{content})
	insert>

上面代码配置了useGeneratedKeys和keyProperty属性,这样程序在插入记录时,MyBatis会将数据库生成的主键值传给参数对象的id属性

下面是主程序中插入记录的方法代码

public static void selectTest(SqlSession sqlSession) {
		var news = new News(null,"奥迪A4L","高配要30W落地");
		//获取Mapper对象
		var newsMapper = sqlSession.getMapper(NewsMapper.class);
		//调用Mapper对象的方法执行持久化操作
		var n = newsMapper.insertNew(news);
		System.out.printf("插入了%d条记录%n",n);
		System.out.println("数据库为新插入记录生成主键为:"+news.getId());
		//提交事务
		sqlSession.commit();
		sqlSession.close();
	}
//Mapper抽象类(DAO接口)
int insertNew(News news);

第二章 MyBatis的基础用法_第36张图片

由于底层news_inf数据表的news_id列是自增长的,程序在插入记录时无须为该列分配值,因此上面程序在创建News对象时将id属性设置为null即可
上面方法依然按原有的方式添加记录,当添加记录完成后,方法中的代码获取了news参数的id属性——该id属性值就是新插入记录的自增长主键值
如果使用@Select注解定义SQL语句,那么配置@Options注解指定useGeneratedKeys和keyProperty属性同样可获取数据库生成的自增长主键值

下面Mapper接口的注解示范了这种用法

public interface NewsMapper
{
	@Insert("insert into news_inf values"+"(null,#{title},#{content})")
	@Options(useGeneratedKeys = true,keyProperty = "id")
	int CommentInsert(News news);
}

在主方法的调用代码与上面没有区别,将方法名改改就行
第二章 MyBatis的基础用法_第37张图片

使用上面的Mapper组件保存News对象后,同样可通过getId()方法来获取数据库生成的主键值

使用selectKey生成主键值

还有一种数据库(如Oracle、PostgreSQL等)并不支持自增长主键值,而是使用sequence来生成徐立志,再将序列值作为新纪录的主键值

虽然PostgreSQL并不支持自增长主键值,但是只要在创建数据表时将主键列定义成serial类型,PostgreSQL就会在底层为该列创建序列,并将该序列关联到该主键列,这样该主键列其实也相当于拥有了自增长功能

为了演示使用序列生成主键值的情形,本书并利用PostgreSQL的serial类型的主键列,而是分开创建数据表和序列的。下面是本例所用的PostgreSQL数据库的SQL脚本

create database mybatis;
\c mybatis;
-- 创建数据表
create table news_inf
(
news_id integer primary key,
news_title varchar(255),
news_content varchar(255)
);
-- 创建序列
create sequence news_inf_seq;

程序需要在保存参数对象之前,先设置参数对象的主键列属性的值——该值应该是从数据库序列中获取的。此时可借助元素来实现
通常将元素放在元素内使用,在使用该元素时可指定如下属性

  • keyProperty:指定selectKey定义的语句获取的值用于设置参数对象的哪个属性。如果希望为多个列生成值,该属性也可以是用逗号分隔的属性名列表
  • keyColumn:该属性指定用于保存keyProperty的列名。该属性可不用指定
  • resultType:指定selectKey定义的语句获取的值类型。通常MyBatis可以推断出该值的类型,但明确指定类型会更加精准
  • order:该属性可被设置为BEFORE或AFTER。如果被设置为BEFORE,MyBatis会执行selectKey语句来获取主键值,为参数对象的keyProperty属性设置值,然后执行插入语句;如果被设置为AFTER,MyBatis会先执行插入语句,然后再执行selectKey语句来获取主键值,为参数对象的keyProperty属性设置值
  • statementType:指定用哪种Statement来执行selectKey语句。该属性同样支持STATEMENT,PRERARED或CALLABLE其中之一,其意义与等元素的statementType属性的意义完全相同

下面XML Mapper使用元素先为参数对象设置了主键属性值,然后保存数据

<mapper namespace="org.crazyit.app.dao.NewsMapper">
	<insert id="saveNews">
		
		<selectKey keyProperty="id" resultType="int" order="BEFORE">
			select nextval('news_inf_seq')
		selectKey>
		insert into news_inf values(#{id},#{title},#{content})
		
	insert>
mapper>

上面元素配置的SQL语句为select nextval(‘news_inf_seq’),这条SQL语句用于获取news_inf_seq序列的下一个序列值

不同数据库获取下一个序列值的语法并不相同,比如Oracle就不用这条语句,具体请参考各数据库的相关文档

元素的keyProperty属性值为id,说明它对应的SQL语句获取的值将会被设置给参数对象的id属性。order="BEFORE"表明显执行定义的SQL语句
使用主程序调用该Mapper组件的方法来获取主键值

@SelectKey注解可替代元素,该注解支持如下属性

  • boolean before:等同于的order属性
  • String keyProperty:等同于的keyProperty属性
  • ClassresultType:等同于的resultType属性
  • String[]statement:等同于元素中定义的SQL语句
  • String keyColumn:等同于的keyColumn属性

下面将演示通过注解实现

public interface NewsMapper{
	@Insert("insert into news_inf values(#{id},#{title},#{content})")
	//先从序列获取主键值,该SQL语句返回的值将被设置给参数对象的id属性
	@SelectKey(keyProperty = "id",resultType=Integer.class,before=true,
	statement="select nextval('news_inf_seq')")
	int saveNews(News news);
}

update和delete元素的用法

(或@Update注解)与(或@Delete)注解通常用于定义update、delete语句(虽然完全也可定义insert语句),它们支持的属性与元素支持的属性也差不多
下面的Mapper接口则示范了使用@Update、@Delete定义SQL语句

public interface NewsMapper
{
	@Update("update news_inf set news_title=#{title},"
	+"news_content=#{content} where news_Id=#{id}")
	int updateNews(News news);
	@Delete("delete from news_inf where news_id=#{id}")
	int deleteNews(int id);
}
public static void CommentUpdateTest(SqlSession sqlSession) {
		var news = new News(50,"明天学习sql元素","定义可复用的SQL片段");
		//获取Mapper对象
		var newsMapper = sqlSession.getMapper(NewsMapper.class);
		//调用Mapper对象的方法执行持久化操作
		var n = newsMapper.updateNews(news);
		System.out.printf("修改了%d条记录%n",n);
		//提交事务
		sqlSession.commit();
		sqlSession.close();
	}
public static void CommentdeleteTest(SqlSession sqlSession) {
		//获取Mapper对象
		var newsMapper = sqlSession.getMapper(NewsMapper.class);
		//调用Mapper对象的方法执行持久化操作
		var n = newsMapper.deleteNews(49);
		if(n==1) {
			System.out.println("删除成功!");
		}else {
			System.out.println("删除失败!");
		}
		//提交事务
		sqlSession.commit();
		sqlSession.close();
	}

上面XML Mapper的两行粗体代码示范了使用@Update和@Delete来定义update、delete语句,该Mapper组件的这两个方法即可用于更新、删除news_inf表中的记录

使用sql元素定义可复用的SQL片段

使用元素定义可复用的SQL片段,可以简化XML Mapper中的SQL定义,元素需要与元素结合使用,其中:

  • 元素定义SQL片段,并指定id属性
  • 元素通过refid属性来引用指定的SQL片段

当使用元素定义SQL片段时,可以通过${}形式定义参数占位符;而使用元素则可通过子元素为SQL片段中的参数占位符传入值
MyBatis会在加载XML Mapper时,将子元素中定义的参数值传给¥{}形式的占位符。这是一种基于“字符串”替换的方式,当MyBatis实际执行SQL语句时,¥{}形式占位符已经被替换成相应的参数值了

例如,如下XML Mapper使用元素定义了SQL片段

<mapper namespace="org.crazyit.app.dao.NewsMapper">
	
	<sql id="newsColumns">
		${alias}.news_id id,
		${alias}.news_title title,
		${alias}.news_content content
	sql>
	<select id = "getNews" result="news">
		select 
		
		<include refid="newsColumns">
			<property name="alias" value="ni" />
		include>
		from news_inf ni where ni.news_id = #{id}
	select>
	<select id="findNewsByTitle" resultType="news">
		select
		
		<include refid="newsColumns">
			<property name="alias" value="t"/>
		include>
		from news_inf t.news_title like #{titlePattern}
	select>
mapper>

上面程序第一段sql定义了一个SQL片段,该SQL片段的id为newsColumns;第二段使用include元素引用了id为newsColumns的SQL片段,并将其中alias参数替换成了ni;第三段元素引用了id为newsColumns的SQL片段,并将其中的alias参数替换成t

上面SQL数据来看,它们引用了相同的SQL片段,只是SQL片段中的${alias}占位符不同

这两条SQL语句已经看不出${alias}占位符存在的痕迹,这是由于MyBatis在加载XML Mapper时已经处理了alias参数

参数处理

Mapper中SQL配置参数处理分以下几种情况

  • 单个标量参数,标量类型代表8个基本类型及其包装类、String类型等
  • 单个符合参数或Map参数
  • 多个参数

1.单个标量参数
当只是单个标量类型时,参数值会被直接传给Mapper中SQL语句的占位符#{},而且#{}中的参数名可以随意定义
例如,在Mapper接口中有如下方法

List<News>findNewsByTitle(String title);

上面方法只包含一个String类型的title参数,该参数是标量类型的,因此该方法对应的元素可被定义成如下形式:

<select id="findNewsByTitle" resultType="news">
	select news_id id,news_title title,news_content content
	from news_inf
	where news_title like #{a}
select>

虽然方法参数名为title,但是SQL中用#{a}就可代表该参数,实际上可以任意填写
程序调用方法的参数值会直接传给SQL语句中唯一#{}占位符——不管#{}中的名称是什么

2.单个符合参数或Map参数
当接口方法只包含单个符合类型的参数或Map参数时,参数对象的属性或Map的value会被传给SQL的#{}占位符,占位符的参数名必须和符合对象属性名或Map的key对应
此外,#{}还支持表达式,例如#{user.name},如果参数是符合对象,那么它代表获取参数对象的getUser().getName()方法的值;如果是Map,那么它代表获取Map中user对应的value的getName()的返回值

3.多个参数
当有多个参数时,#{}占位符的名称必须先指定参数本身,然后可以使用表达式

在#{}占位符中指定参数,通常支持三种方式

  • 使用MyBatis的内置名称。MyBatis使用arg0、arg1、arg2等分别代表第一个、第二个、第三个参数等,也可以使用param1、param2、param3等分别代表第一个、第二个、第三个等
    如果使用argN代表参数,N从0开始;如果使用paramN代表参数,N从1开始
  • 使用@Param注解修饰方法形参,通过该注解可以为Mapper接口中方法的参数指定参数名,接下来既可在#{}中通过名称来引用参数了
  • 使用Java8及以上版本中的-parameters选项编译项目。通过这种方式编译的*.class文件可以保留方法的形参名,接下来既可在#{}中通过形参名来引用参数了

参数的额外声明

正如之前示例,#{}占位符除可指定参数名之后,还可通过typeHandler指定类型处理器,总结来说,#{}还可指定如下额外属性

  • javaType:指定该参数的Java类型
  • jdbcType:指定该参数的JDBC类型
  • numericScale:对于数值类型的参数,指定小数点后的位数
  • typeHandler:指定处理该参数的类型处理器
  • mode:指定该参数的传参模式
  • resultMap:当该参数是游标类型时,可通过该属性将游标对应的结果集映射到指定的Java类型处理器

需要说明的是,如果某一列允许为null,而且可能需要为该参数传入null作为参数值,那么久必须为该参数指定jdbcType属性——这是因为PreparedStatement中setNull(int parameterIndex,int sqlType)方法的第二个参数必须指定sqlType,而MyBatis无法根据null来推断出它的类型

对于允许为null的列,如果该列对应的参数可能传入null值,那就必须为参数对应的#{}部分指定javaType属性

字符串替换

#{}和${}的区别就是,#{}会将内容变成?,而¥{}会将它的值变成占位符的名字,而非?

2-9MyBatis代码生成器

没什么用,略

你可能感兴趣的:(笔记,学习,开发语言,java)