MyBatis
0.前置知识
1.JdbcTemplate:Spring封装了JDBC

2.Hibernate:全自动全映射

3.MyBatis:半自动,轻量级的框架
MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。
MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集
MyBatis可以使用简单的XML或注解用于配置和原始映射,将接口和Java的POJO映射成数据库中的记录.
1.MyBatis简介
MyBatis 是由 Ibatis 发展而来的,Ibatis1.x 和 Ibatis2.x,称之为 Ibatis,
Ibatis3.x 改称为:MyBatis。MyBatis 位于软件三层架构中的 dao 层。
MyBatis 官网:https://github.com/mybatis/mybatis-3
2.MyBatis 的优势[常见面试题]
第一个:MyBatis 本身是一个框架,除了可以对表中的数据进行增删改查操作之外,还有缓存、字段映射等机制。
第二个:MyBatis 支持动态 SQL[根据不同参数生成不同的 SQL 语句]
第三个:MyBatis 支持将 java 代码和 SQL 语句进行分离。
第四个:MyBatis 支持将表的关联关系直接映射为 pojo 对象的关联关系。
第五个:MyBatis 是一个半自动化的 ORM 框架。
ORM:Object Relational Mapping 对象关系映射


3.MyBatis 的第一个案例
第一步:创建动态web工程,导入jar包:mysql、mybatis
第二步:创建mybatis的全局配置文件mybatis-config.xml一个项目中只有一个全局配置文件
? 全局配置文件中配置的是:数据库连接信息、加载了SQL映射文件、事务管理器信息等...系统运行环境信息
第三步:创建SQL映射文件
? 一个项目中可以有多个SQL映射文件
? SQL映射文件主要写对数据表增删改查操作的SQL语句
第四步:写测试代码:
? 1.加载mybatis的全局配置配置文件生成SqlSessionFactory对象
? 2.根据SqlSessionFactory的openSession方法得到SqlSession对象
? 3.通过SqlSession对象完成对数据表的增删改查操作
? 4.释放资源
代码示例(详见源码解析)
版本1 原生写法
public class AppTest {
@Test
public void testFirstMyBatisDemo() throws IOException {
//1.第一步:加载mybatis的全局配置配置文件生成SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.根据SqlSessionFactory的openSession方法得到SqlSession对象
//SqlSession代表服务器和数据库一次会话。对数据表的增删改查操作都是通过SqlSession对象完成的
SqlSession sqlSession = sqlSessionFactory.openSession();
//3.通过SqlSession对象完成对数据表的增删改查操作
Book book = sqlSession.selectOne("org.mybatis.example.BlogMapper.selectBlog", 5);
System.out.println(book);
//4.释放资源
sqlSession.close();
}
}
全局配置文件mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
//数据库连接信息
//去src路径下寻找
SQL映射配置文件book.xml(可以有多个类.xml)
<?xml version="1.0" encoding="UTF-8" ?>
//namespace:名称空间;指定为接口的全类名
//id:唯一标识
//resultType:返回值类型
//#{d}:从传递过来的参数中取出id值
//sql语句,测试类里通过namespace.id来找到对于的sql语句 例如:org.mybatis.example.BlogMapper.selectBlog
版本二 接口式编程
1、接口式编程 (底层使用动态代理,本质是生成实现同一接口的代理类对象)
原生: Dao接口 ====> DaoImpl
mybatis: Mapper接口 ====> xxMapper.xml
2、SqlSession代表和数据库的一次会话;用完必须关闭;
3、SqlSession(底层就是connection)和connection一样 都是非线程安全。每次使用都应该去获取新的对象
? 不关闭会产生资源竞争,A线程使用的SqlSession未关闭,B线程也拿来用,实际B线程应该使用新的SqlSession
4、mapper接口没有实现类,但是mybatis会为这个接口生成一个代理对象。(将接口和xml进行绑定)
? BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
5、两个重要的配置文件:
mybatis的全局配置文件:包含数据库连接池信息,事务管理器信息等...系统运行环境信息
sql映射文件:保存了每一个sql语句的映射信息:将sql抽取出来(一定要有)
接口代码
public interface BookMapper {
public Book getBookById(Integer id);
}
book.xml
@Test
public void test02() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession sqlSession = sqlSessionFactory.openSession();) {
BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
Book book = bookMapper.getBookById(5);
System.out.println(bookMapper.getClass());//class com.sun.proxy.$Proxy4代理类对象
System.out.println(book);
}
}
4.MyBatis-全局配置文件mybatis-config.xml[不重要]
MyBatis 的配置文件mybatis-config.xml包含了影响 MyBatis 行为甚深的设置(settings)和属性(properties)信息。
文档的顶层结构如下
必须按顺序配置,否则报错
-
configuration 配置
-
properties 属性
-
settings 设置
-
typeAliases 类型命名
-
typeHandlers 类型处理器
-
objectFactory 对象工厂
-
plugins 插件
-
environments 环境
- environment 环境变量
- transactionManager 事务管理器
- dataSource 数据源
-
databaseIdProvider 数据库厂商标识 :SQL语句使用databaseId属性引用
-
mappers 映射器
配置详解
<?xml version="1.0" encoding="UTF-8" ?>
1.properties属性
? 如果属性在不只一个地方进行了配置,那么 MyBatis 将按照下面的顺序来加载:
? 1.在 properties 元素体内指定的属性首先被读取。
? 2.然后根据 properties 元素中的 resource 属性读取类路径下属性文件
或根据 url 属性指定的路径读取属性文件,并覆盖已读取的同名属性。
? 3.最后读取作为方法参数传递的属性,并覆盖已读取的同名属性。
...
2.settings设置

3.typeAliases别名处理器
类型别名是为 Java 类型设置一个短的名字,可以方便我们引用某个类。

类很多的情况下,可以批量设置别名这个包下的每一个类创建一个默认的别名,就是简单类名小写。

也可以使用@Alias注解为其指定一个别名

不建议使用别名,使用全类名,便于查看SQL语句是怎么被封装为JAVA对象的
MyBatis已经为许多常见的 Java 类型内建了相应的类型别名。它们都是大小写不敏感的,我们在起别名的时候千万不要占用已有的别名。

4.typeHandlers类型处理器
作用:将数据库中的类型转换为对应的 java 对象中的类型。我们直接使用即可(自动调用)
无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的
方式转换成 Java 类型。

日期类型的处理
日期和时间的处理,JDK1.8以前一直是个头疼的问题。
我们通常使用JSR310规范领导者StephenColebourne创建的Joda-Time来操作。
1.8已经实现全部的JSR310规范了。
日期时间处理上,我们可以使用MyBatis基于JSR310(Date and Time API)编写的各种日期时间类型处理器。
MyBatis3.4以前的版本需要我们手动注册这些处理器,以后的版本都是自动注册的

自定义类型处理器
我们可以重写类型处理器或创建自己的类型处理器来处理不支持的或非标准的类型。
步骤:
1)实现org.apache.ibatis.type.TypeHandler接口或者继承org.apache.ibatis.type.BaseTypeHandler
2)指定其映射某个JDBC类型(可选操作)
3)在mybatis全局配置文件中注册
5.objectFactory
作用:MyBatis 框架底层就是用该工厂对象创建 resultType 值对应的 pojo 对象。最终也是通过反射技术实现的。
6.plugins插件
插件是MyBatis提供的一个非常强大的机制,我们可以通过插件来修改MyBatis的一些核心行为。
插件通过动态代理机制,可以介入四大对象的任何一个方法的执行。
? 四大对象及其调用的重要方法
? Executor (update, query, flushStatements, commit, rollback,getTransaction, close, isClosed)
? StatementHandler (prepare, parameterize, batch, update, query)
? ParameterHandler (getParameterObject, setParameters)
? ResultSetHandler (handleResultSets, handleOutputParameters)
7.environments环境
nvironments标签中有多个enviroment标签,每个enviroment都对应一种环境。例如 开发环境,测试环境,线上环境
每种环境使用一个environment标签进行配置并指定唯一标识符
可以通过environments标签中的default属性指定一个环境的标识符来快速的切换环境
1.environment-指定具体环境
2.transactionManager
type: JDBC | MANAGED | 自定义 (Configuration类中)
JDBC:使用了 JDBC 的提交和回滚设置,依赖于从数据源得到的连接来管理事务范围。JdbcTransactionFactory
MANAGED:不提交或回滚一个连接、让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 ManagedTransactionFactory
自定义:实现TransactionFactory接口,type=全类名/别名
3.dataSource
type: UNPOOLED | POOLED | JNDI | 自定义
? UNPOOLED:不使用连接池,UnpooledDataSourceFactory
? POOLED:使用连接池, PooledDataSourceFactory
? JNDI: 在EJB 或应用服务器这类容器中查找指定的数据源
? 自定义:实现DataSourceFactory接口,定义数据源的获取方式。
实际开发中我们使用Spring管理数据源,并进行事务控制的配置来覆盖上述配置
4.databaseIdProvider环境为数据库服务器起别名
MyBatis 可以根据不同的数据库厂商执行不同的语句。
? Type: DB_VENDOR
使用MyBatis提供的VendorDatabaseIdProvider解析数据库厂商标识。也可以实现DatabaseIdProvider接口来自定义。
? Property-name:数据库厂商标识
? Property-value:为标识起一个别名,方便SQL语句使用databaseId属性引用
sql语句可以根据别名指定数据库服务器

DB_VENDOR
会通过 DatabaseMetaData#getDatabaseProductName() 返回的字符串进行设置。
由于通常情况下这个字符串都非常长而且相同产品的不同版本会返回不同的值,所以最好通过设置属性别名来使其变短
MyBatis匹配规则如下:
1、如果没有配置databaseIdProvider标签,那么databaseId=null
2、如果配置了databaseIdProvider标签,使用标签配置的name去匹配数据库信息,匹配上设置databaseId=配置指定的值,否则依旧为null
3、如果databaseId不为null,他只会找到配置databaseId的sql语句
4、MyBatis 会加载不带 databaseId 属性和带有匹配当前数据库databaseId 属性的所有语句。
? 如果同时找到带有 databaseId 和不带databaseId 的相同语句,则后者会被舍弃。
5.SQL 映射文件xxMapper
0.1关于XML文件使用"<" 和 "&"的补充说明
在 XML 元素中,"<" 和 "&" 是非法的。
"<" 会产生错误,因为解析器会把该字符解释为新元素的开始。
"&" 也会产生错误,因为解析器会把该字符解释为字符实体的开始。
在mapper文件中写sql语句时,遇到特殊字符时,例如:&& ,“”,< ....特殊字符,有两种解决方案:
第一种:使用特殊字符的实体名称(HTML ISO-8859-1 参考手册)
| 结果 | 描述 | 实体名称 |
|---|---|---|
| " | quotation mark | " |
| ' | apostrophe | ' |
| & | ampersand | & |
| < | less-than | < |
| > | greater-than | > |
第二种:如:< 等,建议使用标记,将sql语句包裹住,不被解析器解析

图中要实现 a.scanTime < concat(.....),就需要图中的写法 time concat(......)
0.2如何引入mapper文件
mappers标签中可以有多个mapper标签
mapper标签:加载sql映射文件和mapper接口
属性:
? resource:默认是加载类路径(src目录下)的sql映射文件
? url:加载网络上的某个sql映射文件[不用]
? class:加载Mapper接口,要求:
? 1.mapper接口和对应的SQL映射文件要在同一个包下
? 2.mapper接口和对应的SQL映射文件必须是同名的
package标签:批量加载mapper接口,要求 :SQL映射文件和Mapper接口必须同包同名
1.全部标签介绍
内部标签是无序的
?cache –命名空间的二级缓存配置
?cache-ref – 其他命名空间缓存配置的引用。
?resultMap – 自定义结果集映射
?parameterMap – 已废弃!老式风格的参数映射
?sql –抽取可重用语句块。
?insert – 映射插入语句
?update – 映射更新语句
?delete – 映射删除语句
?select – 映射查询语句
1.增删改查标签
使用原生的mybatis(不结合Spring),对于增删改操作:需要提交事务
1.手动提交:sqlSession.commit();
2.自动提交:sqlSessionFactory.openSession(true)
代码示例
Mapper 接口:
public interface BooksMapper {
public Book getBookById(Integer bid);
public void insertBook(Book book);
public void updateBook(Book book);
public void deleteBookByBid(Integer bid);
}
SQL 映射文件:
insert into book values(null,#{title},#{author},#{price},#{sales},#{stock},#{imgPath})
update book set title=#{title} where id =#{id}
delete from book where id = #{bid}
id,title,author,price,sales,stock,img_path
2.sql片段标签:做sql语句重用
1、sql抽取:经常将要查询的列名,或者插入用的列名抽取出来方便引用
2、include来引用已经抽取的sql:
3、include还可以自定义一些property,sql标签内部就能使用自定义的属性
? include-property:取值的正确方式${prop},
? #{不能使用这种方式}
id,title,author,price,sales,stock,img_path
在使用到的地方通过 include 标签引入即可:
3.resultMap 标签:结果集映射
ResultMap 标签作用:
1.做结果映射
2.将有关联关系的表的数据直接映射为 POJO 对象的关联关系。
这个功能是 select标签的 resultType 属性所不具备的,resultType 属性只能完成单表映射。
注意:resultType和resultMap都是处理sql语句返回结果,只能同时存在一个属性。

当表的列名和 pojo 对象的属性名不对应的时候?有几种解决方案
? 1. 给表的列名起别名
? 2. 在 mybatis 的全局配置文件中开启 mapUnderscoreToCamelCase (下划线转驼峰命名)
? 3. 使用 ResultMap 标签
2.在映射文件中获取接口各种类型的入参
各种各样的请求参数:
1.普通类型单个参数:#{随便写}
2.普通类型多个参数[两个及以上]:
? 在每个参数前面加一个@Param(value="") , #{@Param注解value属性值}
3.参数是一个POJO对象:#{pojo对象的属性名}
4.参数是一个Map对象:#{map的key}
5.参数是一个List、Collection:#{list[下标]} | #{collection[下标]}|#{@Param注解value属性值[下标]} @Param****注解不可省略
6.参数是一个数组:#{array[下标]}| |#{@Param注解value属性值[下标]}
7.参数是复杂类型[由上面任意组合而成]:#{@Param注解的value属性值}
Mapper 接口:
public Map getUserMapByUid(Integer uid);
public int deleteByUid(Integer uid);
public int getTotalRecord();
@MapKey("uid")//专门用来指定将表的哪一列作为返回值Map的key
public Map getUsersMap();
public User getUserByUserNameAndPassword(@Param("username") String username,@Param("username")String password);
Mapper 的 SQL 映射文件:
delete from user where uid = #{uid}
3.如何映射接口中各种类型的返参
1.普通数据类型:int、Integer、String、double、Double、boolean
? 增删改: 只需要在接口处声明返回值类型即可,在SQL映射文件中不需要指定resultType
? 查询: 需要在接口处声明返回值类型,同时必须在SQL映射文件中指定返回结果类型
2.POJO对象: 需要在接口处声明返回值类型,同时指定resultType为POJO对象全类名
3.POJO对象列表:需要在接口处声明返回值类型,同时指定resultType为POJO对象全类名
4.Map:
? 返回一行数据:接口处声明Map
? 返回多行数据:接口处声明Map<表的主键类型,POJO对象类型>,同时指定resultType为Map类型,并在接口上使用注解@MapKey("uid")
public interface UserMapper {
public User getUserByUnameAndPwd(@Param("uname") String uname, @Param("pwd") String pwd);
public void insertUser(User user);
public User getUerByMap(Map map);
public List getUsersByUids(List ids);
public List getUsersByComplex(@Param("map") Map map, @Param("user")User user);
public int deleteUser(List ids);
public int getTotalRecord();
@MapKey("id")
public Map getUserByUnames(String[] unames);
@MapKey("uname")
public Map getUserByMap2();
}
select last_insert_id()
insert into t_user values(null,#{uname},#{pwd},#{email},#{role})
delete from t_user where id in (#{list[0]},#{list[1]},#{list[2]})
6.基于注解的 SQL 语句
不需要xml文件,不建议使用
@Insert("insert into users(name,age) values(#{name},#{age})")
public void insertT(User user);
@Delete("delete from users where id=#{id}")
public void deleteById(int id);
@Update("update users set name=#{name},age=#{age} where id=#{id}")
public void updateT(User user);
@Select("select * from users where id=#{id}")
public User getUser(int id);
@Select("select * from users")
public List getAllUsers();
7.#{}与${}的区别
区别:
? #{}:是采用占位符的方式[?],是以预编译的形式,将参数设置到 sql 语句中,相当于PreparedStatement对象,能防止 sql 注入
? ${}:取出的值直接拼装在 sql 语句中,会有sql注入等安全问题,相当于 Statement 对象;
大多情况下,我们取参数的值都应该去使用#{};
原生 JDBC 不支持使用占位符预编译的一些情况,我们就可以使用${}进行取值来拼接,#{}只是取出参数中的值!
在某些情况下,比如 获取表名、排序字段、有些模糊查询;按照年份分表拆分
select * from ${year}_salary where xxx;[表名不支持预编译]
select * from tbl_employee order by ${f_name} ${order} :排序(字段名、asc|desc)是不支持预编译的!
select * from ${name} where title like '${condition}' order by ${orderByField} desc limit 0,1
? 模糊查询:name like concat('%',#{value},'%') //name like concat(concat('%', #{name}), '%') **orcle数据库支持
? name like '%${name}%'
? "%"#{value}"%"
8.关联查询:一对多、多对一。。
表与表之间关系:一对一、一对多、多对一、多对多
Mybatis中:
? x 对一:一对一、多对一 (3种查询方法)
? x 对多:一对多、多对多 (2种查询方法)
如何查询关联表
1.x对一[三种解决方案]
查询语句
第一种:连缀的方式
第二种解决方案:使用association标签(用来给引用数据类型属性赋值)
association标签 : 处理对一关联关系。
property标签 : 指定属性名(引用数据类型的)
javaType标签 : 指定属性的类型(引用数据类型的)
第三种解决方案:分步查询
property:指定属性名(引用数据类型的)
select:指定要引用哪个select语句(可以引用外部的sql语句,使用 namespace.id )
column:将查询出来的某一列传递给引用的select语句
针对分步查询,存在懒加载机制。
懒加载[按需加载]: 需要的时候就加载,不需要的时候就不加载
? 多表时,后续没有用到哪个表的信息,其对应的分步的sql语句就不执行
懒加载分为:局部懒加载和全局懒加载
注意:如果全局懒加载和局部懒加载都开启,就近原则,听局部懒加载的。
? 局部懒加载:适用于只有少量的 SQL 语句需要懒加载的时候
? fetchType:指定局部懒加载策略,值:eager立即加载 lazy:懒加载
? 全局懒加载:适用于大部分的 SQL 语句都需要懒加载。
? 在 myatis-config.xml 的全局配置文件中配置:
//开启自动驼峰命名映射
2.x对多[二种解决方案]
查询语句
第一种:在 resultMap 标签内部嵌套collection标签(用来给集合类型等对多的属性赋值)
属性:
? property:指定属性名(集合类型等对多的)
? ofType:指定数据类型(关联属性集合中元素的数据类型)
第二种:在 resultMap 标签内部嵌套 collection 标签,分步查询(可以设置懒加载,同上)
property:指定属性名(集合类型等对多的)
select:指定要引用哪个select语句(可以引用外部的sql语句,使用 namespace.id 即sql的id )
column:将查询出来的某一列传递给引用的select语句
如何创建关联表
一对一表的创建:
第一种:在多对一的基础上,对多的一端的外键,加 unique 唯一约束。
? 从表的外键列引用主表被参照的列时,外键列加唯一约束
CREATE TABLE wife (
wid int PRIMARY KEY AUTO_INCREMENT,
wife_name varchar(30) NOT NULL
);
CREATE TABLE husband (
hid int PRIMARY KEY AUTO_INCREMENT,
husband_name varchar(30) NOT NULL,
wid int UNIQUE,
FOREIGN KEY (wid) REFERENCES wife (wid)
)
第二种:从表的主键列同时还作为外键列引用主表的主键
CREATE TABLE wife (
wid int PRIMARY KEY AUTO_INCREMENT,
wname varchar(30) NOT NULL
);
CREATE TABLE husband (
hid int PRIMARY KEY,
husband_name varchar(30) NOT NULL,
FOREIGN KEY (hid) REFERENCES wife (wid)
)
多对多表的创建:
创建中间表,中间表引用多个外部表的主键作为外键,主键为多表主键的联合主键(任何一列都可以重复,但不能同时重复)
CREATE TABLE teacher (
tid int PRIMARY KEY AUTO_INCREMENT,
tname varchar(30) NOT NULL
);
CREATE TABLE student (
sid int PRIMARY KEY AUTO_INCREMENT,
sname varchar(30) NOT NULL
);
CREATE TABLE teacher_student (
tid int,
sid int,
PRIMARY KEY (tid, sid),
FOREIGN KEY (tid) REFERENCES teacher (tid),
FOREIGN KEY (sid) REFERENCES student (sid)
);
9.动态 SQL
功能:根据复杂的业务条件灵活的拼接成不同的SQL语句
动态 SQL 中涉及到几个标签:
if: 相当于jstl中的标签,用于但分支判断
choose (when, otherwise):相当于jstl中的用于多分支判断
trim (where, set):trim标签用于修改SQL语句
where标签:代替SQL语句中的where关键字
set标签:代替SQL语句中的set关键字
foreach标签:遍历集合或者数组
传单个值test=。。。只能写value
select *
from t_travelgroup
code = #{value} or name like concat("%",#{value},"%") or helpCode = #{value}
if 标签
提供的参数有啥就获取啥,然后拼接到sql语句中
public List getBooksByCondition(Map map);
where标签
用来代替where关键字,where标签能将where标签内部所有拼接之后的SQL语句最前面的and|or关键字给去掉
select id="getBooksByCondition" resultType="com.atguigu.pojo.Book">
select * from t_book
and id = #{id}
and bname = #{bname}
and author = #{author}
and price = #{price}
trim 标签
属性:
? prefix: 表示前缀,具体是指当trim标签内部所有的SQL拼接完毕之后,在最前面加的SQL关键字
? prefixOverrides:表示前缀覆盖,具体是指使用指定的SQL关键字将trim标签内部所有的SQL拼接完毕之后最前面的SQL关键字去掉
? suffix: 表示后缀,具体是指当trim标签内部所有的SQL拼接完毕之后,在最后面加的SQL关键字
? suffixOverrides:表示后缀覆盖,具体是指使用指定的SQL关键字将trim标签内部所有的SQL拼接完毕之后最后面的SQL关键字去掉
set 标签
用于代替 sql 语句中的 set 关键字,可以将 set 标签内部的 sql 语句 最前 | 后面 多出来的,[逗号]去掉。
update t_book
, bname = #{bname}
, author = #{author}
, price = #{price}
where id =#{id}
此处也可以使用 trim 标签解决
update t_book
bname = #{bname} ,
author = #{author} ,
price = #{price} ,
where id =#{id}
choose (when, otherwise)标签
用于多分支判断,类似于 java 基础学过的 switch..case 语句。
需求:如果前端携带了书名,就用书名查询,如果携带了作者信息,就用作者信息查,如果携带了价格,就用价格查询,如果这些字段都携带了,就用书名查询,如果都没携带,就查询所有的书籍。
foreach标签:遍历集合和数组
collection:表示要遍历的集合
item:指定一个变量接收集合中遍历出来的元素
separator:指定元素与元素之间的分隔符
open:当foreach标签遍历完集合中所有的元素之后,在元素的最前面加的sql字符串
close:当foreach标签遍历完集合中所有的元素之后,在元素的最后面加的sql字符串
批量添加数据|删除
insert into t_book values
(null,#{book.bname},#{book.bimg},#{book.author},#{book.price},#{book.sales},#{book.bcount},#{book.bstatus})
10.MyBatis 的缓存机制
MyBatis系统中默认定义了两级缓存
一级缓存和二级缓存。
1、默认情况下,只有一级缓存(SqlSession级别的缓存,也称为本地缓存)开启。
2、二级缓存需要手动开启和配置,他是基于namespace级别的缓存。
3、为了提高扩展性。MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存
一级缓存
? 一级缓存(local cache), 即本地缓存, 作用域默认为sqlSession。当 Session flush 或 close 后, 该Session 中的所有 Cache 将被清空。
? 本地缓存不能被关闭, 但可以调用 clearCache()来清空本地缓存, 或者改变缓存的作用域.
? 在mybatis3.1之后, 可以配置本地缓存的作用域,在 mybatis.xml 中配置

一级缓存演示&失效情况
? 同一次会话期间只要查询过的数据都会保存在当
? 前SqlSession的一个Map中
? key:hashCode+查询的SqlId+编写的sql查询语句+参数
? 一级缓存失效的四种情况
? 1、不同的SqlSession对应不同的一级缓存
? 2、同一个SqlSession但是查询条件不同
? 3、同一个SqlSession两次查询期间执行了任何一次增删改操作
? 4、同一个SqlSession两次查询期间手动清空了缓存 —— 执行sqlSession.clearCache()方法
二级缓存
当二级缓存开启后,同一个命名空间(namespace) 所有的操作语句,都影响着一个共同的 cache,也就是二级缓存被多个 SqlSession 共享,
是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
? 二级缓存(second level cache),全局作用域缓存
? 基于namespace级别的缓存:一个namespace对应一个二级缓存(即一个XXMapper.xml文件对应一个二级缓存)
? 二级缓存默认不开启,需要手动配置
? MyBatis提供二级缓存的接口以及实现,缓存实现要求POJO实现Serializable接口
? 二级缓存在 SqlSession 关闭或提交之后才会生效
工作机制:
? 1、一个会话,查询一条数据,这个数据就会被放在当前会话的一级缓存中;
? 2、如果会话关闭;一级缓存中的数据会被保存到二级缓存中;新的会话查询信息,就可以参照二级缓存中的内容;
? 3、sqlSession=EmployeeMapper>查出的Employee对象
? DepartmentMapper===>查出的Department对象
? 不同namespace查出的数据会放在自己对应的缓存中(底层是map)
? 效果:数据会从二级缓存中获取
? 查出的数据都会被默认先放在一级缓存中。
? 只有会话提交或者关闭以后,一级缓存中的数据才会转移到二级缓存中
? 二级缓存使用步骤
? 1、全局配置文件中开启二级缓存
~~~xml
? 2、需要使用二级缓存的XXMapper.xml映射文件处使用cache标签配置缓存
~~~xml
? 3、注意:POJO需要实现Serializable接口
缓存有关设置
1、全局setting的cacheEnable:
配置二级缓存的开关。一级缓存一直是打开的。
2、select标签的useCache属性:
配置这个select是否使用二级缓存。一级缓存一直是使用的
3、sql标签的flushCache属性:
增删改默认flushCache=true。sql执行以后,会同时清空一级和二级缓存。
查询默认flushCache=false。
4、sqlSession.clearCache():
只是用来清除一级缓存。
5、当在某一个作用域 (一级缓存Session/二级缓存Namespaces) 进行了 C/U/D 操作后,默认该作用域下所有select 中的缓存将被clear。

第三方缓存整合
EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。
MyBatis定义了Cache接口方便我们进行自定义扩展。
步骤:
1、导入ehcache包,以及整合包,日志包
? ehcache-core-2.6.8.jar、mybatis-ehcache-1.0.3.jar
? slf4j-api-1.6.1.jar、slf4j-log4j12-1.6.2.jar
2、编写ehcache.xml配置文件
3、配置cache标签
?
参照缓存:若想在命名空间中共享相同的缓存配置和实例。
可以使用 cache-ref 元素来引用另外一个缓存

11.SSM整合
12.MyBatis 逆向工程
MyBatis 逆向工程:是指 MyBatis 可以根据表直接生成表对应的 pojo 类、Mapper 接口、 SQL 映射文件。
MyBatis 逆向工程使用 MyBatis Generator(MyBatis 生成器)完成。
MyBatis Generator 简称为 MBG
MyBatis 逆向工程只是针对单表的,对于有关联关系的表生成不了连表操作。
MyBatis Generator 具体使用:
1.原生ssm工程中使用
第一步:创建动态 web 工程,导入 jar 包
? mybatis-generate-core、mysql、mybatis
第二步:准备 MyBatis Generator 配置文件:generatorConfig.xml
? 需要注意:该配置文件必须放在当前项目文件夹下
<?xml version="1.0" encoding="UTF-8"?>
第三步:使用 java 代码进行操作即可
public static void main(String[] args) throws Exception {
List warnings = new ArrayList();
boolean overwrite = true;
File configFile = new File("generatorConfig.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
myBatisGenerator.generate(null);
}
2.maven工程中使用
1. maven的pom文件中添加插件
org.mybatis.generator
mybatis-generator-maven-plugin
1.3.0
org.mybatis.generator
mybatis-generator-core
1.3.2
com.mchange
c3p0
0.9.2
mysql
mysql-connector-java
5.1.8
2. 添加配置文件
<?xml version="1.0" encoding="UTF-8"?>
13.源码解析
四大对象及其调用的重要方法
Executor (update, query, flushStatements, commit, rollback,getTransaction, close, isClosed) --simple(默认) resuer batch 3种(详见下)
StatementHandler (prepare, parameterize, batch, update, query)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
Executor:执行器,由它来调度StatementHandler、并由StatementHandler来调度ParameterHandler、ResultSetHandler等来执行对应的SQL。
StatementHandler:使用数据库的Statemet(PreparedStatement)执行操作
ParameterHandler:处理SQL参数;比如查询的where条件、和insert、update的字段值的占位符绑定。
ResultSetHandler:处理结果集ResultSet的封装返回。将数据库字段的值赋给实体类对象的成员变量。
疑问:MyBatis是对JDBC的封装。那SqlSession是不是就是对Connection的封装,SqlSessionFactory是否就是对DrimverMangaer的封装,Mapper是否就是对Statement的封装。错了,都错了。
SqlSession是一个接口,这里返回的是其实现类DefaultSqlSession的一个实例。
其中有一个Executor类型的成员变量。其实进行数据库操作时SqlSession就是一个门面,真正干活的却是Executor。
Executor 执行目标方法时创建了StatementHandler(详见下图)
StatementHandler(预编译时调用了)>ParameterHandler(对参数安装TypeHandle规则进行了转化)=>parameterObject(具体的参数)
RoutingStatementHandler:根据stateType类型生成 SimpleStatementHandler / PreparedStatementHandler(默认) / CallableStatementHandler
1.源码解析总结
@Test
public void test01() throws IOException {
// 1、获取sqlSessionFactory对象,详见下
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
// 2、获取sqlSession对象,详见下
SqlSession openSession = sqlSessionFactory.openSession();
try {
// 3、获取接口的实现类对象,详见下
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
//4、执行增删改查方法
Employee employee = mapper.getEmpById(1);
System.out.println(mapper.getClass());
System.out.println(employee);
} finally {
openSession.close();
}
}
1、获取sqlSessionFactory对象:
解析文件的每一个信息保存在Configuration中,返回包含Configuration的DefaultSqlSession;
注意:【MappedStatement】:代表一个增删改查的详细信息
2、获取sqlSession对象
返回一个DefaultSQlSession对象,包含Executor和Configuration;
这一步会创建Executor对象;
3、获取接口的代理对象(MapperProxy)
getMapper,使用MapperProxyFactory创建一个MapperProxy的代理对象
代理对象里面包含了,DefaultSqlSession(Executor)
4、执行增删改查方法
总结:
1、根据配置文件(全局,sql映射)初始化出Configuration对象
2、创建一个DefaultSqlSession对象,
他里面包含Configuration以及
Executor(根据全局配置文件中的defaultExecutorType创建出对应的Executor)
3、DefaultSqlSession.getMapper():拿到Mapper接口对应的MapperProxy;
4、MapperProxy里面有(DefaultSqlSession);
5、执行增删改查方法:
1)、调用DefaultSqlSession的增删改查(Executor);
2)、会创建一个StatementHandler对象。
(同时也会创建出ParameterHandler和ResultSetHandler)
3)、调用StatementHandler预编译参数以及设置参数值;
使用ParameterHandler来给sql设置参数
4)、调用StatementHandler的增删改查方法;
5)、ResultSetHandler封装结果
注意:
四大对象每个创建的时候都有一个interceptorChain.pluginAll(parameterHandler);
2.具体细节
0.图示

1、根据配置文件创建SQLSessionFactory


parser:解析 XML (DOM解析 Document对象)

Configuration:保存了所有配置文件的详细信息

MappedStatement:(全局Configuration中的一个重要属性)
? 表示一个增删改查标签

MapperRegistry:(全局Configuration中的一个重要属性)
· 内部维护一个map:knownMappers.put(type, new MapperProxyFactory
? MapperProxyFactory通过JDK动态代理生成Maper接口的代理对象

2、返回SqlSession的实现类DefaultSqlSession对象。
他里面包含了Executor和Configuration(Executor会在这一步被创建)
? ExecutorType:SIMPLE(默认)、REUSE、BATCH ---根据不用的ExecutorType来new 不同的Executor对象
SimpleExecutor是一种常规执行器,每次执行都会创建一个statement,用完后关闭。
ReuseExecutor是可重用执行器,将statement存入map中,操作map中的statement而不会重复创建statement。
BatchExecutor是批处理型执行器,doUpdate预处理存储过程或批处理操作,doQuery提交并执行过程。
? 二级缓存:
? 判断是否二级缓存(cacheEnabled默认为false),来将Executor包装一层
? 本质还是调用原来的Executor对象,只是在查询前先去查询缓存(第一次查询结果会放入缓存中),缓存中没有才会去DB查询

3、getMapper返回接口的代理对象,包含了SqlSession对象


4、查询流程


查询时先走缓存
? 缓存中保存的key:方法id(全类名.方法名) + sql + 参数xxx

14.MyBatis-插件开发
插件原理
在四大对象创建的时候
1、每个创建出来的对象不是直接返回的,而是 基于 interceptorChain.pluginAll(parameterHandler);
2、获取到所有的Interceptor(拦截器)(插件需要实现的接口);调用interceptor.plugin(target);返回target包装后的对象
3、插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP(面向切面)
? 我们的插件可以为四大对象创建出代理对象;
? 代理对象就可以拦截到四大对象的每一个执行;
插件会产生目标对象的代理对象

多个插件就会产生多层代理
? 后面的插件会把前面的包起来再生成代理对象,而执行interceptor方法时是外层方法先执行,所以后面的插件定义的方法先执行

即:创建动态代理的时候,是按照插件配置顺序创建层层代理对象。执行目标方法的之后,按照逆向顺序执行

插件编写步骤
1、编写Interceptor的实现类
2、使用@Intercepts注解完成插件签名
3、将写好的插件注册到全局配置文件的plugins标签中
代码示例:
import java.util.Properties;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
/**
* 完成插件签名:
* 告诉MyBatis当前插件用来拦截哪个对象(4大对象)的哪个方法,args用来区分重载的方法
*/
@Intercepts(
{
@Signature(type=StatementHandler.class,method="parameterize",args=java.sql.Statement.class)
})
public class MyFirstPlugin implements Interceptor{
/**
* intercept:拦截:
* 拦截目标对象的目标方法的执行;
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// TODO Auto-generated method stub
System.out.println("MyFirstPlugin...intercept:"+invocation.getMethod());
//动态的改变一下sql运行的参数:以前1号员工,实际从数据库查询3号员工
Object target = invocation.getTarget();
System.out.println("当前拦截到的对象:"+target);//target为StatementHandler对象(@Signature中type表明了)
//拿到:StatementHandler==>ParameterHandler===>parameterObject
//拿到target的元数据
MetaObject metaObject = SystemMetaObject.forObject(target);
Object value = metaObject.getValue("parameterHandler.parameterObject");
System.out.println("sql语句用的参数是:"+value);
//修改完sql语句要用的参数
metaObject.setValue("parameterHandler.parameterObject", 11);
//执行目标方法
Object proceed = invocation.proceed();
//返回执行后的返回值
return proceed;
}
/**
* plugin:
* 包装目标对象的:包装:为目标对象创建一个代理对象
*/
@Override
public Object plugin(Object target) {
// TODO Auto-generated method stub
//我们可以借助Plugin的wrap方法来使用当前Interceptor包装我们目标对象
System.out.println("MyFirstPlugin...plugin:mybatis将要包装的对象"+target);
Object wrap = Plugin.wrap(target, this);
//返回为当前target创建的动态代理
return wrap;
}
/**
* setProperties:
* 将插件注册时 的property属性设置进来
*/
@Override
public void setProperties(Properties properties) {
// TODO Auto-generated method stub
System.out.println("插件配置的信息:"+properties);
}
}
PageHelper插件进行分页
MyBatis 分页插件 PageHelper 的具体使用:
第一步:导入 PageHelper 的 jar 包
第二步:在 mybatis 全局配置文件中配置分页插件
第三步:在 service 层写分页代码
@Test
public void test01() throws IOException {
// 1、获取sqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
// 2、获取sqlSession对象
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
Page
@Autowired
private BooksMapper booksMapper;
public PageInfo getPage(Integer pageNum){
BooksExample booksExample = new BooksExample();
//1.在查询列表之前使用PageHelper.startPage(pageNum, pageSize);
PageHelper.startPage(pageNum,5);
//2.查询列表
List books = booksMapper.selectByExample(booksExample);
//3.使用PageInfo对象构造器封装查询的列表
PageInfo pageInfo = new PageInfo<>(books,5);
return pageInfo;
}
15.Others
如何向数据表中插入数据后,获取主键值
要求:接口参数必须是 pojo 对象
第一种解决方案:适用于数据库支持表的自增主键:mysql、sqlserver(oracle不支持)
//useGeneratedKeys:表示使用数据表的自增主键
//keyProperty:指定使用pojo对象的哪个属性接收主键值
insert into user(uid,username,password,email) values(null,#{username},#{password},#{email})
第二种解决方案:适用所有的数据库
? keyColumn:表示查询表的哪一列作为主键,指定表的列名
? keyProperty:指定使用pojo对象的哪个属性接收主键值
? resultType:返回结果的类型
? order:表示当前selectKey标签中的SQL语句在当前insert标签的insert语句的执行顺序。
select last_insert_id()
insert into user(uid,username,password,email) values(null,#{usernmae},#{password},#{email})
测试代码
@Test
public void test04() throws Exception {
。。。
BookMapper mapper = sqlSession.getMapper(BookMapper.class);
Book book = new Book(0,"PP","ZZ","WW",12.9,233,555,1);
mapper.insertBook(book);
System.out.println(book.getId());
sqlSession.close();
}
批量操作
一般不会在全局配置文件mybatis-config.xml中配置ExecutorType,而是针对sqlSession级别来设置
@Test
public void testBatch() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
//可以执行批量操作的sqlSession
SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
long start = System.currentTimeMillis();
try{
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
for (int i = 0; i < 10000; i++) {
mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b", "1"));
}
openSession.commit();
long end = System.currentTimeMillis();
//批量:(预编译sql一次==>设置参数===>10000次===>执行(1次))
//Parameters: 616c1(String), b(String), 1(String)==>10000/4598次 (4598是根据控台打印所得)
//非批量:(每次都要 预编译sql=设置参数=执行)==》10000次 10200
System.out.println("执行时长:"+(end-start));
}finally{
openSession.close();
}
}
与Spring整合中,额外的配置一个可以专门用来执行批量操作的sqlSession
? 使用时,自动注入 SqlSession对象,调用它的getMapper(xxxMapper.class) 返回一个xxxMapper对象,此时调用此对象的crud方法即为批量操作
注意:
1、批量操作是在session.commit()以后才发送sql语句给数 据库进行执行的
2、如果想让其提前执行,以方便后续可能的查询操作获取数据,可以使用sqlSession.flushStatements()方 法,让其直接冲刷到数据库进行执行
存储过程
oracle分页:
? 步骤:1.子查询先获取行号rownum 2.对已有的行号rownum进行操作 (详见SQL笔记)
? rownum 是在已产生数据的基础上伪生成的编号(已有数据会自动生成rownum 这个列)
? 所以 使用rownum 必须在已有数据的基础上,因此Oracle分页才加入了多个子查询
--查询前3条:类似Sqlserver中的TOP 3
select *
from (select * from table_Name order by active_count desc)
where rownum <= 3;
--查询第2至第3条记录:
select *
from (select t.*, rownum as no
from (select * from table_Name order by active_count desc) t)
where no between 2 and 3;
--在TOP3条记录的基础上查询第2至第3条记录:
select *
from (select t.*, rownum as no
from (select * from table_Name order by active_count desc) t where rownum <= 3 )
where no between 2 and 3;
--查询第2条以后的记录:
select *
from (select t.*, rownum as no
from (select * from table_Name order by active_count desc) t)
where no >=2
select rownum ,e.* from emp e;
--查询员工信息的前5条数据 第一页数据
select rownum r,e.* from emp e where rownum <=5;
select * from (select rownum r,e.* from emp e where rownum <=5) t where r>0;
--查询员工信息的6-10条数据 第二页数据
select rownum r,e.* from emp e where rownum <=10;
select rownum,t.* from (select rownum r,e.* from emp e where rownum <=10) t where r>5;
--查询员工信息的11-15条数据 第三页数据
select rownum r,e. * from emp e where rownum<=15;
select * from (select rownum r,e. * from emp e where rownum<=15) t where r>10;
--分页规律总结:每页显示m条数据,查询第n页数据*
select * from (select rownum r,e. * from 要分页的表 e where rownum<=m*n) t where r>m*n-m ;
--分页查询员工信息按照工资排序后的 6-10条数据 第二页数据
select * from (select rownum r,t.* from (select * from emp order by sal) t where rownum<=10 ) where r>5
代码示例
1.创建 封装分页查询数据的java类
/**
* 封装分页查询数据
*/
@Data
public class OraclePage {
private int start;
private int end;
private int count;
private List emps;
}
2.在oracle中创建存储过程procedure hello_test
create or replace procedure
hello_test(p_start in int,
p_end in int,
p_count out int,
ref_cur out sys_refcursor) AS
BEGIN
select count(*) into p_count from emp;
open ref_cur for
select * from (
select e.*,rownum as rm from emp e where rownum<=p_end
)
where rm >= p_start;
END hello_test;
3.配置Oracle数据库环境
? 全局配置文件
3.xxxMapper接口中定义分页查询方法
public interface EmployeeMapper {
public void getPageByProcedure(OraclePage page);
}
4.写sql调用存储过程
5.调用分页方法
@Test
public void testProcedure() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
OraclePage page = new OraclePage();
page.setStart(1);
page.setEnd(5);
mapper.getPageByProcedure(page);
//存储过程需要两个入参(p_start,p_end),在OraclePage中设置好传入方法,
//查询结果的两个返参(p_count,ref_cur)会自动设置给OraclePage
System.out.println("总记录数:"+page.getCount());
System.out.println("查出的数据:"+page.getEmps().size());
System.out.println("查出的数据:"+page.getEmps());
}finally{
openSession.close();
}
}
自定义TypeHandler处理枚举
1、指定全局配置EnumOrdinalTypeHandler(Mybatis默认是使用EnumTypeHandler,存、取时都是用name属性)
? 在全局配置文件中指定,处理某个类时,使用指定TypeHandler处理

2.测试参数位置设置自定义TypeHandler
? 1.插入时,对单个字段处理 2.在全局配置文件中指明:对某个类型存、取时,都使用指定的TypeHandler

? 自定义TypeHandler
public class MyEnumEmpStatusTypeHandler implements TypeHandler {
/**
* 定义当前数据如何保存到数据库中
*/
@Override
public void setParameter(PreparedStatement ps, int i, EmpStatus parameter,
JdbcType jdbcType) throws SQLException {
// TODO Auto-generated method stub
System.out.println("要保存的状态码:"+parameter.getCode());
ps.setString(i, parameter.getCode().toString());
}
@Override
public EmpStatus getResult(ResultSet rs, String columnName)
throws SQLException {
// TODO Auto-generated method stub
//需要根据从数据库中拿到的枚举的状态码返回一个枚举对象
int code = rs.getInt(columnName);
System.out.println("从数据库中获取的状态码:"+code);
EmpStatus status = EmpStatus.getEmpStatusByCode(code);
return status;
}
@Override
public EmpStatus getResult(ResultSet rs, int columnIndex)
throws SQLException {
// TODO Auto-generated method stub
int code = rs.getInt(columnIndex);
System.out.println("从数据库中获取的状态码:"+code);
EmpStatus status = EmpStatus.getEmpStatusByCode(code);
return status;
}
@Override
public EmpStatus getResult(CallableStatement cs, int columnIndex)
throws SQLException {
// TODO Auto-generated method stub
int code = cs.getInt(columnIndex);
System.out.println("从数据库中获取的状态码:"+code);
EmpStatus status = EmpStatus.getEmpStatusByCode(code);
return status;
}
}
补充说明:
1.自定义类型处理器可以处理任意类型的字段对DB存、取时按照自定义的规则进行转换
2.可以在处理某个字段的时候告诉MyBatis用什么类型处理器(单个SQL级别):可以通过在xml的sql语句中增加对应配置来实现
注意:如果在参数位置修改TypeHandler,应该保证保存数据和查询数据用的TypeHandler是一样的
? 插入时,使用自定义的TypeHandler处理Enum

#{empStatus,typeHandler=xxxx}
? 查询时,使用自定义的TypeHandler处理Enum

实战中学到的知识
根据list(id的集合)内容order后将结果映射为map
方法使用示例:
//此处Map中k:String指字段名,v:Object为每行数据指导字段的值,所以只能使用Object封装
List
mapper接口
List
mapper.xml映射文件
函数:
group_concat([DISTINCT] 要连接的字段 [Order BY ASC/DESC 排序字段] [Separator '分隔符'])
? GROUP_CONCAT(... ORDER BY ...) ... GROUP BY..;
SELECT GROUP_CONCAT(attr_value ORDER BY attr_id) attr_values,sku_id FROM pms_sku_attr_value WHERE sku_id IN (1,2,3) GROUP BY sku_id;--默认分隔符为 , 可以指定分隔符
==》 
SELECT GROUP_CONCAT(attr_value ORDER BY attr_id separator '|') attr_values,sku_id FROM pms_sku_attr_value WHERE sku_id IN (1,2,3) GROUP BY sku_id;--指定分隔符
