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 / falsetrue
lazyLoadingEnabled延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。true / falsefalse
aggressiveLazyLoading当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载(参考lazyLoadTriggerMethods).true / falsefalse (true in ≤3.4.1)
mapUnderscoreToCamelCase是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。true / falseFalse

类型别名(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 定义了一些默认的别名,直接用就可以了:

别名映射的类型
_bytebyte
_longlong
_shortshort
_intint
_integerint
_doubledouble
_floatfloat
_booleanboolean
stringString
byteByte
longLong
shortShort
intInteger
integerInteger
doubleDouble
floatFloat
booleanBoolean
dateDate
decimalBigDecimal
bigdecimalBigDecimal
objectObject
mapMap
hashmapHashMap
listList
arraylistArrayList
collectionCollection
iteratorIterator

另外,当传入的是个数组的时候,根据源码的定义要类似这样的形式: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(依赖驱动)。
statementTypeSTATEMENT,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

你可能需要魔法上网~~