第二章 MyBatis的基础用法
本章目录:
MyBatis是一个古老的框架,现在仍有很多项目使用MyBatis技术
MyBatis与普通ORM框架不同,MyBatis并不是真正的ORM框架,它只是一个半自动的SQL映射框架。从字面上看,MyBatis没有ORM框架的功能强大(因为是半自动的),但正是由于这种半自动,反而让MyBatis拥有更多的灵活性:①MyBatis需要开发者自己编写SQL语句,因此开发者可以充分的自由执行SQL优化,但是对开发者具有一定的SQL功底要求;②MyBatis需要开发者自己定义ResultSet与对象之间的映射关系,因此可以简单地避免循环引用等问题
ORM的全称是Object/Relation Mapping,即对象/关系映射。可以将ORM理解为一种规范:完成面向对象编程语言到关系数据库的映射,ORM将Java的面向对象机制语言转换到数据库中进行交互,可以将ORM理解为数据之间的桥梁
因为数据库是关系型数据库,而Java语言是面向对象语言,它们的访问方式不同,如果来回切换,开发体验会非常糟糕,于是需要一种工具,它可以把关系数据库包装成面向对象的模型,这个工具就是ORM
ORM框架是解决面向对象程序设计和关系数据库发展不同步的一个中间解决方案,随着面向对象数据库的发展,最终会取代关系数据库,随着面向对象数据库的广泛使用,ORM工具就会自动消亡
面向对象数据库优势
关系型数据库优势
面对这种面向对象的程序设计语言与关系型数据库系统并存的局面,采用ORM就变成一种必然。采用ORM框架之后,应用程序不再直接访问底层数据库,而是以面向对象的方式来操作持久化对象)(例如创建、修改、删除等),ORM框架则将这些面向对象的操作转换成底层的SQL(结构化查询语言)操作。
ORM全称是对象/关系 映射,ORM工具提供了持久化类和数据表之间的映射关系,通过这种映射关系的过度,程序员可以很方便地通过持久化实现对数据表的操作
ORM基本映射有如下几种映射关系
下面将通过三张图来演示这三个映射关系大意
数据表映射类
数据表的行映射对象(即实例)
数据表的列(字段)映射对象的属性
数据表 -> 持久化类
数据表的字段 -> 持久化类的对象属性
数据表的字段的记录 -> 持久化类的对象属性的值
数据表和Java持久化类互相映射
数据表的字段和Java持久化类的属性互相映射
数据表的记录和Java持久化类属性的变量值内容互相映射
ORM工具的最大意义就是让开发者用面向对象的思维去操作关系数据库
严格来说,MyBatis并不是真正的ORM框架,它只是一个ResultSet映射框架
MyBatis与ORM框架不同,MyBatis并不会将表映射到持久化类。实际上,MyBatis完全没有“持久化类”的概念
MyBatis负责将JDBC查询得到的ResultSet映射成实体对象——同一条查询语句,在不同地方完全可以映射成不同的实体
下图可以看出,MyBatis先使用JDBC(默认使用PreparedStatement)执行查询,查询结果通常会返回一个ResultSet
ResultSet相当于一个包含多行、多列的数据表,ResultSet的每列都有列名,JDBC可通过ResultSetMetaData获取ResultSet包含的列信息,如各列的名称、类型等
开发者通过resultType或resultMap属性来指定转换Java对象
从上面的介绍不难看出,即使是一个ResultSet,程序也完全可以通过不同的resultType或resultMap属性值让MyBatis将它转换成不同的Java对象——这就是MyBatis令人着迷的魅力
虽然MyBatis很灵活,但是每次都需要烦琐的去设置sql语句和返回类型…否则MyBatis就会报错
在指定了resultType或resultMap属性之后,MyBatis就知道将每行记录转换为哪个Java类的实例了。接下来的问题是:MyBatis如何将各列的值传给Java对象的属性呢?MyBatis有两种方式
使用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
下载完3.5.2的MyBatis后,看如下文件结构
MyBatis底层依然是基于JDBC的,因此在应用程序中使用MyBatis执行持久化时同样少不了JDBC驱动
前面已经说了,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核心配置文件的主要部分
上面的配置文件使用
MyBatis提供了内置的数据源来管理数据库连接,因此可以无须第三方数据源。当然,MyBatis也支持切换使用第三方更优秀的数据源(比如C3P0等),后面会介绍使用第三方数据源的示例
数据源是一种提高数据库连接性能的常规手段,数据源会负责维持一个数据连接池,当程序创建数据源实例时,系统会一次性地创建多个数据库连接,并把这些数据库连接保存在连接池中。当程序需要进行数据库访问时,无须重新获取数据库连接,而是从连接池中取出一个空闲的数据库连接。当程序使用数据库连接访问数据库结束后,无须关闭数据库连接,而是将数据库连接归还给连接池即可。通过这种方式,就可避免因频繁获取数据库连接、关闭数据库连接所导致的性能下降
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文件更是简单,该文件的根元素是
在
上面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并不支持自动建表,因此需要手动创建数据表:
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底层依然使用JDBC访问数据库,因此同样少不了数据库驱动。此外,为了方便观察MyBatis底层执行的每条SQL语句及参数(这对于MyBatis学习有帮助),本书使用Log4j 2作为MyBatis的日志工具,因此还需要添加Log4j 2的两个JAR包
为了让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>
该日志配置文件中的
接下来使用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()方法需要两个参数
可见,SqlSession的insert()方法其实就是执行一条insert SQL语句——MyBatis的insert()方法在底层封装了下面的过程:
一、获取Connection对象(数据库连接)
二、根据insert()方法的第一个参数指定的SQL语句(现将#{}部分替换成问号)创建PreparedStatement
三、根据insert()方法的第二个参数为PreparedStatement设置参数
四、调用PreparedStatement的execute()方法执行SQL语句
这个过程就是MyBatis底层的源码实现,其实实现也非常简单,但对于开发者来说,使用MyBatis操作数据库就变得更简洁了,开发者只要关心两点
至于如何调用JDBC API来执行SQL语句、返回结果值,这些都交给MyBatis负责搞定
MyBatis底层执行的SQL语句
insert into news_inf values(null,?,?)
这就是在NewsMapper中定义的SQL语句,只是将#{}部分替换成了问号
从上面执行过程可以看出,MyBatis并不是真正的ORM框架,MyBatis只是对JDBC的简单封装。对比MyBatis和JDBC操作数据库的方式,不难发现这种封装至少带来了两种优势
正如前面的示例所示,MyBatis操作数据库的核心API是SqlSeesion,因此在调用MyBatis执行持久化操作之前,必须先获取SqlSeesion。
为了使用MyBatis进行持久化操作,通常有如下操作步骤
①使用Mapper定义SQL语句
②创建SqlSeesionFactoryBuilder
③调用SqlSessionFactoryBuilder对象的build()方法创建SqlSeesionFactory
④获取SqlSeesion(同时会打开事务)
⑤调用SqlSeesion的方法执行Mapper中的SQL语句(或通过Mapper对象来操作数据库)
⑥关闭事务,关闭SqlSession
如果需要对数据表中的记录进行更新,同样只需按上面步骤进行。上面步骤可以简化为两个核心步骤:
①在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文件中的
接下来在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>
上面代码使用
接下来在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>
我们想进行上面的完整操作需要先创建一个项目,并且按照书上的包创建出来,然后添加4个JAR包,MyBatis、JDBC、Log4j2
不过本次不知道什么原因,Log4j2日志并没有效果
在部署完环境开发后,首先创建MyBatis的核心配置文件-mybatis-config,该文件有2个大内容,数据源配置和映射文件
其次再创建Mapper映射文件,该文件是MyBatis的核心,通过该文件去管理SQL语句,达到SQL从程序中分离出来,该文件首先是创建一个mapper,在命名空间中输入名称,该名称用来配合标签元素id来达到唯一标识的效果,在调用MyBatis的API时,通过命名空间+元素id,来找到该标签的SQL语句,并且填充占位符参数,来达到执行的效果
因为此处我的日志并没有效果,所以现在暂时不在赘述
在测试类中,我们首先创建成员变量
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查询数据,同样还是执行上面两步,先在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();
}
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对应一个
②MyBatis通过反射为结果集的每行记录创建一个resultType(或resultMap)所指定的Java对象
③MyBatis根据列名(列别名)与Java对象属性名的对应关系将每行记录的数据设置为Java对象的属性
④整个ResultSet被依次转换成多个Java对象,多个Java对象被封装成List集合后返回。
运行上面的程序(保证news_inf表中id拥有)
前面的程序都是利用SqlSession的方法来执行SQL语句的,这些例子虽然上手简单,但它们至少存在如下两点不同:
接下来,对上一个实例针对这两点进行改进。首先为项目中的每一个领域对象都创建对应的类,而不是使用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增加如下接口
//定义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组件的方法操作数据库
正如前面所提到的,在使用任何IDE工具辅助开发之前,开发者都应该很清楚不使用工具如何使用该技术。IDE工具1仅用于辅助开发,提供开发效率,绝对无法弥补开发者的知识缺陷。
MyBatis为Eclipse提供了一个MyBatipse插件,该插件为XML Mapper编辑器提供了如下常用工具
MyBatipse也为Java编辑器提供了如下常用功能
前面介绍的例子都是使用XML Mapper定义SQL语句、映射关系的,实际上MyBatis同样允许不使用XML Mapper,而是直接在Mapper接口中使用注解来定义SQL语句、映射关系,本书后面的示例都会同时使用XML Mapper和注解两种方式
此外,MyBatipse插件元允许使用向导式的方式来创建XML Mapper,也允许在XML Mapper或Java编辑器面对MyBatis元素进行重命名,熟悉这些功能可以加速MyBatis的开发
为Eclipse安装MyBatipse插件
重复上述操作将MyBatis、Log4j_2和MySQL添加上
完成效果
刚才所添加的Eclipse用户库只能保证在Eclipse下编译、运行该程序可找到相应的类库;如果需要发布该应用,则还要将刚刚添加的用户库所引用的JAR文件岁应用一起发布。对于一个Web应用,由于在Eclipse中部署Web应用时不会将用户库的JAR文件复制到WEB-INF/lib路径下,因此需要主动将用户库所引用的JAR文件复制到Web应用的WEB-INF/lib路径下
将该文件放到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”按钮,将出现下图所示的对话框
④为XML Mapper选择保存位置,输入文件名之后,单击“Finish”按钮,即可成功创建XML Mapper文件。接下来使用Eclipse编译该文件时,可随时通过“Alt+/”快捷键调出MyBatipse插件的辅助功能,例如,在
从上图中可以看出,MyBatipse提示了XML Mapper允许在
加入选择定义
从上图可以看出,MyBatipse列出了
总之,读者随时可通过“Alt+/”快捷键调出MyBatipse的辅助功能,这样在某种程序度上提高开发效率。但最终编辑得到的NewsMapper.xml文件的内容依然与上一个示例完全相同,
这里还是要忠告开发者:不要过度依赖MyBatipse的提示功能!毕竟开发人员利用工具,不是工具利用开发人员。真正熟练的开发人员其实基本不靠这种插件提示——这样效率太低了!它们通常都是先赋值之前定义的内容,然后根据需要进行修改的
开发玩Mapper组件(Mapper+XML Mapper)后,接下来既可调用Mapper组件的方法来操作数据库了。这些代码很简单,此处不再赘述
从前面的程序中可以看到,在实现了Mapper组件之后,主程序要用到SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession等核心API。下面详细介绍这些核心API
SqlSessionFactoryBuilder甚至算不上核心API,从它的源码来看,它甚至没有专门定义构造器(Java会提供默认的、无参的构造器),它主要提供了如下方法
SqlSessionFactory build(Reader|InputStream reader,String environment,Properties properties)
实际上,该方法有多个重载版本。其中,第一个reader参数即可使用字符流,也可使用字节流(本质是一样的),都指定告诉SqlSessionFactoryBuilder到哪里去加载MyBatis核心配置文件
第二个environment参数引用了MyBatis配置文件中
第三个properties参数用于为MyBatis配置文件传入额外的属性配置。在重载的build()方法中,也可以不指定properties参数,表明不传入额外的属性配置
由此可见,SqlSessionFactoryBuilder的唯一作用就是创建SqlSessionFactory,一旦SqlSessionFactory对象创建成功,就再也不需要SqlSessionFactoryBuilder了。因此,建议在应用启动时,使用局部变量的SqlSessionFactoryBuilder来创建SqlSessionFactory,这样能保证该对象被尽快销毁,从而释放它底层用到的XMLConfigBuilder及XML资源。
以作者的经验来看,SqlSessionFactoryBuilder其实根本没有必要创建实例,Clinton(MyBatis的作者)在设置该类时将build()方法定义成static会更加简洁,可是他没没有这么做
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
execType参数支持以下三个枚举值
SqlSessionFactory底层封装了数据库配置环境(包括Datasource和事务配置),SqlSessionFactory应该是整个应用相关的,因此,建议在应用启动时创建SqlSessionFactory,整个应用在运行期间它应该一致存在,没有任何理由丢弃它或重新创建另一个实例
SqlSessionFactory的最佳作用域就是应用作用域,例如,使用单例模式或静态单例模式即可实现这一点。简单来说,应用不退出,SqlSessionFactory就不退出
SqlSessionFactory被设计成线程安全的实例,因此,它可以被多个线程共享
SqlSession是MyBatis执行数据库操作最核心的API,它提供了如下常用方法来执行SQL语句
正如前面所介绍的,上面的insert()、update()、delete()方法都可用于执行DML语句,它们只是语义上的区别,在功能上基本相似。这些方法在本章已经有过示例示范,实际上MyBatis不再推荐直接使用这些方法执行数据库操作
SqlSession还提供了如下方法用于控制事务
SqlSession提供了如下方法来支持批量更新
如果要使用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元素来利用该特性
SqlSession提供了如下方法来获取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();
}
MyBatis使用核心配置文件来管理相关配置信息,下面就来详细介绍这些配置的作用和用法。
MyBatis可以在核心配置文件中使用
通过使用属性,可以将核心配置文件中的部分信息(比如数据库连接信息)提取到属性文件中集中管理,或者以参数形式传给SqlSessionFactory的build()方法
总结来说,MyBatis允许在三个地方配置属性
当多出配置存在同名的属性时,优先级高的属性会覆盖优先级低的属性。上述三种方式所配置的属性优先级如下
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属性与url属性的区别如下
一般来说,通过resource属性指定要加载的属性文件中定义的属性。不难看出,引用属性值的语法格式如下
${属性名}
为了更好地演示MyBatis属性配置的加载机制,本例的属性配置略微有点复杂
下面是主程序代码,该程序会在使用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的三种属性配置方式及其优先级
上面示例演示了再三个地方配置属性,但实际项目中通常不需要搞得这么复杂,一般来说,将某种特定的信息(比如数据库连接信息)集中放在属性文件中配置,然后使用
当使用${属性名}来引用属性值时,可能存在属性值并不存在的情况,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>
从上面的配置可以看出,
正如从前面示例中看到的,很多时候项目可能并不需要为MyBatis配置这些设置,因为这些设置都有一个比较合理的默认值,通常使用默认值就能让MyBatis具有较好的行为。MyBatis支持的设置及其有效值、默认值如下表所示
在MyBatis的Mapper配置中必然涉及大量的类,在默认情况下,开发者必须为每个类都指定全限定类名,这样的代码不仅冗长,而且十分烦琐
MyBatis允许在
为了简化开发,MyBatis默认已经为常见的Java类型提供了别名。常见的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这样的全限定类名,因此配置文件就简洁多了
如果希望为指定包下的所有类指定别名,则可通过
在没有注解的情况下,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类的全限定类名,如下面的代码所示
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="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个方法
本示例将开发一个自定义对象工厂,该对象工厂会对生成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()方法,该方法将用于在
配置自定义工厂
在核心配置文件中可以使用
在下面的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组件之后,还需要在核心配置文件中进行配置,用于告诉MyBatis加载这些Mapper
前面在加载Mapper时使用了类似于如下的配置片段
<mappers>
<mapper resource="org/crazyit/app/dao/NewsMapper.xml"/>
mappers>
那么问题来了,MyBatis是否还支持加载Mapper的其他配置方式?答案是肯定的。MyBatis共支持4种加载Mapper的配置方式
<mappers>
<mapper url="file://G:/abc/NewsMapper.xml"/>
mappers>
<mappers>
<mapper class="org.crazyit.app.dao.NewsMapper" />
mappers>
虽然MyBatis提供了4种配置方式来加载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
MyBatis的功能就是处理Java对象与底层数据库之间的转换,无论是JavaMyBatis为PreparedStatement设置参数时,还是从ResultSet取出记录来封装Java对象时,都需要使用类型处理器(Type Handler)来处理Java类型与数据库类型之间的转换
简单来说,MyBatis的类型处理器类似于其他语言的类型转换器,它负责完成Java类型与JDBC类型之间的转换
类型处理器同样可以在MyBatis核心配置文件中配置,不过类型处理器的内容比较多,故本章将它单独作为一节来介绍
对于绝大部分开发场景而言,MyBatis使用者根本意识不到类型处理器的存在,这是因为MyBatis内置了大量的类型处理器,这些类型处理器基本可以处理日常开发处理的各种类型
MyBatis内置的类型处理器如下表所示
从上面的类型处理器可以看出,MyBatis已能支持Java8新增的日期、时间类型,MyBatis从3.4.5版本开始提供了内置的类型处理器来支持这些新增的日期、时间类型
对于一些特殊类型,MyBatis没有提供相应的类型处理器你,此时可以通过自定义类型处理器进行转换
与开发自定义对象工厂一样,开发自定义类型处理器同样只需要两部。
1、开发自定义类型处理类
2、在核心配置文件中配置类型处理器
1、开发自定义类型处理器类
自定义类型处理器类需要实现TypeHandler
MyBatis内置的类型处理器,也是通过继承BaseTypeHandler
通过继承BaseTypeHandler
上面列出了4个方法,但实际上只有两个,其中setNonNullParameter负责将Java类型的实例转换成数据库类型;而getNullableResult则负责将数据库类型的值转换成Java类型的实例
下图显示了这两个方法的功能示意图
本例将会示范把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两个注解修饰,这两个注解的作用如下
其实这两个注解是可选的,这意味着即使不添加这两个注解,MyBatis也可通过反射识别该类型处理器负责处理哪些JDBC类型与Java类型之间的转换。出于准确性考虑,建议添加这两个注解(至少添加@MappedJdbcTypes)
2、配置自定义类型处理器
在实现了自定义类型处理器之后,接下来还要在核心配置文件中使用
正如大家所想的,这两个属性与@MappedTypes和@MappedJdbcTypes两个注解的功能是类似的。因此,如果在定义类型处理器时已经添加了@MappedTypes和@MappedJdbcTypes两个注解,那么在配置类型处理器时可以不用指定javaType和jdbcType属性
需要指出的是,在
下面配置文件中配置了上面开发的自定义类型处理器
<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为枚举类型提供了如下两个类型处理器
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中为特定属性指定类型处理器的方式有两种:
本示例先为上一个示例的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时配置文件更加简洁,但它的功能比较若,它只能根据“同名映射”规则进行映射;指定resultMap属性时则比较复杂(需要额外配置
由于此处需要将record_season列映射成recordSeason属性,而且需要为该属性指定类型处理器,因此程序必须使用功能强大的
经过在Mapper中如上所示的配置,MyBatis就知道了EnumType来处理recordSeason属性
这种在Mapper中指定类型处理器的方式,既使用与在XML Mapper中指定,也适用于使用注解指定。使用注解指定类型处理器时,对于#{}的形式,同样在花括号中用typeHandler指定类型处理器,这与XML Mapper的形式并没有什么区别;至于XML Mapper中的
将上面一个示例中的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注解与
第二段代码则使用@Result定义了record_season列与recordSeason属性之间的对应关系,并通过typeHandler指定类型处理器。通过这段代码也可以看出,@Result注解与
很多Java框架都同时支持XML配置和注解两种方式,对于真正掌握了框架的开发者而言,无论是XML配置还是注解,用起来都没有太大的区别,因为它们的本质其实完全一样,它们需要提供的配置信息也是完全相同的,区别只是提供信息的载体不同而已
在本章第一个示例的MyBatis核心配置文件中,
由此可见,MyBatis允许在配置文件中配置多个数据库环境,这样可以为MyBatis项目的开发、测试或生产环境提供不同配置,也可在相同环境下分别使用不同的数据库。总之,每个环境对应一个数据库配置
使用
使用
例如如下配置片段
<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(该名字任意)的
回忆一下SqlSessionFactoryBuilder中build()方法的参数:
这两个build()方法都可传入一个environment参数——其实将该参数名定义成environmentId会更合适,该参数不能随便填写,它也对应于引用
假如有下配置片段:
<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参数传入哪个
如果在调用build()方法时没有传入environment参数,那么
虽然MyBatis可以配置多个数据库环境,但每个SqlSessionFactory实例只能选择一个环境:要么根据build()方法的environment参数选择,要么根据
MyBatis项目底层也可支持多个数据库,如果想连接两个数据库,就需要创建两个SqlSessionFactory实例,依此类推
每个数据库对应一个SqlSessionFactory实例
下面详细介绍事务管理器和数据源配置
配置事务管理器使用
该type指定事务管理器的实现类,此处同样既可使用全限定类名,也可使用预定义的类别名
MyBatis默认提供了两个事务管理器的实现类
在默认情况下,ManagedTransactionFactory事务管理工厂会自动关闭数据库连接,但有些容器却希望自己来处理数据库连接,不希望MyBatis自动关闭数据库连接,因此可通过将closeConnection属性设为false来阻止它默认关闭行为。例如如下配置片段
<transactionManager type="MANAGED">
<property name="closeConnection" value="false">
transactionManager>
需要说明的是,如果使用Srping+MyBatis整合开发,Spring的事务管理器会接管底层的事务,因此不再需要配置事务管理器
MyBatis完全允许开发者使用自定义的事务管理器,开发者只要提供一个TransactionFactory接口的实现类(JdbcTransactionFactory、ManagedTransactionFactory都实现了该接口),接下来既可将该实现类的全限定类名或类别名设为type属性值,这样MyBatis就会使用自定义的事务管理器了。
TransactionFactory接口内有三个抽象方法
由于两个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、POLLED和JNDI分别对应于UnpooledDataSourceFactory、PooledDataSourceFactory和JndiDataSourceFactory实现类
当使用UNPOOLED类型的配置时,只需指定如下属性
此外,如果需要传递额外的属性给数据库驱动,则可选择在属性名之前添加“driver”。例如,要为数据库驱动指定字符串的属性,则可进行如下配置:
driver.encoding=UTF8
由于UNPOOLED并不是基于连接池的,因此它不需要配置与连接池相关的信息;而POOLED采用连接池来管理数据库连接,因此它除可指定上面列出的属性之外,还可指定以下连接池相关的配置信息
此外,如果要将配置属性传给InitialContext,则可通过添加“env.”前缀来实现。例如,下面配置将encoding属性传给InitalContext
env.encoding=UTF8
除MyBatis官方提供上述的三种数据源配置之外,如果开发者需要使用其他数据源,则可通过自定义DataSourceFactory来实现
跳过2-6和2-7的内容
XML Mapper的根元素是
读者可能发现,MyBatis官方说明文档说上面这些元素应按顺序定义(in the order that they should be defined),但实际上查看
<! ELEMENT mapper(cache-ref|cache|resultMap*|parameterMap*|sql*|insert*|update*|delete*|select*)+>
<!ATTLIST mapper
namespace CDATA #IMPLIED
>
从该DTD片段可明显看到,
MyBatis早期还支持在
正如前面示例所示的,元素或@Select注解用于定义一条select语句,在使用元素时必须指定id属性,该属性值将作为这条select语句的唯一标识
此外,还可为元素指定如下属性
元素要么使用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执行存储过程的详细示例
本书第三章将会详细介绍多结果及的应用
如果使用@Select注解,该注解仅能指定一个value属性,用于定义SQL语句。如果需要为@Select注解指定类似于元素的额外属性,则需要使用@Options注解
@Options注解支持如下属性
从上面介绍不难看出,@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
需要说明的是,当使用@Select注解定义select语句时,由于没有指定resultType属性,因此必须将该方法的返回值类型声明为List
使用主程序调用上面Mapper组件的findNews()方法,将会看到程序运行完全正常
其实select语句是数据库操作中最复杂的用法之一,这种复杂的用法通常都需要结合resultMap来进行结果集映射,本书第三章将会详细介绍resultMap的内容,故此处不再进一步讲解
在使用
此外,这可为这三个元素指定如下属性
当使用@Insert注解来定义insert语句时,如果程序需要为该注解指定额外的属性,则可使用@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()方法,将会看到程序完全允许正常
当为数据库的主键列设置了自增长特性之后,程序在插入记录时会自动生成主键值,如果需要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);
由于底层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);
}
使用上面的Mapper组件保存News对象后,同样可通过getId()方法来获取数据库生成的主键值
还有一种数据库(如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;
程序需要在保存参数对象之前,先设置参数对象的主键列属性的值——该值应该是从数据库序列中获取的。此时可借助
通常将
下面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>
上面
不同数据库获取下一个序列值的语法并不相同,比如Oracle就不用这条语句,具体请参考各数据库的相关文档
该
使用主程序调用该Mapper组件的方法来获取主键值
@SelectKey注解可替代
下面将演示通过注解实现
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);
}
下面的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表中的记录
使用
当使用
MyBatis会在加载XML Mapper时,将
例如,如下XML Mapper使用
<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配置参数处理分以下几种情况
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.多个参数
当有多个参数时,#{}占位符的名称必须先指定参数本身,然后可以使用表达式
在#{}占位符中指定参数,通常支持三种方式
正如之前示例,#{}占位符除可指定参数名之后,还可通过typeHandler指定类型处理器,总结来说,#{}还可指定如下额外属性
需要说明的是,如果某一列允许为null,而且可能需要为该参数传入null作为参数值,那么久必须为该参数指定jdbcType属性——这是因为PreparedStatement中setNull(int parameterIndex,int sqlType)方法的第二个参数必须指定sqlType,而MyBatis无法根据null来推断出它的类型
对于允许为null的列,如果该列对应的参数可能传入null值,那就必须为参数对应的#{}部分指定javaType属性
#{}和${}的区别就是,#{}会将内容变成?,而¥{}会将它的值变成占位符的名字,而非?