从 0 开始的 MyBatis 3.x 学习
封面画师:T5-茨舞(微博) 封面ID:74539039
本文参考:2018 尚硅谷 雷丰阳 MyBatis 3.x
0. MyBatis 的初步使用
0.1 为什么要使用 MyBatis?
MyBatis 是一个半自动化的持久层框架。
JDBC
- SQL 夹在 Java 代码块里,耦合度高导致硬编码内伤
- 维护不易且实际开发需求中 SQL 是有变化的,频繁修改的情况很多见
Hibernate 和 JPA
- 长难复杂 SQL ,对于 Hibernate 而言处理也不容易
- 内部自动生产的 SQL,不容易做特殊优化
- 基于全映射的全自动框架,大量字段的 POJO 进行部分映射时比较困难,导致数据库性能下降
对于开发人员而言,核心的 SQL 还是需要自己优化。
SQL 和 Java 编码分开,功能边界清晰,一个专注业务,一个专注数据!
0.2 简单使用步骤
1、编写 xml 配置文件(全局配置文件)
2、编写 SQL 映射文件(配置了每一个 SQL ,以及 SQL 的封装规则)
3、将 SQL 映射文件注册在全局配置文件中
4、写代码
- 根据全局配置文件创建一个
SqlSessionFactory
对象 - 使用 SqlSession 工厂,获取到 SqlSession 实例,然后使用它进行 CRUD (一个 SqlSession 代表和数据库的一次会话,使用后记得关闭)
- 使用 SQL 的唯一标志来告诉 MyBatis 执行哪一个 SQL ,SQL 都是保存在 SQL 映射文件中的
当然还可以使用接口式编程,获取接口的实现类对象,MyBatis 会为接口自动创建一个代理对象,代理对象去实现 CRUD。(mapper 接口没有实现类,但是 MyBatis 会为这个接口生成一个代理对象)
如果实体类的属性 名和数据库的字段名不对应,将无法查询出结果,可以在 SQL 语句中对相关的字段名(字段名的别名设置为属性名)使用别名进行查询。
1. 全局配置文件标签
全局配置文件,其中包含了数据库连接池信息、事务管理器信息等系统运行环境信息(非必须,可以使用 Java 代码编写)。
全局配置文件约束信息:
1 |
1.1 properties
-
作用:使用该标签可以引入外部 properties 配置文件(数据库连接信息)的内容。
-
<properties>
标签属性:resource:引用类路径下的资源
url:引用网络路径下或磁盘路径下的资源
该标签仅做了解,实际开发中会将 MyBatis 与 Spring 绑定,这些配置文件信息一般都会交给 Spring 进行管理,因此该标签的使用并不多。
1.2 settings
-
<settings>
标签包含很多重要的设置项,一个<settings>
标签下可以包含多个<setting>
标签,<setting>
标签用来设置一个设置项。 -
<setting>
标签属性:name:设置相关的参数
value:参数的有效值。
-
属性列表(以官方文档 mybatis – MyBatis 3 | Configuration 为准,下图仅供参考):
使用示例
1 | <settings> |
1.3 typeAliases
作用:别名处理器,能够为我们的 Java 类型取别名, 别名不区分大小写
尽管别名可以让 SQL 映射文件中的代码变得简洁,但是为了方便后续阅读代码,还是建议使用全类名
单个别名
<typeAliases>
标签下可以包含多个 <typeAliase>
,<typeAliase>
可以为某个 Java 类型取别名。
<typeAliase>
标签属性:
-
type:指定要取别名的类型全类名,默认别名就是类名首字母小写。
-
alias:指定要取的别名。
批量取别名
<typeAliases>
标签下可以包含一个 <package>
标签,<package>
可以为某个包下的所有类批量取别名。
<package>
标签属性:
- name:指定包名(为当前包以及下面所有后代包的每一个类都取一个默认别名,默认别名是类名首字母小写)
@Alias
:在批量取别名的情况下(使用了 <package>
标签的情况下),可以为某个 JavaBean 指定新的别名(该注解的位置在类上),以防止别名冲突。
MyBatis 为普通的 Java 类型提供了许多内建的类型别名,因此我们在自定义别名时应当避免与这些别名相同。比如下图的部分内建别名(全部内建别名请参考官方文档):
1.4 typeHandlers
作用:类型处理器。无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数,还是从结果集中取出一个值, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。
使用示例
处理枚举类型(需要从 EnumTypeHandler
或者 EnumOrdinalTypeHandler
中选一个来使用)时,MyBatis默认使用的是 EnumTypeHandler
,现在将处理枚举类型改为 EnumOrdinalTypeHandler
:
1 | <!--自定义枚举为UserStatus--> |
1.5 plugins
见 7. 插件 。
1.6 environments
作用:配置 MyBatis 环境,MyBatis 可以配置多种环境。
<environments>
标签属性:
- default:指定当前 MyBatis 使用的环境,可以达到快速切换环境。
需要注意的是: <environments>
标签下需要包含的标签:<environment>
标签。而<environment>
标签下需要包含 <transactionManager>
标签和 <dataSource>
标签。
1.7 environment
作用:配置一个具体的环境信息。这个标签下必须包含两个标签: <transactionManager>
、 <dataSource>
。
<environment>
标签属性:
- id:指定当前环境的唯一标识。
1.8 transactionManager
作用:事物管理器。(了解)
<transactionManager>
标签属性:
- type:事物管理器的类型。值有两种选择:JDBC 、 MANAGED ,这两个值其实是别名(分析源码可得)。
自定义事物管理器:实现 TransactionFactory
接口,type 属性指定为全类名。
1.9 dataSource
作用:配置数据源。
<dataSource>
标签属性:
- type:数据库类型。值有三种选择:UNPOOLED(不使用连接池) 、 POOLED(使用连接池) 、 JNDI ,这三个值也是别名(分析源码可得)。
自定义数据源:实现 DataSourceFactory
接口,type 属性指定为全类名。
1.10 databaseIdProvider
作用:数据库厂商标识,用于支持多数据库厂商。
<databaseIdProvider>
标签属性:
- type:值为 DB_VENDOR(这也是一个别名) :VendorDatabaseIdProvider.class。type 属性作用就是得到数据库厂商的标识(驱动getDatabaseProductName()),MyBatis 能够根据数据库厂商标识来执行不同的 SQL 语句。
- 常见数据库厂商标识:MySQL、Oracle、SQL Server。
具体使用
1 | <databaseIdProvider type="DB_VENDOR"> |
最后在 SQL 映射文件下,在 sql 操作标签中加上 databaseId
属性并指定全局配置文件中设置的数据库厂商标识别名。比如:
1 | <select id="findById" resultType="com.yang.domain.Users" databaseId = "mysql"> |
假设当前数据库环境是 MySQL ,同时需要根据员工的 id 来查询对应的员工,又假设在 SQL 映射文件中针对这个需求编写了两条 SQL 语句,一条没有添加数据库厂商标识,另一条添加了 MySQL 数据库厂商标识,MyBatis 在执行 SQL 语句时会优先执行指定更加明确的语句 ,即:添加了 MySQL 数据库厂商标识的 SQL 语句。
1.11 mappers
作用:将 SQL 映射文件注册到全局配置文件中,将 SQL 映射文件和全局配置文件进行关联( 重要 )。
<mappers>
标签下可以包含多个 <mapper>
标签, <mapper>
标签可以关联一个 SQL 映射文件。<mappers>
标签下还可以包含一个 <package>
标签,以进行批量注册。
<mapper>
标签属性:
-
resource:引用类路径下的 SQL 映射文件(注册配置文件)。
-
url:引用网络路径下或磁盘路径下的 SQL 映射文件(注册配置文件)。
-
class:引用接口(注册接口)。
class
属性使用规范
1、有 SQL 映射文件时,映射文件必须和接口同名,并且放在与接口同一目录下。
2、没有 SQL 映射文件时,所有的 SQL 语句都是利用注解写在接口上。class
属性值为 SQL 注解(eg:@Select)所在接口的全类名。简单的 SQL 语句可以使用注解,复杂的 SQL 语句还是写在 SQL 映射文件中。
<package>
标签
<package>
标签属性:
- name:SQL 映射文件(或接口)的包路径。
<package>
标签中 name 属性使用规范:存在 SQL 映射文件时,映射文件必须和接口同名,并且放在与接口同一目录下。
1.12 标签书写顺序
MyBatis 全局配置文件中的标签顺序并不是任意的。
全局配置文件中可以不存在某些标签,但是存在的标签必须按照以下的先后顺序进行书写:
1 | properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers? |
2. 获取主键值与参数处理
2.1 SQL 映射文件约束
SQL 映射文件中保存了每一个 SQL 语句的信息( 必须 )。
SQL 映射文件约束信息:
1 |
实体类与数据库字段
相关实体类:(下文中涉及的SQL语句环境都如下)
1 | public class User{ |
数据库表名与字段名:
1 | user table: |
2.2 简单 CRUD
在 MyBatis 中,允许增删改查直接定义以下类型的返回值:Integer
、Long
、Boolean
、 void
。
对于 Boolean 类型的返回值来说,如果执行增加、删除、修改等操作时影响了一行以上的数据,那么就会返回 true
。
在 Mybatis 中进行 CRUD 时,需要使用对应的标签,比如:<insert>
、<delete>
、<update>
和 <select>
,在这些标签中都有一个名为 parameterType
的属性,这个属性是老式风格的参数映射,已被废弃,但是也可以使用,使用时指定参数类型的全类名或别名即可。既然这个属性已被废弃,因此 我们在编写时也可以不写这个属性。
在 SQL 映射文件中写 SQL 时,请不要在末尾写上分号!
2.3 插入获取主键值
对于支持主键自增的数据库
例如,MySQL 数据库支持主键自增,MyBatis 可以利用 statement.getGeneratedKeys()
获取自增主键值。
那么应该怎么做呢?
我们只需要在 <insert>
标签中添加属性:useGeneratedKeys
和 keyProperty
。
其中,需要将 useGeneratedKeys
设置为 "true"
,这表示使用自增主键获取主键值策略。
而 keyProperty
属性用于指定对应的主键属性,也就是 MyBatis 获取到主键值后,将这个值封装给 JavaBean 的哪个属性。比如我插入数据时想让自增的主键值封装到实体类中的 id 属性,那么需要这么写: keyProperty = "id"
。
对于不支持主键自增的数据库:
例如,Oracle不支持主键自增,因此 Oracle 需要使用序列来模拟自增, 即:每次插入数据的主键是从序列中拿到的值。那么如何获取到这个值呢?
假设需要向某张表插入一行数据,且这张表拥有每次自增 1 的序列主键,那么我们可以这么写插入的 SQL:
1 | <insert id = "addUser" databaseId = "oracle"> |
在上述语句中,使用了一个名为 <selectKey>
的标签,见名识意,这个标签就是用于“查询主键”的。在这个标签中,有三个属性:
- keyProperty:查出的主键封装给 JavaBean 的哪一个属性。比如我想将查出的主键封装给 id 属性,那么就像上述代码那样书写即可。
- order:用于指定
<selectKey>
包裹的 SQL 语句在插入的 SQL 语句执行之前(BEFORE)或之后(AFTER)执行。 - resultType:指定
<selectKey>
包裹的 SQL 语句查询的数据的返回值类型。
使用 order = "BEFORE"
后插入语句的执行顺序是:先运行 <selectKey>
标签查询 id 的 SQL 语句,查出 id 的值后封装给 JavaBean 的 id 属性,然后再运行插入的 SQL 语句,这样就可以取出 id 属性对应的值。
如果将 <selectKey>
标签中的 order 属性改为 AFTER
(表示当前 SQL 语句在插入的 SQL 语句执行后再执行),则有:
1 | <insert id = "addUser" databaseId = "oracle"> |
使用 order = "AFTER"
后插入语句的执行顺序是:先运行插入的 SQL 语句(从序列中取出新值作为 id),然后再运行 <selectKey>
标签中查询 id 的 SQL 。
实际使用中通常使用 BEFORE 方式,而不是 AFTER 方式,因为 AFTER 方式插入多条数据时会出现问题,插入多条数据时只能获取到最后一个主键值!
2.4 参数处理
使用 MyBatis 时,我们通常会编写若干个 Mapper 接口,每个 Mapper 接口还有与之对应的 SQL 映射文件。在 SQL 映射文件中获取接口中的参数时,似乎只用 #{参数名}
的方式就能够获取到。其实并不是这么简单,不同情况下参数的获取方式也不同。
单个参数
MyBatis 不会做特殊处理 ,直接使用 #{参数名}
即可取出参数值。
其实不使用 #{参数名}
,参数名换成任意字符串都能获取到,因为只传递了一个参数。
比如,在单个参数的情况下,使用 #{a}
、 #{abc}
等等方式都能准确地获取到。
多个参数
MyBatis 会做特殊处理 :
多个参数会被封装成一个 Map ,在这个 Map 中:
-
key:param1 … paramN,或者参数的索引(0、1…)
-
value:传入的参数值
在 SQL 映射文件中, #{}
就是从 map 中获取指定的 key 值。我们可以使用 #{param1}
或 #{0}
来获取多个参数中的第一个参数值。
但是参数过多的时候,使用这种方式不便于阅读,因此我们可以使用注解对参数进行命名。
2021-10-13 补充:使用最新的 MyBatis 3.5.7 版本时,方法参数名称与 SQL 映射文件中参数名称相同时也可映射成功。
命名参数
命名参数就是明确指定封装参数时 map 的 key ,使用注解@Param
:
1 | public User getUserByIdAndUserName( Integer id, |
多个参数的情况下,在接口中使用了 @Param
注解后,多个参数会被封装成一个 map,在这个 map中:
- key:使用
@Param
注解指定的值 - value:传入的参数值
然后在 SQL 映射文件中,就可以使用 #{指定的key}
的方式取出对应的参数值。
实体类参数
POJO:如果多个参数恰好是我们业务逻辑的数据模型,我们可以在接口参数中直接传入实体类。然后在 SQL 映射文件中使用 #{属性名}
就可以取出传入的实体类的属性值。
Map:如果传入的数据不是业务模型的数据,没有对应的 POJO ,且不经常使用,我们可以传入 Map 集合。在 SQL 映射文件中使用 #{key}
就可取出 Map 集合中对应的 value 值。
TO:如果多个参数不是业务模型中的数据,但需要经常使用,推荐编写一个 TO(Transfer, Object)数据传输对象。
2.5 参数处理思考
根据下列给出的接口,写出在 SQL 映射文件中获取参数的方式。
1 | public User getUser(; Integer id, String username) |
id: #{id} 或者 #{param1}
username:#{param2} 或 #{username}(MyBatis 3.5.7 可成功映射)
1 | public User getUser(Integer id, ; User user) |
id:#{param1} 或 #{id}(MyBatis 3.5.7 可成功映射)
username:#{param2.username} 或者 #{u.username}
1 | public User getUserById(List<Integer> ids); |
需要特别注意的是,如果接口中参数传递的是一个 Collection(List、Set) 集合或者是数组, MyBatis 也会对其进行特殊处理,也就是把传入的集合或数组封装到 Map 中。
如果我们使用 #{key}
的方式获取 Map 中的 value 时:
- 对于 Collection 集合来说,key 是 collection
- 对于 List 来说,key 是 list
- 对于数组来说,key 是 array
因此,对于上述给出的接口,我们想在 SQL 映射文件中获取第一个 id 的值时,可以使用 #{list[0]}
进行获取。
2.6 #{ } 和 ${ } 的区别
在 SQL 映射文件中获取参数值时可以使用 #{ }
和 ${ }
来获取 Map 中的值或者 POJO 对象属性的值。
#{ } 和 ${ } 的区别
1、 #{ }
采用预编译的形式,将参数添加到 SQL 语句中,类似 PreparedStatement,相当于jdbc的 ?。使用这种方式防止 SQL 注入,并会自动添加单引号。
2、 ${ }
取出的值会直接封装在 SQL 语句中,这种方式不能防止 SQL 注入,会有安全问题。简单来说,这种方式就是字符串的拼接,不会自动添加单引号,因此 为字符串类型或日期类型的字段进行赋值时,需要手动加单引号。
3、大多数情况下,我们取参数的值都是使用 #{ }
。
4、对于原生 SQL 不支持占位符的地方(表名拼接、 SQL 关键字等)我们就可以使用 ${ }
进行取值。
使用 ${ } 取值的示例
比如按照年份分表拆分时,可以这么使用:select * from ${year}_salary where xxxx
对查询内容进行排序时,可以这么使用:select * from user order by ${name} ${order}
上述 ${order} 指的是排序方式,比如升序(ASC)、降序(DESC)
#{ }
内的参数
MyBatis 对 #{ }
还指定了一些参数:javaType、jdbcType、mode(存储过程)、numericScale、resultMap、typeHandler、jdbcTypeName。
jdbcType 通常需要在某些特定的环境下需要被设置。在我们数据为 null
时,有些数据库(比如Oracle…)可能无法识别 MyBatis 对 null
的默认处理。如果不进行配置就会进行报错:JdbcType OTHER(无效的类型,MyBatis 对所有 null 都映射的是原生 JDBC 的 OTHER 类型)。
可以进行如下两种设置:
1、对需要为 null 的字段加上 jdbcType=NULL
。eg:#{email, jdbcType=NULL}
2、或者进行全局配置:
1 | <settings> |
3. select 标签
3.1 resultType
使用 <select>
标签进行数据查询时,通常需要设置这个标签的两个属性:
- 一个属性是 id ,值是 Mapper 接口中的方法名
- 另一个属性是 resultType,值是查询结果的类型,即:接口中抽象方法返回值类型。不能和 resultMap 同时使用。
对于 resultType 属性来说,在使用时仍有需要注意的地方。 👇
方法返回值是 List
如果返回的是一个集合(List),resultType 的值是集合中元素的类型。
比如,接口返回值类型是 List<User>
,那么 resultType = com.yang.pojo.User
方法返回值是 Map
返回一条记录的 Map 时 ,key 就是数据库列名,value 就是对应的值:
1 | public Map<String, Object> getUserByIdReturnMap(Integer id); |
1 | <select id = "getUserByIdReturnMap" resultType = "map"> |
多条记录封装成一个 Map 时,Map<Integer, User>:key 是这条记录的主键,value 是记录封装后的JavaBean:
1 | //id为User中的属性,Map中key为主键 |
1 | <select id="getUserByUsernameLikeReturnMap" resultType="com.yang.pojo.User"> |
3.2 resultMap
这个标签用于数据库字段名与自定义的 JavaBean 属性名不同的场合 ,比如:
1 | public User findUserById (Integer id); |
1 | <!-- 自定义某个JavaBean的封装规则 |
3.3 关联查询
场景一
查询用户的同时查询用户所在的部门 (级联属性封装结果集)。
在 Mapper 接口中的方法如下:
1 | public User getUserAndDept(Integer id); |
则在 SQL 映射文件中可以这样编写:
1 | <resultMap type = "com.yang.pojo.User" id = "MyDifUser"> |
除了上述的写法外,<resultMap>
标签内还有另一种写法, 可以使用 <association>
标签定义关联的单个对象的封装规则
1 | <resultMap type = "com.yang.pojo.User" id = "MyDifUser_2"> |
<association>
标签还有其他的用处,我们可以使用 <association>
对场景一(查询用户的同时查询用户所在的部门)进行 分步查询 :
- 先按照用户 id 查询用户信息
- 再根据查询出的用户信息中的 did 值去部门表查出部门信息
- 最后将部门信息设置到员工信息中
创建一个新的 Mapper 接口,取名为 DepartmentMapper
,其中有一个方法:
1 | public interface DepartmentMapper{ |
那么对应的 SQL 映射文件代码如下:
1 | <mapper namespace = "com.yang.dao.DepartmentMapper"> |
UserMapper 接口中分步查询的抽象方法如下:
1 | public User getUserByIdStep(Integer id); |
对应的 SQL 映射文件代码:
1 | <resultMap type = "com.yang.pojo.User" id = "MyUserByStep"> |
除此之外,利用分步查询还可以进行 延迟加载 :比如我们每次查询 User 对象的时候,都将部门信息一起查询了出来,但部门信息最好是在我们使用时才采取加载。
要想实现延迟加载,只需要在分段查询的基础上在全局配置文件加上两个配置:
1 | <settings> |
场景二
在查询部门时,将部门对应的所有员工信息也查询出来(一对多)
前提,在Department类中添加属性:
1 | public class Department{ |
在 DepartmentMapper
接口中新增一个抽象方法:
1 | public interface DepartmentMapper{ |
对应的 SQL 映射文件代码:
1 | <resultMap type = "com.yang.pojo.Department" id = "MyDept"> |
上面这种实现的方式属于嵌套结果集的方式,使用 <collection>
标签定义关联的集合类型的属性封装规则。
跟场景一的实现一样,可以使用 <collection>
(不是 <association>
)对场景二(在查询部门时,将部门对应的所有员工信息也查询出来)进行 二分步查询 。
在 UserMapper
接口中新增一个抽象方法:
1 | public List<User> getUsersByDeptId(Integer deptId); |
UserMapper
接口对应的 SQL 映射文件代码:
1 | <mapper namespace = "com.yang.dao.UserMapper"> |
在 DepartmentMapper
接口中新增一个抽象方法:
1 | public interface DepartmentMapper{ |
对应的 SQL 映射文件代码:
1 | <resultMap type = "com.yang.pojo.Department" id = "MyDeptStep"> |
3.4 模糊查询
在 DepartmentMapper
接口中新增一个抽象方法:
1 | /** |
对应的 SQL 映射文件代码:
1 | <select id="getUserByLike" resultType="User"> |
3.5 拓展
<collection>
标签中的 column 属性需要传递多列的值时,可以将多列的值封装换成 Map 进行传递,那么 column 可以这么写:
1 | column= "{key1 = column1, key2 = column2}" |
因此上文中,<collection>
标签中的 column 属性值也可以改写为:
1 | column= "{deptId = id}" |
在 <collection>
标签中还有一个名为 fetchType
的属性,这个属性默认值是 lazy
,表示使用延迟加载 。就算在全局配置文件中开启了延迟加载 ,我们也可以在某个 <collection>
标签内将 fetchType
属性设置为 eager
,这样就可以将这条语句设置为立即加载 (全局配置的延迟加载并没失效)。
鉴别器
仅作了解,基本用不到!
MyBatis 可以使用 <discriminator>
(鉴别器)判断某列的值,然后根据某列的值改变封装行为。
比如,我们可以封装 User:
- 如果查询结果是女生,就把部门信息查询出来,否则不查询
- 如果查询结果是男生,把 username 这一列的值赋值给 email 属性
1 | <resultMap type = "com.yang.pojo.User" id = "MyUserDis"> |
4. 动态SQL
4.1 动态 SQL 简介
动态 SQL 是 MyBatis 强大的特性之一,极大的简化了我们拼接 SQL 的操作。如果条件很多,使用原生的方式拼接 SQL 简直就是煎熬!😡
动态 SQL 元素和使用 JSTL 或其他类似基于 XML 的文本处理器相似。
MyBatis 采用功能强大的基于 OGNL 表达式来简化操作:
- if
- choose(when,otherwise)
- trim(where,set)
- foreach
涉及的文件
在本节中,Mapper 接口名为 UserMapperDynamicSQL
,对应的 SQL 映射文件名为 UserMapperDynamicSQL
,这两个文件都位于 com.yang.dao.UserMapper
包下。
4.2 条件判断
现在有一个需求:根据传递的信息查询对应的用户,这些信息可能是多个或没有,如果没有时,就查询出全部用户。
Mapper 接口的抽象方法如下:
1 | // 携带了那个字段查询条件就带上这个字段的值 |
SQL 映射文件有以下几种编写方式:
- 使用
<if>
标签 - 使用
<if>
和<where>
标签
<if>
标签
1 | <!--查询用户:要求携带了哪个字段查询条件就带上这个字段的值--> |
<if>
标签中的 test 属性的值支持 OGNL 表达式,表达式中的字段(如:id、username…)是从抽象方法的参数中获取的,而非数据库字段。
OGNL 表达式还可以进行字符串和数字的判断,比如我需要判断 gender 是否等于 0 ,那么就可以直接写成:
gender == 0
这似乎像一个字符串,但是它就会对其进行判断!🐮
<where>
标签
像上面那样编写好 SQL 映射文件后似乎就完成了我们的需求?
其实并没有,查询的时候如果某些条件没带sql可能拼装会出现问题。比如:传入的参数中 id 为空,但是 username 不为空,这个时候使用上述方法拼接 SQL 就会出现问题。
为了解决这个问题,我们可以采用以下两种方式(说实话,推荐第一种,简单粗暴):
- 给 where 后面加一个 1=1 或 true ,以后的条件都是 and xxxx 的格式
- MyBatis 推荐使用
<where>
标签来将所有的查询条件包括在内,然后MyBatis 就会将<where>
标签中拼装的 SQL 中多出来的and
或者or
去掉
<where>
标签的使用:
1 | <select id="getUsersByConditionIf" resultType="com.yang.pojo.User"> |
但需要注意的是: <where>
标签只会去掉首位多出来的 and 或者 or ,不会去掉末尾的 and 或 or。
但是有些小机灵鬼就喜欢将 and 或者 or 写在末尾,那应该怎么办呢?
4.3 字符串截取
如果只使用 <where>
标签是无法解决拼接 SQL 末尾多余的 and 或 or ,在 MyBatis 中,可以使用 <trim>
标签来解决这个问题。
使用 <trim>
标签后得到的 SQL 语句是 <trim>
标签体中整个字符串拼串后的结果。
<trim>
标签提供了 4 个属性用于规范拼串得到的 SQL 语句:
1、 prefix:前缀。使用这个属性后,会为拼串得到的 SQL 添加一个 前缀 。
2、suffix:后缀。使用这个属性后,会为拼串得到的 SQL 添加一个 后缀 。
3、prefixOverrides:前缀覆盖。使用这个属性后,会去除拼串得到的 SQL 前多余的字符串。
3、suffixOverrides:后缀覆盖。使用这个属性后,会去除拼串得到的 SQL 后多余的字符串。
如果将 and 或 or 写在了末尾,那么就可以这样书写 SQL 映射文件:
1 | <select id="getUsersByConditionTrim" resultType="com.yang.pojo.User"> |
Mapper 接口的抽象方法如下:
1 | public List<User> getUsersByConditionTrim(User user); |
4.4 分支选择
在 Java 语言中,用于条件判断的关键字是 if
,还有一些用于分支选择的关键字switch
、case
和 default
,在 MyBatis 的动态 SQL 中,也提供了分支选择的标签:<choose>
、<when>
和 <otherwise>
。
假设现在有一个需求:根据携带的条件(id、username、email)在用户表中查询用户信息,携带的条件只能使用一个。如果没有携带任何条件,就查询性别为女(gender = 0)的用户。
Mapper 接口的抽象方法如下:
1 | public List<User> getUsersByConditionChoose(User user); |
对应的 SQL 映射文件内容如下:
1 | <!--如果带了id就用id查,如果带了username就用username查,且只会进入其中一个--> |
题外话
在 Java 中,使用 switch
关键词时并不强制要求使用 default
关键词。
switch
和 case
组成一个条件选择语句,找到相同的 case
值做为入口,执行后面的程序;若所有的 case
都不满足,则找 default
入口;若未找到则退出整个 switch
。 所以 default
只是一个备用的入口,有没有都无所谓。
不写 default
是建立在自己书写的条件已经包含了所有情况下,为了程序逻辑的完整性,建议写上 default
。
PS:在阿里巴巴的代码规范中,switch
和 case
后强制要求书写 default
。
4.5 封装修改条件
<where>
标签用于封装查询条件,而 <set>
标签可用于封装修改条件。
在查询数据时,可能会存在多出的 and 或 or,我们可以使用 <where>
或 <trim>
来解决。
在更新数据时,可能会存在多出的逗号,那么可以使用 <set>
或 <where>
来解决。
Mapper 接口的抽象方法如下:
1 | public void updateUser(User user); |
对应的 SQL 映射文件内容如下:
1 | <update id="updateUser"> |
除了使用 <set>
标签来解决多余的逗号外,还可以使用 <trim>
标签,使用该标签中的 suffixOverrides
属性,将其值指定为 ,
即可。
4.6 遍历
现在有一个需求:接口的抽象方法接收到一个包含了多个 id 的 List 集合,需要从数据库中查询这些这些 id 对应的用户信息。
我们可以将每个 id 一一取出来,然后进行查询,这个时候就需要用到 MyBatis 中的遍历了,需要用到 <foreach>
标签。
具体使用
Mapper 接口的抽象方法如下:
1 | public List<User> getUsersByConditionForeach(; List<Integer> ids) |
对应的 SQL 映射文件内容如下:
1 | <select id="getUsersByConditionForeach" resultType="com.yang.pojo.User"> |
标签属性
<foreach>
中主要有以下几个属性:
1、collection:指定要遍历的集合,该属性为必选
2、item:集合中元素迭代时的别名,该属性为必选
3、separator:每个元素之间的分隔符
4、open:遍历出所有结果拼接一个开始的字符
5、close:遍历出所有结果拼接一个结束的字符
6、 index:索引。遍历 list 或数组时,index 是索引,item 是当前值;遍历map时,index是 map 的 key,item 是 map 的值
最后使用 #{item}
就能取出每个遍历的值,如果该元素是一个对象,可以使用 #{item.xx}
的方式取出这个对象对应的成员变量的值。
collection 属性
参考链接:mybatis之foreach用法
在 <foreach>
标签中,collection 属性是必选的,表示需要进行遍历的对象。该属性的属性值一般是接口抽象方法中的需要被遍历的参数。
如果这个参数是 List 对象,默认用 “list” 代替作为键;如果这个参数是数组,默认用 “array” 代替作为键。这两个默认值相当于 MyBatis 内置的别名。
在作为入参时还可以使用 @Param("keyName")
来设置键,设置 keyName 后,两个默认值 list 和 array将会失效。
4.7 MySQL 批量保存
在 MySQL 环境下,使用 MyBatis 实现批量保存可以使用 <foreach>
进行遍历,MySQL 支持以下语法:
1 | values(), (), () |
Mapper 接口的抽象方法如下:
1 | public void addUsers(; List<User> users) |
对应的 SQL 映射文件内容如下:
1 | <!--mysql批量保存--> |
除此之外,还可以执行多条 SQL 语句,这些 SQL 语句使用分号进行隔开。
MySQL 下允许在一条语句中使用分号来分割多条查询,但这个功能默认是关闭的,开启这个功能需要在 jdbc.url(数据库连接属性) 中添加设置 allowMultQueries = true
。
4.8 其他
MyBatis 默认内置参数
1、 _parameter:代表整个参数体
- 单个参数:_parameter 就是这个参数
- 多个参数:参数会被封装成一个 map,_parameter 就是代表这个 map
2、 _databaseId:如果配置了 <databaseIdProvider>
标签,_databaseId 就是代表当前数据库的别名
<bind>
标签
这个标签可以将 OGNL 表达式的值绑定到一个变量中,方便后面引用这个变量的值。
比如模糊查询 username 时,就可以:
1 | <bind name = "_username" value = "'%'+username+'%'"/> |
<sql>
标签
这个标签可用于抽取可重用的 sql 片段,方便以后引用。例如:
1 | <sql id="insertColumn"> |
<sql>
标签内部也可以进行动态判断,比如使用标签 <if>
进行判断。
在 IDEA 中使用 <sql>
标签时, IDEA 可能会出现这样的错误提示:
1 | <statement> or DELIMITER expected, got 'xxxxx' |
出现这样的错误提示很正常(应该属于 IDEA 的 BUG), 大多数情况下 是不影响正常使用的。
<include>
标签
这个标签可以用来引用外部定义的 <sql>
标签内的内容。
1、 <include refid="insertColumn"/>
2、还可以在 <include>
标签中定义一个<property>
标签,用于自定义属性。
3、include-property 取值的正确方式是 ${prop}
,不能使用#{ }
取值。
5. 缓存
MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地进行配置和定制。缓存可以极大的提升查询效率。
5.1 一级缓存
一级缓存又被称为本地缓存,属于 SqlSession 级别的缓存,一级缓存是一直开启,不可被关闭的 。与数据库同一次会话期间查询到的数据会被放到本地缓存中, 以后如果需要获取相同的数据,直接从缓存中拿,不会再去查询数据库。通常只有在单机的情况下才有效,集群情况下会有数据不一致的问题。
一级缓存失效的情况
所谓一级缓存失效,就是在获取相同的数据时还需要向数据库发出查询请求。
1、SqlSession 不同
2、SqlSession 相同,查询条件不同(缓存中没有数据)
3、SqlSession 相同,两次查询之间执行了增删改操作(可能对当前数据有影响)
4、SqlSession 相同,手动清除了一级缓存,使用 sqlSession.clearCache()
手动修改数据库数据
直接修改数据库数据不会让一级缓存失效。这是什么意思呢?
假设先根据 id 在数据库中查询一条用户信息,然后程序休眠 10 秒,在这 10 秒内迅速去数据库修改这个 id 对应的用户信息,最后再次查询这个 id 对应的用户信息。
最终这两次得到用户信息是一样的,都是未修改前的信息。
部分测试代码:
1 |
|
控制台打印结果:
可以看到整个过程只执行了一条 SQL,两次查询得到的对象也是同一个,证明手动修改数据库不会令一级缓存失效。
5.2 二级缓存
二级缓存又称为全局缓存,基于 namespace 级别的缓存,一个 namespace 对应一个二级缓存,同时一个 namespace 也对应了一个 SQL 映射文件。
工作机制
1、一个会话:查询一条数据,这个数据就会被放到当前会话的一级缓存中
2、如果会话关闭,一级缓存中的数据就会被保存到二级缓存,新的会话查询信息就可以参照二级缓存中的内容
3、 不同的namespace查询出的数据会被放在自己对应的缓存中(map)
使用
1 | <settings> |
再去相关的 SQL 映射文件中配置使用二级缓存:<cache></cache>
,加上这个标签就行。当然也可以对这个标签的一些属性进行设置。
最后实体类需要 实现序列化接口 Serializable
。二级缓存必须在 SqlSession
关闭或提交之后有效。
两次查询之间执行了任意的增删改,会使一级和二级缓存同时失效。
启用二级缓存后,数据会从二级缓存中获取。首次查询出的数据会被默认放到一级缓存中,只有对话关闭 sqlSession.close()
或提交后,一级缓存中的数据才会转移到二级缓存中。
SQL 映射文件中
<cache>
标签属性
1、eviction:缓存的回收机制
-
LRU:最近最少使用的(默认),移除最长时间不被使用的对象
-
FIFO:先进先出,按对象进入缓存的顺序来移除它们
-
SOFT:软引用,除移基于垃圾回收器状态和软引用规则的对象
-
WEAK:弱引用,更积极地移除基于垃圾回收器和弱引用规则的对象
2、flushInterval:缓存刷新间隔。缓存多久清空一次,默认不清空,单位是毫秒。默认不设置。
3、readOnly:是否只读。
-
true:只读。MyBatis 认为从所有缓存中获取数据的操作都是只读的,不会修改数据。MyBatis 为了加快获取速度,直接就将数据在缓存中的应用交给用户。不安全,速度快。
-
false:非只读。MyBatis认为获取的数据可能会被修改。MyBatis 会利用序列化和反序化的技术克隆一份数据。安全,速度慢。 默认值。
4、size:缓存存放多少元素。可被设置为任意正整数,缓存的对象数目等于运行环境的可用内存资源数目。默认是1024。
5、type:指定自定义缓存的类名。需要实现 Cache
接口。
- 注意:查出的数据都会默认先放到一级缓存中,只有会话提交或关闭后,一级缓存中的数据才会转移到二级缓存。
5.3 相关设置与属性
全局配置文件中 <setting>
标签中的 cacheEnabled:设置为false时,关闭二级缓存,一级缓存一直可用且不可被关闭。
每个<select>
标签默认都有一个 useCache="true"
属性。这个属性设置为 false 时,不使用二级缓存,但一级缓存依然可用。
每个增删改标签中 默认 都有 flushCache=“true”,表示执行增删改后 一、二级缓存都被清空 。
<select>
标签中 默认 有 flushCache=“false”,若设置为 true,每次查询后都会清空所有缓存。
sqlSession.clearCache()
表示只清空当前 sqlSession 的一级缓存,但关闭后二级缓存也无法使用,因为一级缓存已被清空了。
在全局配置文件中 <setting>
标签有一个属性 localCacheScope
,表示本地缓存作用域。
- SESSION:使用一级缓存,当前会话所有数据保存在会话缓存中。
- STATEMENT:相当于禁用一级缓存。
缓存的顺序:二级缓存 --> 一级缓存 --> 数据库
SqlSession关闭之后,一级缓存中数据会写入二级缓存。
5.4 整合 EhCache
PS:了解即可,这年头谁还用这玩意啊,都用 Redis了 😏
首先需要在 pom.xml 中导入相关依赖:
1 | <!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache --> |
然后前往需要启用 EhCache 缓存的 SQL 映射文件中,添加以下代码:
1 | <cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache> |
最后还需要在类路径下编写 ehcache.xml
文件,如果在加载时未找到 /ehcache.xml
资源或该资源出现问题,则将使用默认配置。配置文件标签和参数信息可以自行查阅~ 😜
1 |
|
如果每个 SQL 映射文件都要使用 EhCache ,那岂不是每次都要写一遍 <cache></cache>
,这里还有一个标签:
1 | <cache-ref namespace="com.yang.dao.IUserDao"/> |
这个标签表示引用缓存,它有一个名为 namespace
的属性,可以通过这个属性指定当前 SQL 映射文件使用的缓存和哪个名称空间下的缓存一样。
6. 运行原理
以“根据员工ID查询员工信息”为例:
6.1 执行流程
1、读取配置文件
1 | InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml"); |
2、创建 SqlSessionFactory
工厂
1 | SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); |
3、使用工厂生产 SqlSession
对象,设置自动提交事务
1 | SqlSession sqlSession = factory.openSession(true); |
4、获取接口的代理对象(MapperProxy)
1 | UserMapper userMapper = sqlSession.getMapper(UserMapper.class); |
5、执行增删改查方法
6、释放资源
1 | sqlSession.close(); |
6.2 SqlSessionFactory 初始化
执行流程
MappedStatement
:一个 MappedStatement 就代表 SQL 映射文件中一个增删改查的详细信息。
Configuration 对象封装了所有配置文件的详细信息。
总结: 把配置文件的信息解析并保存在 Configuration 对象中,返回包含了 Configuration 的 DefaultSqlSessionFactory 对象。
6.3 openSession 获取 SqlSession 对象
执行流程
总结: 返回 SqlSession 的实现类 DefaultSqlSession 对象,它里面包含了 Executor 和 Configuration,四大对象中的 Executor 会在这一步被创建。
SqlSession 和 JDBC 中的 Connection 一样,是非线程安全的,不能将 SqlSession 写成一个成员变量,SqlSession 代表和数据库的一次对话,每次使用都应该去重新获取,用完后必须关闭。
6.4 getMapper 获取接口代理对象
执行流程:
总结: getMapper 使用 MapperProxyFactory 创建一个 MapperProxy 的代理对象,这个代理对象包含了 DefaultSqlSession(Executor)对象。
6.5 查询实现
执行流程
查询流程总结
6.6 原理总结
1、根据配置文件(全局配置文件、SQL 映射文件)初始化 Configuration 对象
2、创建一个 DefaultSqlSession 对象,里面包含 Configuration 以及 Executor(根据全局配置文件中的 defaultExecutorType 创建出对应的 Executor)
3、DefaultSqlSession.getMapper():拿到 Mapper 接口对应的 MapperProxy
4、MapperProxy 中有 DefaultSqlSession
5、执行增删改查方法:
-
调用 DefaultSqlSession 的增删改查( 调用 Executor 的增删改查)
-
创建一个 StatementHandler 对象(同时也会创建出 ParameterHandler 和 ResultSetHandler )
-
调用 StatementHandler 预编译参数以及设置参数值,使用 ParameterHandler 来给 SQL 语句设置参数
-
调用 StatementHandler 的增删改查方法
-
使用 ResultSetHandler 封装结果
6、注意:每个四大对象创建的时候都有一个 interceptorChain.pluginAll(parameterHandler)
7. 插件
7.1 插件原理
MyBatis 四大对象创建的时候:
1、每个创建出来的对象不是直接返回的,而是调用 interceptorChain.pluginAll(parameterHandler)
后返回的
2、pluginAll() 可以获取所有的 interceptor(拦截器)(插件需要实现的接口),调用 interceptor.plugin(target);
返回 target 包装后的对象
插件机制:我们可以使用插件为目标对象创建一个代理对象(类似AOP)。
我们的插件可以为四大对象创建出代理对象,代理对象可以拦截到四大对象的每一个执行。
7.2 插件编写
编写流程
1、编写 Interceptor
的实现类
2、使用 @Intercepts
注解完成插件签名
3、将写好的插件注册到全局配置文件中
编码实现
MyFirstPlugin.class
1 | package com.yang.dao.plugin; |
在主配置文件中注册插件:
1 | <!--plugins注册插件--> |
运行后控制台打印结果:
1 | 插件配置的信息{password=123456, username=root} |
注意
如果有多个插件就会产生多层代理:
- 创建动态代理对象的时候,是按照插件的配置顺序创建层层代理对象。
- 执行目标方法后,按照逆向顺序执行。(感觉和栈有点像?😳)
8. 其他
8.1 日志
使用日志:
在全局配置文件中添加(以使用 STDOUT_LOGGING
为例):
1 | <configuration> |
有效值:
1 | SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING |
Java代码中使用(以 log4j 为例):
1 | static Logger logger = logger.getLogger("xxxxxx.class"); //当前类的class |
8.2 分页
方法一: SQL 语句使用limit子句:
1 | select * from tableName limit i,n |
方法二: 使用 PageHelper 插件进行分页(项目中需要先导入相关依赖):PageHelper官网
8.3 批量
为什么要批量执行?数据库无法接收太长的拼接 SQL 语句,因此需要批量执行。
批量与非批量的比较(假设插入1w条数据):
- 非批量:(预编译sql次数 = 设置参数次数 = 执行次数) ==> 10000次
- 批量:预编译一次 ==> 设置参数10000次 ==> 执行1次
实现方式:
1 | //ExecutorType.BATCH表示将重用语句并执行批量更新 |
与 Spring 整合时参考视频:尚硅谷 MyBatis 3.x P84
8.4 存储过程
参考视频:尚硅谷 MyBatis 3.x P85
8.5 处理枚举类型
准备
假设原始数据库用户表中再添加一列 userStatus,用于保存用户的状态,最终用户表形式为:
1 | user table: |
自定义枚举类:
1 | public enum UserStatus { |
测试枚举的使用:
1 | // junit 注解 |
在 User 实体类中添加属性:
1 | //用户状态 |
尝试在数据库中添加一个用户,并得到结果…
总结
进行测试后发现,MyBatis默认使用的是 EnumTypeHandler
,保存到数据库的是枚举名。
按照 1.4 typeHandlers 更改使用的类后,保存到数据库的枚举的索引(索引从0开始)。
8.6 自定义类型处理器
在8.5 处理枚举类型的基础上进行修改,使数据库保存的是我们自定义的状态码,最后返回的是枚举名:
1 | package com.yang.domain; |
编写自定义类型处理器:
1 | package com.yang.typehandler; |
最后记得在全局配置文件中进行配置:
1 | <typeHandlers> |
经过上面的编写后,可以让枚举保存到数据库的数据是自定义的状态码,而不是枚举从 0 开始的索引。