MyBatis学习笔记

MyBatis 是一个 Java 持久化框架,它通过 XML 描述符或注解把对象与存储过程或 SQL 语句关联起来。
MyBatis 是在 Apache 许可证 2.0 下分发的自由软件;MyBatis 的前身是 iBatis ,是 Apache 的一个开源项目
由于 MyBatis 是直接基于 JDBC 做了简单的映射包装,所以从性能的角度来看:
JDBC > MyBatis > hibernate

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。

整体架构

首先来看下 MyBatis 的整体架构,有一个大体的了解,相比 Hibernate 真是简单多了:

MyBatis架构.png

可以看出,MyBatis 也是依赖于两类配置文件,一类是主配置文件(只有一个),一般约定命名为 mybatis-config.xml ,配置了运行参数、插件、连接池等信息。
还有就是 Mapper.xml 映射文件,可以有多个,里面配置的是 Statement (简单说是 SQL 也行)
执行的时候会通过主配置文件构建出 SqlSessionFactory ,然后获得 SqlSession 对象,利用 SqlSession 就可以操作数据库了
SqlSession 的底层会通过一个执行器来执行 Statement(SQL),执行器一般有两种实现,一种是基本的,一种是带有缓存功能的
前面说过 Statement 可以简单理解为 SQL 语句,一般我们写 SQL 语句都是用 ?占位符,所以需要输入参数,然后执行,返回执行结果,至于输入输出的类型,图上已经说的很清楚了

一个入门栗子

了解其的最好方法就是看文档,官方有中文文档哦,首先我们需要配置基本的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 引入外部资源配置文件,以使用 ${} -->
<properties resource="jdbc.properties" />
<!-- 配置环境,数据库等信息 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!-- 引入映射文件 -->
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>

上面就是主配置文件,如果是 Maven 工程,默认是在工程的 resurces 中查找映射文件,所以可以直接写文件名;
然后来看看映射文件应该怎么写,这个映射和 Hibernate 的可不一样,简单的多,其实就是写 SQL 语句:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bfchengnuo">
<select id="selectBlog" resultType="com.bfchengnuo.domain.Blog">
select * from Blog where id = #{id}
</select>
</mapper>

resultType 指定的就是结果集映射到的相应的实体类,其他的先不说,看完下面的 Java 代码更好理解,下面是一段基本的 MyBatis 使用:

1
2
3
4
5
6
7
8
9
10
11
String resource = "org/mybatis/example/mybatis-config.xml";
// Resources 是 MyBatis 提供的工具类
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession session = sqlSessionFactory.openSession();
try {
Blog blog = (Blog) session.selectOne("com.bfchengnuo.selectBlog", 101);
} finally {
session.close();
}

然后就可以看出,通过 session 的 selectOne 方法进行查询的时候是用 namespace.id 来定位 Statement 的

添加日志支持

按照上面的方法确实是可以执行,但是控制台没任何日志输出,也看不到执行的 SQL,如果想了解就需要添加日志支持,既然是日志,那就用大名鼎鼎的 log4j 了,MyBatis 是支持的,会自动检测,如果发现有 slf 就会加载的~所以只需要写个配置文件了。
导入依赖这个就不用说了,只要在 log4j 的配置文件中写入下面的代码即可看到效果:

1
2
3
4
5
log4j.rootLogger=DEBUG,A1
log4j.logger.org.mybatis = DEBUG
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n

简单说下意思:第一行是设置日志的等级和位置(可以随便起个名字,比如 A1),MyBatis 的许多信息都是用的 Debug 级别,可以去源码看看;
第二行是单独指定 MyBatis 的级别,第一行是全局的,相当于个性化设置;
第三行就是指定位置(A1)具体是什么,这里是控制台;
下面是布局和格式,d-时间;t-线程名;p-显示级别名;n-换行;
关于 log4j 的使用,待补充…

进行CRUD操作

下面就再深入一点,看下 MyBatis 是如何进行 CRUD 操作的:
首先来定义映射文件 UserMapper (习惯上,命名为 xxxMapper):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?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="user">

<select id="queryUserById" resultType="cn.itcast.mybatis.pojo.User">
SELECT *,user_name userName FROM tb_user WHERE id = #{id}
</select>

<select id="queryAll" resultType="cn.itcast.mybatis.pojo.User">
SELECT *,user_name userName FROM tb_user
</select>

<insert id="saveUser" parameterType="cn.itcast.mybatis.pojo.User">
INSERT INTO tb_user (
id,
user_name,
age,
sex,
updated
)
VALUES
(
NULL,
#{userName},
#{age},
#{sex},
NOW()
);
</insert>

<update id="updateUser" parameterType="cn.itcast.mybatis.pojo.User">
UPDATE tb_user
SET
user_name = #{userName},
age = #{age},
sex = #{sex},
updated = NOW()
WHERE
id = #{id}
</update>

<delete id="deleteUserById" parameterType="java.lang.Long">
DELETE FROM tb_user WHERE id = #{id}
</delete>
</mapper>

一般一个 mapper 文件对应一个 Dao 层中的一些方法,一般来说方法名就是其 id,毕竟方法的执行需要 SQL 语句;注意下它们的标签就行了,下面 dao 层的具体实现也是都差不多,就是方法名和 SQL 语句的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public User queryUserById(Long id) {
return this.sqlSession.selectOne("user.queryUserById", id);
}

public List<User> queryAll() {
return this.sqlSession.selectList("user.queryAll");
}

public void saveUser(User user) {
this.sqlSession.insert("user.saveUser", user);
this.sqlSession.commit();//提交事务
}

public void updateUser(User user) {
this.sqlSession.update("user.updateUser", user);
this.sqlSession.commit();//提交事务
}

public void deleteUserById(Long id) {
this.sqlSession.delete("user.deleteUserById", id);
this.sqlSession.commit();//提交事务
}

于是就会想,能不能不写这个 dao 的实现,因为感觉都是重复代码啊….最好是连映射文件也不用写…..
这是可能的,后面再说(如果连接口都不用写那就更爽了


解决字段名于属性名不一致的问题:
最容易想的是用别名的方式,在 SQL 语句中将字段名起一个和属性名相同的别名就可以了,例如:select *,user_name userName from users 就能正确映射了
或者直接用 MyBatis 提供的功能,在主配置文件中加入:

1
2
3
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

这样貌似更简单是吧…..但是只限于驼峰命名规则,也就是:A_COLUMN ===> aColumn

动态代理实现DAO

MyBatis 提供了使用动态代理的方式来实现 DAO,也就是说只需要写个 dao 的接口就行了,而不需要写具体的实现类;
然后先来介绍映射文件 XML 法,让我们只要配置好映射文件就不需要再写实现类:

  • Mapper 中的 namespace 指定为接口
  • id 对应接口中的方法名
  • 接口方法中的参数必须和 Mapper 中 parameterType 配置的一致(可以省略,会根据传入值进行判断)
  • 接口方法的返回值必须和 Mapper 中 resultType 配置的一致(不可省略)

满足上面四个条件应该就可以使用了,所以 dao 层接口又称为是 mapper 接口,使用的时候直接通过下面一行代码获取代理对象:

1
2
3
4
// 设置事务自动提交
SqlSession session = sqlSessionFactory.openSession(true);

userDAO = sqlSession.getMapper(UserDAO.class);

这样就省去了写 dao 实现类的时间,如果还想省去 XML 文件那么就需要使用注解了,它就是专门做这个的,关于注解等会再说


那么为什么不需要具体的实现类呢,很显然使用的是动态代理技术;源码中有个 MapperProxy 类来做这件事,它实现了 InvocationHandler 接口,当我们调用 getMapper 方法时,就会创建出相应的一个代理对象,当我们执行这个接口的方法时就会调用 MapperProxy 中的 invoke 方法,从而不需要具体的实现类了。
那么它如何找到相关的 SQL 语句呢,在 MyBatis 加载主配置文件的同时,映射文件也一同加载了,如果接口和配置文件是对应的,那么就可以利用动态代理以及反射拿到接口名、方法名等数据通过方法名等找到对应的映射配置文件,也就知道对应的 SQL 语句了。
我们使用 getMapper 方法的时候还传入了一个类,内部通过使用泛型做了强转,所以即使是返回的代理对象我们可以直接用 UserDAO 去接收。


这里的 getMapper 方法的实现应该是:

1
2
3
4
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}

它是在 DefaultSqlSession 中被定义的,可以看出深层次的调用中还传入了 this (DefaultSqlSession),这是为后面调用 selectOne、selectList 等方法做准备。
在 MyBatis 加载配置文件的时候(准确说是 build 的时候),就会判断映射文件配置的 namespace 是不是个接口,如果是就做为 key (class 对象),并且根据这个接口创建一个 MapperProxyFactory 对象作为值,put 进一个叫 knownMappers 的 Map 对象中去。
在调用 getMapper 的时候,其实就是在从这个 Map 中获取相应的代理工厂,最终通过 MapperProxyFactory 的 newInstance 方法生产出一个具体的代理对象( MapperProxy ),其中会根据返回值而调用不同的 SQL 查询方法,调用会触发其 invoke 方法

configuration常用配置

因为官方有中文文档,所以可以直接去看官方的详细解释,这里写几个比较常用的以便查询
下面的配置都是写在主配置文件里的!!

属性(properties)

常用它来读取外部的配置文件(properties 文件),然后用 ${name} 来引用,让配置更加的灵活,例如可以这样定义:

1
2
3
4
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>

这样是不是感觉更灵活了,读取顺序是先读取 properties 中定义的,再读取指定的文件,出现重名的情况时,文件中定义的会覆盖 xml 定义的;
从 MyBatis 3.4.2 开始,你可以为占位符指定一个默认值,但需要先开启这个功能,详情可看官方,因为感觉用的不多就不贴了

设置(settings)

第一个其实已经说过了,就是解决属性名和字段名不同的问题,会自动转换的那个(mapUnderscoreToCamelCase);
然后重点是这几个,后面会说到:

设置参数 描述 有效值 默认值
cacheEnabled 该配置影响的所有映射器中配置的缓存的全局开关。 true / false true
lazyLoadingEnabled 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。 true / false false
aggressiveLazyLoading 当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载(参考lazyLoadTriggerMethods). true / false false (true in ≤3.4.1)
mapUnderscoreToCamelCase 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。 true / false False

类型别名(typeAliases)

简单说就是把 Java 类型取一个别名,在其他地方用的时候就不需要写一大堆的包了

1
2
3
4
5
6
7
<typeAliases>
<typeAlias alias="Author" type="domain.blog.Author"/>
<typeAlias alias="Blog" type="domain.blog.Blog"/>

<!-- 可以写一个包,将会从这个包下找 JavaBean -->
<package name="domain.blog"/>
</typeAliases>

别名的首字母是不区分大小写的,但是习惯是大写,毕竟是类;然后 MyBatis 定义了一些默认的别名,直接用就可以了:

别名 映射的类型
_byte byte
_long long
_short short
_int int
_integer int
_double double
_float float
_boolean boolean
string String
byte Byte
long Long
short Short
int Integer
integer Integer
double Double
float Float
boolean Boolean
date Date
decimal BigDecimal
bigdecimal BigDecimal
object Object
map Map
hashmap HashMap
list List
arraylist ArrayList
collection Collection
iterator Iterator

另外,当传入的是个数组的时候,根据源码的定义要类似这样的形式:parameterType="Object[]" ,解析配置文件的过程中在 TypeAliasRegistry 这个类中定义了这些别名。

环境(environments)

前面刚开始就用到了,用它来配置的数据库信息,因为一般都有有多个环境,比如开发环境、测试环境、生产环境;每个环境的数据库是不一样,这样做是为了便于分离
可以使用 new SqlSessionFactoryBuilder().build(reader, environment); 来指定加载某个环境的配置
实际上,都是用 Spring 来处理这个,所以了解就好

映射器(mappers)

这个是一个重点,mappers 可以通过四种方式引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- Using classpath relative resources -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
</mappers>
<!-- Using url fully qualified paths -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
</mappers>
<!-- Using mapper interface classes -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
</mappers>
<!-- Register all interfaces in a package as mappers -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>

第一种是比较熟的了;第二种通过绝对路径的方式基本不会用;第三种通过类引用就要必须将映射文件和接口文件放在一起,名字也要统一;第四种的包扫描方式也是如此,需要放在一起才行
这四种并没有想象中的那样优雅,但是好消息是,和 Spring 整合后就能使用更优雅的方式了,不用和类混在一起也不需要配置那么多 mapper,等下篇说吧

Mapper常用配置

这里所有的配置都是写在映射文件中的哦。
这方面官方文档中写的也是非常详细的,也是挑常用的说,SQL 映射文件有很少的几个顶级元素(按照它们应该被定义的顺序):

  • cache – 给定命名空间的缓存配置。
  • cache-ref – 其他命名空间缓存配置的引用。
  • resultMap – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。
  • sql – 可被其他语句引用的可重用语句块。
  • insert – 映射插入语句
  • update – 映射更新语句
  • delete – 映射删除语句
  • select – 映射查询语句

最后的四个其实已经用过了,对应 CRUD 操作,对它们还是有些补充的东西;
对于 insert/update,如果设置的是 id 自动增长,插入一条数据后是获取不到 id 的,为了让 id 回填,需要配置一些东西(下表加黑的属性):

属性 描述
id 命名空间中的唯一标识符,可被用来代表这条语句。
parameterType 将要传入语句的参数的完全限定类名或别名。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset。
flushCache 将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:true(对应插入、更新和删除语句)。
timeout 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)。
statementType STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
useGeneratedKeys (仅对 insert 和 update 有用)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系数据库管理系统的自动递增字段),默认值:false。
keyProperty (仅对 insert 和 update 有用)唯一标记一个属性,MyBatis 会通过 getGeneratedKeys 的返回值或者通过 insert 语句的 selectKey 子元素设置它的键值,默认:unset。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
keyColumn (仅对 insert 和 update 有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(像 PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
databaseId 如果配置了 databaseIdProvider,MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。

简单来说是 useGeneratedKeys 开启回填 id;keyColumn 指定数据库中的列(如果和 keyProperty 相同可以省略不写);keyProperty 指定对象中的属性名:

1
2
3
4
5
<insert id="insertAuthor" 
parameterType="Author"
keyProperty="id"
keyColumn="id"
useGeneratedKeys="true" >

这样插入以后获取 id 就是有值的

resultMap

官方给出的介绍是:是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象;
可以看出这个标签是很重要的,同时也看出了它的作用,就是来处理字段和属性之间的映射关系的,所以它也是可以处理字段名和属性名不同的问题,例如:

1
2
3
4
5
6
7
8
9
<!-- 定义 -->
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name" />
</resultMap>
<!-- 使用 -->
<select id="selectUser" resultMap="userResultMap">
select * from user where id = #{id}
</select>

其中 resultMap 有一个 autoMapping 自动映射的属性,开启后如果没写的字段也会进行映射,也就是不用写全,这个默认应该是开启状态(当使用了集合等标签默认是关闭),但还是写上比较好

sql片段

sql 片段就是为了 sql 代码的复用,和 Java 代码类似,如果有很多重复的 sql 语句,可以提取出来,也便于以后的统一维护;
一般情况下,我们会把这些 sql 片段统一在一个单独的 mapper 文件中,就像 Java 中的工具类;下面就来看看 sql 片段的写法和使用:

1
2
3
4
5
6
7
8
9
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
<!-- 使用片段 -->
<select id="selectUsers" resultType="map">
select
<include refid="userColumns"><property name="alias" value="t1"/></include>,
<include refid="userColumns"><property name="alias" value="t2"/></include>
from some_table t1
cross join some_table t2
</select>

使用 include 标签来引用,在这其中可以使用 property 标签来指定片段中引用的值

$与#.

在 Mapper 中,参数传递有两种方式,一种是 ${} 另一种是 #{} 它们有很大的区别;

  • #{} 是实现的 sql 语句的预处理,之后执行的 sql 中会用 ?来代替,不需要关注数据类型(自动处理),并且能防止 SQL 注入,因为本质是占位符,所以名字其实可以随便写(#{xxx}),最后都会换成 ?,前提是参数是基本数据类型;
  • ${} 是 SQL 语句的直接拼接,不做数据类型转换,需要自行判断数据类型,不能防止 SQL 注入;

当我们的表名不确定时,就可以使用 $ 了,select * from ${tabName} ,表名可以通过参数传递过来,如果使用 # 那么最终的 sql 语句就会变成:select * from ? ,当然会报错;
使用 $ 时要注意的是它默认会从传入的对象中找 getter 方法获得配置的名(# 也是类似,但当传入的是基本数据类型时名称其实可以随便写),例如,你传入的是一个字符串类型的表名,但是它会调用 String.getTabName() 方法,自然会报错,所以你知道为什么前面 CRUD 的时候一条 SQL 可以使用多次 #{name} 了吧。
解决方法有两种:

  • 使用 ${value} ,这样就会获取默认传入的参数,它是 MyBatis 中提供的默认参数名
  • 代码中使用注解 ( @Param )
    public List<Map<String,Object>> queryByTableName(@Param("tabName") String tabName);

还记得那张架构表么,resultType 的类型是可以设置为 Map 的,不过 Map 是个接口,需要写具体类,比如 HashMap;

多个参数

当传入的是多个参数的时候,就和 #{name} 中的名字有关了,但是你直接写肯定是不行的,方法也是有两种:

  • 使用 #{0}#{param1} 这样依次增加
  • 代码中使用 @Param 注解,然后就可以使用 #{name} 的形式了

优先选择的当然是注解了,这样看着比较舒服….
单个参数的时候 # 是与参数名无关的,最终反正会被替换成 ?

当我们传入一个对象时,可以在 SQL 中用多个 #{name} 来获取对象中的属性,就像刚开始的栗子里用的一样;这种情况算是只传入了一个参数哦

使用OGNL

MyBatis 默认采用的是 OGNL 表达式,所以在这里也是可以使用的,在写 SQL 的时候写的 #{name} 中就可以使用,还可以用在一些标签里面(比如 if 里的 test 表达式)。
获取几种值的写法(对大小写敏感),例如获取基本数据类型是 #{_parameter}
获取基本类型:_parameter
自定义类型:属性
获取集合:数组(用 array)、List(用 list )、Map(用 _parameter );例如:对于基本数据类型的数组:array[索引]
如果是 Map 的话就是 _parameter.key ,自定义的类型(Map<string,obj>) 可以直接 key.属性名 获取

在 OGNL 中可以直接使用 Java 中的操作符(+、-、/、==、!=、||、&&),以及调用 Java 中的一些方法
因为是写在 XML 中,所以有些符号需要转义,于是 OGNL 提供了自己的操作符代替(and、or、in、not in、mod)mod 就是取余

所以,当 parameterType 的类型是字符串或者基本数据类型时(因为值是唯一的),并且只有一个的情况下,可以 #{_parameter} 也可以 #{随便写点啥} 或者还可以使用 OGNL 的方式直接写 _parameter

其他

因为 mapper 的定义可以省去写 dao 层接口实现类,所以很多情况下直接把原来的 dao 层命名为 mapper 层,原接口的命名 UserDAO 就写成了 UserMapper ,它们其实是指的一个东西,这样在使用 MyBatis 的情况下看着还是比较规范的。

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~