官方文档
- 简介
- 入门
- XML配置
- XML映射文件
- 动态SQL
- Java API
- SQL语句构建器
- 日志
一、 JDBC回顾
JDBC的使用过程:
register the JDBC driver 加载驱动
open a connection 打开连接
execute using statment, including binding params 执行statment并绑定参数
extract data from result set (only select) 从结果集中获取数据
close database resources 关闭连接
JDBC具有的问题:
代码与sql语句耦合
动态组装sql语句处理繁琐
数据对象与结果集的映射与代码耦合
需要关心数据库资源的释放
二、 MyBatis的简介
MyBatis是一个支持普通 SQL查询,存储过程和高级映射的优秀持久层框架;MyBatis 消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis 使用简单的 XML或注解用于配置和原始映射,将接口和 Java 的POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。
依赖
org.mybatis
mybatis
术语解释
SqlSessionFactoryBuilder
SqlSessionFactory
SqlSession
(1)每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先定制的 Configuration 的实例构建出 SqlSessionFactory 的实例。
(2)SqlSessionFactory可以在xml配置文件中构建,也可以使用代码构建。
(3)三者的作用域(Scope)和生命周期:不同作用域和生命周期类是至关重要的,因为错误的使用会导致非常严重的并发问题:
SqlSessionFactoryBuilder:这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但是最好还是不要让其一直存在以保证所有的 XML 解析资源开放给更重要的事情。
SqlSessionFactory:SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由对它进行清除或重建。使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏味道(bad smell)”。因此 SqlSessionFactory 的最佳作用域是应用作用域。有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
SqlSession:每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。也绝不能将 SqlSession 实例的引用放在任何类型的管理作用域中,比如 Serlvet 架构中的 HttpSession。如果你现在正在使用一种 Web 框架,要考虑 SqlSession 放在一个和 HTTP 请求对象相似的作用域中。换句话说,每次收到的 HTTP 请求,就可以打开一个 SqlSession,返回一个响应,就关闭它。这个关闭操作是很重要的,你应该把这个关闭操作放到 finally 块中以确保每次都能执行关闭。-
mybatis 配置文件(xml,全局设置、数据库连接信息等)
sql mapper文件(存放需要执行的sql语句)
三、 XML配置文件
Mybatis使用xml配置文件主要有两个地方:Mybatis配置和Mapper映射配置。具体可以参考官方文档,说得非常详细。
properties
配置属性
settings
这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为,比如是否使用缓存、是否延迟加载、是否允许单一语句返回多结果集、是否允许 JDBC 支持自动生成主键等;一个例子:
typeAliases
类型别名是为 Java 类型设置一个短的名字。它只和 XML 配置有关,存在的意义仅在于用来减少类完全限定名的冗余。
typeHandlers
用来指定所使用的类型处理器。无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。
objectFactory(对象工厂)
MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。 如果想覆盖对象工厂的默认行为,则可以通过创建自己的对象工厂来实现。
plugins(插件)
MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用,默认情况下,MyBatis 允许使用插件来拦截的方法调用。
environments(配置环境)
MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者共享相同 Schema 的多个生产数据库, 想使用相同的 SQL 映射。许多类似的用例。
不过要记住:尽管可以配置多个环境,每个 SqlSessionFactory 实例只能选择其一。
mappers(映射器)
既然 MyBatis 的行为已经由上述元素配置完了,我们现在就要定义 SQL 映射语句了。但是首先我们需要告诉 MyBatis 到哪里去找到这些语句。 Java 在自动查找这方面没有提供一个很好的方法,所以最佳的方式是告诉 MyBatis 到哪里去找映射文件。你可以使用相对于类路径的资源引用, 或完全限定资源定位符(包括 file:/// 的 URL),或类名和包名等
四、XML映射文件
MyBatis 的真正强大在于它的映射语句,也是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 就是针对 SQL 构建的,并且比普通的方法做的更好。
select
select 元素有很多属性允许你配置,来决定每条语句的作用细节。
insert, update 和 delete
数据变更语句 insert,update 和 delete 的实现非常接近:
如前所述,插入语句的配置规则更加丰富,在插入语句里面有一些额外的属性和子元素用来处理主键的生成,而且有多种生成方式。
-
如果你的数据库支持自动生成主键的字段(比如 MySQL 和 SQL Server),那么你可以设置 useGeneratedKeys=”true”,然后再把 keyProperty 设置到目标属性上就OK了。例如:
insert into Author (username,password,email,bio) values (#{username},#{password},#{email},#{bio})
这样就可以将自动生成的主键绑定到Author对象的id属性上了。
-
如果你的数据库还支持多行插入, 你也可以传入一个Authors数组或集合,并返回自动生成的主键。
insert into Author (username, password, email, bio) values (#{item.username}, #{item.password}, #{item.email}, #{item.bio})
sql
这个元素可以被用来定义可重用的 SQL 代码段,可以包含在其他语句中。它可以被静态地(在加载参数) 参数化. 不同的属性值通过包含的实例变化. 比如:
${alias}.id,${alias}.username,${alias}.password
这个 SQL 片段可以被包含在其他语句中,例如:
参数(Parameters)
参数是 MyBatis 非常强大的功能;
-
例子1:
上面的这个示例说明了一个非常简单的命名参数映射。参数类型被设置为 int,这样这个参数就可以被设置成任何内容。
-
例子2:
insert into users (id, username, password) values (#{id}, #{username}, #{password})
如果 User 类型的参数对象传递到了语句中,id、username 和 password 属性将会被查找,然后将它们的值传入预处理语句的参数中。这点对于向语句中传参是比较好的而且又简单,不过参数映射的功能远不止于此。
- 参数的属性
像 MyBatis 的其他部分一样,参数也可以指定一个特殊的数据类型。
例子1 : #{property,javaType=int,jdbcType=NUMERIC}
例子2 : #{height,javaType=double,jdbcType=NUMERIC,numericScale=2}
例子3 :#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}
javaType 通常可以从参数对象中来去确定,前提是只要对象不是一个 HashMap。那么 javaType 应该被确定来保证使用正确类型处理器。尽管看起来配置变得越来越繁琐,但实际上是很少去设置它们。
numericScale 对于数值类型,还有一个小数保留位数的设置,来确定小数点后保留的位数。
mode 属性允许你指定 IN,OUT 或 INOUT 参数。如果参数为 OUT 或 INOUT,参数对象属性的真实值将会被改变,就像你在获取输出参数时所期望的那样。如果 mode 为 OUT(或 INOUT),而且 jdbcType 为 CURSOR(也就是 Oracle 的 REFCURSOR),你必须指定一个 resultMap 来映射结果集到参数类型。要注意这里的 javaType 属性是可选的,如果左边的空白是 jdbcType 的 CURSOR 类型,它会自动地被设置为结果集。
- 字符串替换
默认情况下,使用#{}格式的语法会导致 MyBatis 创建预处理语句属性并安全地设置值(比如?)。这样做更安全,更迅速,通常也是首选做法,不过有时你只是想直接在 SQL 语句中插入一个不改变的字符串。比如,像 ORDER BY,你可以这样来使用:
ORDER BY ${columnName}
这里 MyBatis 不会修改或转义字符串。
注意:以这种方式接受从用户输出的内容并提供给语句中不变的字符串是不安全的,会导致潜在的 SQL 注入攻击,因此要么不允许用户输入这些字段,要么自行转义并检验。
Result Maps
resultMap 元素是 MyBatis 中最重要最强大的元素。它就是让你远离 90%的需要从结果集中取出数据的 JDBC 代码的那个东西, 而且在一些情形下允许你做一些 JDBC 不支持的事情。 事实上, 编写相似于对复杂语句联合映射这些等同的代码, 也许可以跨过上千行的代码。 ResultMap 的设计就是简单语句不需要明确的结果映射,而很多复杂语句确实需要描述它们的关系。
resultMap用来映射查询结果,不论是否在标签属性中显示的指定,MyBatis 会在幕后自动创建一个 ResultMap,基于属性名来映射列到 JavaBean 的属性上。
我将其概括为3种形式:
(1)指定resultType为map,将所有列被自动映射到 HashMap 的键上:
这在很多情况下是有用的,但是 HashMap 不能很好描述一个领域模型,所以我们还有以下几种映射方式。(2)指定resultType为JavaBean或 POJO,查询结果自动映射到java类属性上,映射规则是基于属性名来映射列到 JavaBean 的属性上:
也许会受限于映射规则,不过如果列名没有精确匹配,你可以在列名上使用 select 字句的别名(AS,一个基本的SQL特性)来匹配标签:
-
(3)使用外部的 resultMap,这也是解决列名不匹配的另外一种方式
1. 定义外部resultMap与实体之间的映射关系
2.引用它的语句使用 resultMap 属性就行了(注意我们去掉了 resultType 属性)。比如:
—— “如果世界总是这么简单就好了”
高级结果映射
MyBatis的构想:数据库不用永远是你想要的或需要它们是什么样的。而我们 最喜欢的数据库最好是第三范式或 BCNF 模式,但它们有时不是。如果可能有一个单独的 数据库映射,所有应用程序都可以使用它,这是非常好的,但有时也不是。结果映射就是 MyBatis 提供处理这个问题的答案。
具体地说,当我们查询的结果非常复杂,比如说使用了连接查询,那么查询到的结果该如何映射?
【例子】:比如有下面这样一个查询需求,需要查询一篇博客和它的作者信息、评论列表以及每条评论的回复列表,下面是具体的查询语句:
你可能想把它映射到一个智能的对象模型,包含一个作者写的博客,有很多的博文,每 篇博文有零条或多条的评论和标签,就像这样:
class Blog
- id
- title
- class Author
- id
- username
- password
- email
- bio
- favouriteSection
- List class Post
- id
- subject
- class Author
- ...
- List Comment
- id
- List class Tag
- id
- draft(是否是草稿)
下面是一个完整的复杂结果映射例子 (假设Author, Blog, Post, Comment和Tag都是类型的别名):
这就是resultMap的强大作用!将我们的复杂查询映射到智能实体对象。
下面是 resultMap 元素的概念视图:
上述标签中可以具有的属性:
- jdbcType:支持的 JDBC 类型
- constructor:构造方法
- association:关联
- 关联的嵌套查询
- 关联的嵌套结果
- collection:集合
- 集合的嵌套查询
- 集合的嵌套结果
- discriminator:鉴别器
五、动态SQL
动态SQL
MyBatis 的强大特性之一便是它的动态 SQL功能。如果你有使用 JDBC 或其他类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句有多么痛苦。拼接的时候要确保不能忘了必要的空格,还要注意省掉列名列表最后的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。
通常使用动态 SQL 不可能是独立的一部分,MyBatis 当然使用一种强大的动态 SQL 语言来改进这种情形,这种语言可以被用在任意的 SQL 映射语句中。
动态 SQL 元素和使用 JSTL 或其他类似基于 XML 的文本处理器相似。在 MyBatis 之前的版本中,有很多的元素需要来了解。MyBatis 3 大大提升了它们,现在用不到原先一半的元素就可以了。MyBatis 采用功能强大的基于 OGNL 的表达式来消除其他元素。
- if
- choose, when, otherwise
- trim, where, set
- foreach
六、基于注解方式的使用
注解方式又将SQL和Java代码耦合,不推荐使用。但需要掌握一些基本的注解,以更好的配合基于Mapper文件的使用。
@Param
注解对象:方法参数
作用:如果你的映射器的方法需要多个参数, 这个注解可以被应用于映射器的方法 参数来给每个参数一个名字。否则,多 参数将会以它们的顺序位置来被命名 (不包括任何 RowBounds 参数) 比如。 #{param1} , #{param2} 等 , 这 是 默 认 的 。 使 用 @Param(“person”),参数应该被命名为 #{person}。
七、 集成Spring
依赖
org.mybatis
mybatis-spring
将Mybatis与Spring集成,可以利用Spring的依赖注入对Mybatis进行托管, SQLSessionFactory、SqlSession的管理都可以被Spring管理。
0、引入properties
1、配置数据源
我们这里使用druid
2、配置SqlSessionFactoryBean
configLocation:mybatis配置文件路径
mapperLocations:sql mapper文件路径
3、配置MapperScannerConfigurer
配置要扫描的DAO接口(与xml mapper对应)的包位置,这个包下面的接口将会被扫描,我们在Service中可以通过自动装配的方法获取并使用。
sqlSessionFactoryBeanName:指定SqlSessionFactoryBean
4、将数据源DataSource的事务托管给Spring
# 开启注解事务管理
5、mybatis配置文件
6、mapper配置文件
略
八、 typeHandler
无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成 Java 类型。Mybatis默认为我们实现了许多TypeHandler, 当我们没有配置指定TypeHandler时,Mybatis会根据参数或者返回结果的不同,默认为我们选择合适的TypeHandler处理。
那么,Mybatis为我们实现了哪些TypeHandler呢? 我们怎么自定义实现一个TypeHandler ?
从源码看TypeHandler的实现和管理
org.apache.ibatis.type.TypeHandlerRegistry是TypeHandler的注册管理类,在这里注册了所有Mybatis提供的默认类型处理器:
由源码可以看到, mybatis默认实现了很多TypeHandler,继承自一个抽象类:BaseTypeHandler,自定义typeHandler需要实现4个抽象方法:
自定义TypeHandler——以CodeEnumTypeHandler为例
在这里我参考Qunar对枚举的类型处理封装的类型处理器CodeEnumTypeHandler,用于处理枚举类型。在很多场景下,数据库中需要枚举变量作为字段值,比如性别(1-男,2-女)等。
为了适用所有使用Enum的场景,强制规定:使用CodeEnumTypeHandler的Enum必须具有两个方法:
code和静态的codeOf方法:
public int code() {
return this.getId();
}
public static xxxEnum codeOf(int id) {
return map.get(id);
}
code()方法:为一个Enum变量标识了编号,通过code()获取该Enum的编号,当然这个编号的意义必须与数据库中的意义相同,这用codeOf方法获得的枚举才有意义
codeOf()方法:通过编号获取Enum
在CodeEnumTypeHandler中,通过反射获取到Enum的这两个方法,在继承自BaseTypeHandler的四个方法中invoke调用,从而实现从Integer ->> Enum的转换。
最后,要在mybatis-config.xml中注册typeHandler:
这样,凡是LevelEnum类型,都将使用CodeEnumTypeHandler来处理。
根据上面的思路,我这里自己实现一个枚举类型的类型处理器,并且实现一个表示性别的枚举类:
/**
* version date author
* ──────────────────────────────────
* 1.0 17-3-22 wanlong.ma
* Description: 性别枚举类
* Others:
* Function List:
* History:
*/
public enum SexEnum {
MALE(1,"男"),
FEMALE(2,"女");
private int id; // 标识号
private String type; // 类型
SexEnum(int id, String type) {
this.id = id;
this.type = type;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
/////////定义下面的属性和方法用于类型处理//////////
private static final Map map = Maps.newHashMap();
static {
for(SexEnum sexEnum : values()) {
map.put(sexEnum.getId(), sexEnum);
}
}
public int code(){
return this.getId();
}
public static SexEnum codeOf(Integer id) {
return map.get(id);
}
}
/**
* version date author
* ──────────────────────────────────
* 1.0 17-3-22 wanlong.ma
* Description: 参考om.qunar.base.meerkat.orm.mybatis.type.CodeEnumTypeHandler写的Enum通用类型处理器
* 使用该处理器的Enum必须自己实现一个code方法和静态的codeOf方法
* Others:
* Function List:
* History:
*/
public class CustomEnumTypeHandler extends BaseTypeHandler> {
private Method code;
private Method codeOf;
public CustomEnumTypeHandler(Class> enumType) {
String className = enumType.getName();
String simpleName = enumType.getSimpleName();
try {
code = enumType.getDeclaredMethod("code");
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method " + className + "#code():int required.'");
}
try {
codeOf = enumType.getDeclaredMethod("codeOf", int.class);
} catch (NoSuchMethodException e) {
throw new RuntimeException("Static method " + className + "#codeOf(int code):" + simpleName + " required.");
}
if (!Modifier.isStatic(codeOf.getModifiers())) {
throw new RuntimeException("Static method " + className + "#codeOf(int code):" + simpleName + " required.");
}
}
/**
* 调用enum的code方法,返回枚举的标识
* @param object
* @return
*/
private Integer code(Enum object){
try {
return (Integer) code.invoke(object);
} catch (Exception e) {
throw new RuntimeException();
}
}
/**
* 根据枚举标识号获取枚举值
* 注意codeOf方法在枚举类型中是一个静态方法,所以此处是静态调用
* @param value
* @return
*/
private Enum codeOf(int value){
try {
return (Enum) codeOf.invoke(null, value);
} catch (Exception e) {
throw new RuntimeException();
}
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Enum> parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, code(parameter));
}
@Override
public Enum> getNullableResult(ResultSet rs, String columnName) throws SQLException {
return codeOf(rs.getInt(columnName));
}
@Override
public Enum> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return codeOf(rs.getInt(columnIndex));
}
@Override
public Enum> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return codeOf(cs.getInt(columnIndex));
}
}
配置文件中注册:
走起走起~
九、Plugins
十、RowBounds与分页
参考:
MyBatis中的RowBounds
mybatis的两种分页方式:RowBounds和PageHelper
Mybatis如何分页查询?Mysql中可以使用limit语句,但limit并不是标准SQL中的,如果是其它的数据库,则需要使用其它语句。MyBatis提供了RowBounds类,用于实现分页查询。通过设置RowBounds中的两个变量来设置分页起始行和页面大小:offset和limit。
使用方法
-
RowBounds:在mapper.java中的方法中传入RowBounds对象。
RowBounds rowBounds = new RowBounds(offset, page.getPageSize()); // offset起始行,limit是当前页显示多少条数据 public List
findRecords(HashMap map,RowBounds rowBounds); mappep.xml里面正常配置,不用对rowBounds任何操作。mybatis的拦截器自动操作rowBounds进行分页。
从源码分析RowBounds的实现原理
-
RowBounds类
DefaultResultSetHandler类中通过RowBounds实现的分页
RowBounds在处理分页时,只是简单的把offset之前的数据都skip掉,超过limit之后的数据不取出,上图中的代码取自MyBatis中的DefaultResultSetHandler类。跳过offset之前的数据是由方法skipRows处理,判断数据是否超过了limit则是由shouldProcessMoreRows方法进行判断。说简单点,就是先把数据全部查询到ResultSet,然后从ResultSet中取出offset和limit之间的数据,这就实现了分页查询。
参考另外一种分页方法:Mybatis 数据库物理分页插件 PageHelper
十一、关于DataSource
十二、Spring+Mybatis通用dao层、service层的实现原则
spring+mybatis通用dao层、service层的一些个人理解与实现
十三、其他
sql中使用转义字符
在mapper ***.xml中的sql语句中,不能直接用大于号、小于号要用转义字符:
MyBatis中Like语句使用方式
mysql数据库:
SELECT
*
FROM
user
WHERE
name like CONCAT('%',#{name},'%')
如果select查询为空,Mybatis会返回什么
/**
* 验证结果:返回null
*/
@Test
public void testSelectIfNotExist(){
EmployeeModel employeeModel = employeeService.queryEmployeeByStaffId(999);
logger.info("Is employeeModel null? ->> {}", employeeModel == null);
}