文章目录
什么是Mybatis?
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了
几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置 和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
“半自动”的ORM框架能够很好的解决上面所讲的Hibernate的几个问题,半自动化”是相对于Hibernate的全自动化来说的。它的封装程度没有Hibernate那么高,不会自动生成全部的SQL语句,主要解决的是SQL和对象的映射问题。
Mybatis的使用配置阶段
引入依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<!--<version>3.5.4-snapshot</version>-->
<version>3.5.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
POJO对象
@Data
public class User implements Serializable {
private Integer id;
private String userName;
private String realName;
private String password;
private Integer age;
private Integer dId;
private Dept dept;
}
添加配置文件
在MyBatis中我们需要添加全局的配置文件和对应的映射文件。
全局配置文件,这里面是对MyBatis的核心行为的控制
<?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="db.properties"></properties>
<settings>
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!-- 控制全局缓存(二级缓存),默认 true-->
<setting name="cacheEnabled" value="true"/>
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<!--<setting name="proxyFactory" value="CGLIB" />-->
<!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
<!--
<setting name="localCacheScope" value="STATEMENT"/>
-->
<setting name="localCacheScope" value="SESSION"/>
</settings>
<typeAliases>
<!--<typeAlias alias="user" type="com.gupaoedu.vip.domain.User" />-->
<package name="com.gupaoedu.vip.domain"/>
</typeAliases>
<!-- List<String> VARCHAR -->
<typeHandlers>
<typeHandler handler="com.gupaoedu.vip.type.MyTypeHandler" jdbcType="VARCHAR" javaType="String"></typeHandler>
<!--<typeHandler handler="com.gupaoedu.vip.type.MyTypeHandler" ></typeHandler>-->
</typeHandlers>
<!-- 对象工厂 User userName -->
<objectFactory type="com.gupaoedu.vip.objectfactory.GpObjectFactory">
<property name="gupao" value="666"/>
</objectFactory>
<plugins>
<!--<plugin interceptor="com.gupaoedu.interceptor.FirstInterceptor">
<property name="testProp" value="1000"/>
</plugin>-->
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql" />
<!-- 该参数默认为false -->
<!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
<!-- 和startPage中的pageNum效果一样 -->
<property name="offsetAsPageNum" value="true" />
<!-- 该参数默认为false -->
<!-- 设置为true时,使用RowBounds分页会进行count查询 -->
<property name="rowBoundsWithCount" value="true" />
<!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
<!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型) -->
<property name="pageSizeZero" value="true" />
<!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
<!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
<!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
<property name="reasonable" value="false" />
<!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
<!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
<!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 -->
<!-- 不理解该含义的前提下,不要随便复制该配置 -->
<property name="params" value="pageNum=start;pageSize=limit;" />
<!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
<property name="returnPageInfo" value="check" />
</plugin>
</plugins>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
关联的映射文件(即一个Mapper对应的xml文件),通常来说一张表对应一个,我们会在这个里面配置我们增删改查的SQL语句,以及参数和返回的结果集的映射关系。
<?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.gupaoedu.vip.mapper.UserMapper">
<resultMap id="BaseResultMap" type="user">
<id property="id" column="id" jdbcType="INTEGER"/>
<result property="userName" column="user_name" jdbcType="VARCHAR" />
<result property="realName" column="real_name" jdbcType="VARCHAR" />
<result property="password" column="password" jdbcType="VARCHAR"/>
<result property="age" column="age" jdbcType="INTEGER"/>
<result property="dId" column="d_id" jdbcType="INTEGER"/>
</resultMap>
<sql id="baseSQL">
id,user_name,real_name,password,age,d_id
</sql>
<select id="selectUserById" resultType="com.gupaoedu.vip.domain.User" statementType="PREPARED" >
select
id,
user_name userName,
real_name realName,
password,
age,
d_id
from t_user where id = #{id}
</select>
<!-- $只能用在自定义类型和map上 -->
<select id="selectUserByBean" parameterType="user" resultMap="BaseResultMap" >
select * from t_user where user_name = '${userName}'
</select>
<select id="selectUserList" resultMap="BaseResultMap" >
select * from t_user
</select>
</mapper>
数据库属性的配置文件
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatisdb?characterEncoding=utf-8&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456
Mybatis使用实操阶段
@Test
public void test1() throws Exception{
// 1.获取配置文件
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
// 2.加载解析配置文件并获取SqlSessionFactory对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
// 3.根据SqlSessionFactory对象获取SqlSession对象
SqlSession sqlSession = factory.openSession();
// 4.通过SqlSession中提供的 API方法来操作数据库
List<User> list = sqlSession.selectList("com.gupaoedu.vip.mapper.UserMapper.selectUserList");
for (User user : list) {
System.out.println(user);
}
// 5.关闭会话
sqlSession.close();
}
效果
这种方式其实就是通过SqlSession中给我们提供的相关的API方法来执行对应的CRUD操作,查找我们写
的SQL语句是通过 namespace+"."+id的方式实现的,即通过Mapper的位置+Mapper的指定方法
这样的调用方式还是会存在一些问题:
- Statement ID是硬编码,维护起来很不方便;
- 不能在编译时进行类型检查,如果namespace或者Statement ID输错了,只能在运行的时候报
错。
所以我们通常会使用第二种方式,也是新版的MyBatis里面推荐的方式:定义一个Mapper接口的方
式。这个接口全路径必须跟Mapper.xml里面的namespace对应起来,方法也要跟Statement ID一一对
应。
@Test
public void test2() throws Exception{
// 1.获取配置文件
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
// 2.加载解析配置文件并获取SqlSessionFactory对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
// 3.根据SqlSessionFactory对象获取SqlSession对象
SqlSession sqlSession = factory.openSession();
// 4.通过SqlSession中提供的 API方法来操作数据库
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> list = mapper.selectUserList();
for (User user : list) {
System.out.println(user);
}
// 5.关闭会话
sqlSession.close();
}
Mybatis特点
- 特点
- 使用连接池对连接进行管理
- SQL和代码分离,集中管理
- 结果集映射
- 参数映射和动态SQL
- 重复SQL的提取
- 缓存管理
- 插件机制
Hibernate和MyBatis跟DbUtils、Spring JDBC一样,都是对JDBC的一个封装,我们去看源码,最后
一定会看到Connection、Statement和ResultSet这些对象。
- 如何选择?
- 在一些业务比较简单的项目中,我们可以使用Hibernate
- 如果需要更加灵活的SQL,可以使用MyBatis,对于底层的编码,或者性能要求非常高的场合,可
以用JDBC; - 实际上在我们的项目中,MyBatis和Spring JDBC是可以混合使用的;
- 当然,我们也根据项目的需求自己写ORM框架。
详细介绍MyBatis核心配置文件
全局配置文件
- 标签
- configuration(配置)
- properties(属性)
- settings(设置)
- typeAliases(类型别名)
- typeHandlers(类型处理器)
- objectFactory(对象工厂)
- plugins(插件)
- environments(环境配置)
- environment(环境变量)
- transactionManager(事务管理器)
- dataSource(数据源)
- environment(环境变量)
- databaseIdProvider(数据库厂商标识)
- mappers(映射器)
configuration
configuration是整个配置文件的根标签,实际上也对应着MyBatis里面最重要的配置类Configuration。它贯穿MyBatis执行流程的每一个环节。我们打开这个类看一下,这里面有很多的属性,跟其他的子标签也能对应上。
properties
第一个一级标签是properties,用来配置参数信息,比如最常见的数据库连接信息。为了避免直接把参数写死在xml配置文件中,我们可以把这些参数单独放在properties文件中,用properties标签引入进来,然后在xml配置文件中用${}引用就可以了。可以用resource引用应用里面的相对路径,也可以用url指定本地服务器或者网络的绝对路径。
<properties resource="db.properties"></properties>
这就代表属性在db.properties这个文件里面
settings
这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。 下表描述了设置中各
项设置的含义、默认值等。
详细介绍这篇文章有
例子
<settings>
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!-- 控制全局缓存(二级缓存),默认 true-->
<setting name="cacheEnabled" value="true"/>
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<!--<setting name="proxyFactory" value="CGLIB" />-->
<!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
<!--
<setting name="localCacheScope" value="STATEMENT"/>
-->
<setting name="localCacheScope" value="SESSION"/>
</settings>
typeAliases
TypeAlias是类型的别名,跟Linux系统里面的alias一样,主要用来简化类名全路径的拼写。比如我们的参数类型和返回值类型都可能会用到我们的Bean,如果每个地方都配置全路径的话,那么内容就比较多,还可能会写错。我们可以为自己的Bean创建别名,既可以指定单个类,也可以指定一个package,自动转换。
<typeAliases>
<!--<typeAlias alias="user" type="com.gupaoedu.vip.domain.User" />-->
<package name="com.gupaoedu.vip.domain"/>
</typeAliases>
配置后就可以在UserMapper.xml直接用user了,不用那个com…User了
MyBatis里面有很多系统预先定义好的类型别名,在TypeAliasRegistry中。所以可以用string代替
java.lang.String。
TypeHandler
由于Java类型和数据库的JDBC类型不是一一对应的(比如String与varchar、char、text),所以我
们把Java对象转换为数据库的值,和把数据库的值转换成Java对象,需要经过一定的转换,这两个方向的转换就要用到TypeHandler。
当参数类型和返回值是一个对象的时候,我没有做任何的配置,为什么对象里面的一个String属性,
可以转换成数据库里面的varchar字段?
这是因为MyBatis已经内置了很多TypeHandler(在type包下),它们全部全部注册在TypeHandlerRegistry中,他们都继承了抽象类BaseTypeHandler,泛型就是要处理的Java数据类型。这个也是为什么大部分类型都不需要处理。当我们查询数据和登记数据,做数据类型转换的时候,就会自动调用对应的TypeHandler的方法。
我们可以自定义一个TypeHandler来帮助我们简单的处理数据,比如查询的结果的字段如果是一个字符串,且值为"zhangsan"就修饰下这个信息
public class MyTypeHandler extends BaseTypeHandler<String> {
/**
* 插入数据的时候回调的方法
* @param ps
* @param i
* @param parameter
* @param jdbcType
* @throws SQLException
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
//System.out.println("---------------setNonNullParameter1:"+parameter);
ps.setString(i, parameter);
}
//下面这三个方法都是查找结果后,进入的,
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String name = rs.getString(columnName);
if("zhangsan".equals(name)){
return name+"666";
}
return name;
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String name = rs.getString(columnIndex);
if("zhangsan".equals(name)){
return name+"666";
}
return name;
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String name = cs.getString(columnIndex);
if("zhangsan".equals(name)){
return name+"666";
}
return name;
}
}
同时将我们的处理器在全局配置文件中注册下
<typeHandlers>
<typeHandler handler="com.gupaoedu.type.MyTypeHandler"></typeHandler>
</typeHandlers>
然后我们在映射文件中配置对应的处理器
效果
可以看到返回的结果zhangsan加上了666,说明查找完后,就是会进入typeHandler的方法,然后可以进行自定义处理
objectFactory
当我们把数据库返回的结果集转换为实体类的时候,需要创建对象的实例,由于我们不知道需要处理
的类型是什么,有哪些属性,所以不能用new的方式去创建。只能通过反射来创建。
在MyBatis里面,它提供了一个工厂类的接口,叫做ObjectFactory,专门用来创建对象的实例
(MyBatis封装之后,简化了对象的创建),里面定义了4个方法。
package org.apache.ibatis.reflection.factory;
import java.util.List;
import java.util.Properties;
public interface ObjectFactory {
//作用:设置参数时调用
default void setProperties(Properties properties) {
}
//创建对象(调用无参构造函数)
<T> T create(Class<T> var1);
//创建对象(调用带参数构造函数)
<T> T create(Class<T> var1, List<Class<?>> var2, List<Object> var3);
//判断是否集合
<T> boolean isCollection(Class<T> var1);
}
ObjectFactory有一个默认的实现类DefaultObjectFactory。创建对象的方法最终都调用了instantiateClass(),这里面能看到反射的代码。默认情况下,所有的对象都是由DefaultObjectFactory创建。
源码
private <T> T instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
try {
Constructor constructor;
if (constructorArgTypes != null && constructorArgs != null) {
constructor = type.getDeclaredConstructor((Class[])constructorArgTypes.toArray(new Class[0]));
try {
return constructor.newInstance(constructorArgs.toArray(new Object[0]));
} catch (IllegalAccessException var7) {
if (Reflector.canControlMemberAccessible()) {
constructor.setAccessible(true);
return constructor.newInstance(constructorArgs.toArray(new Object[0]));
} else {
throw var7;
}
}
} else {
constructor = type.getDeclaredConstructor();
try {
return constructor.newInstance();
} catch (IllegalAccessException var8) {
if (Reflector.canControlMemberAccessible()) {
constructor.setAccessible(true);
return constructor.newInstance();
} else {
throw var8;
}
}
}
} catch (Exception var9) {
String argTypes = (String)((List)Optional.ofNullable(constructorArgTypes).orElseGet(Collections::emptyList)).stream().map(Class::getSimpleName).collect(Collectors.joining(","));
String argValues = (String)((List)Optional.ofNullable(constructorArgs).orElseGet(Collections::emptyList)).stream().map(String::valueOf).collect(Collectors.joining(","));
throw new ReflectionException("Error instantiating " + type + " with invalid types (" + argTypes + ") or values (" + argValues + "). Cause: " + var9, var9);
}
}
自己写一个
public class TykObjectFactory extends DefaultObjectFactory {
/**
* 重写通过无参构造方法创建实例的方法
* @param type
* @param <T>
* @return
*/
@Override
public <T> T create(Class<T> type) {
System.out.println("Object Factory .... create ");
if(type.equals(User.class)){
// 创建的类型如果是User类型 我们就自己来创建,如果不是就用父类的create方法
User user = new User();
user.setUserName("ObjectFactory 测试");
return (T) user;
}
return super.create(type);
}
}
测试类
public class Test {
public static void main(String[] args) {
TykObjectFactory factory = new TykObjectFactory();
User myBlog = (User) factory.create(User.class);
System.out.println(myBlog);
}
}
这样,就可以让MyBatis的创建实体类的时候使用我们自己的对象工厂
plugins
插件是MyBatis的一个很强大的机制。跟很多其他的框架一样,MyBatis预留了插件的接口,让
MyBatis更容易扩展。
官网详情
environments
environments标签用来管理数据库的环境,比如我们可以有开发环境、测试环境、生产环境的数
据库。可以在不同的环境中使用不同的数据库地址或者类型。
例
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
-
environment
一个environment标签就是一个数据源,代表一个数据库。这里面有两个关键的标签,一个是事务
管理器,一个是数据源。 -
transactionManager(事务管理器)
- 如果配置的是JDBC,则会使用Connection对象的commit()、rollback()、close()管理事务。
- 如果配置成MANAGED,会把事务交给容器来管理,比如JBOSS,Weblogic。因为我们跑的是本地程序,如果配置成MANAGE不会有任何事务。
- 如果是Spring + MyBatis,则没有必要配置,因为我们会直接在applicationContext.xml里面配置数
据源和事务,覆盖MyBatis的配置。
-
dataSource
数据源,顾名思义,就是数据的来源,一个数据源就对应一个数据库。在Java里面,它是对数据库连接的一个抽象。
一般的数据源都会包括连接池管理的功能,所以很多时候也把DataSource直接称为连接池,准确的说法应该是:带连接池功能的数据源。
mappers
标签配置的是映射器,也就是Mapper.xml的路径。这里配置的目的是让MyBatis在启动
的时候去扫描这些映射器,创建映射关系。
我们有四种指定Mapper文件的方式:
a.使用相对于类路径的资源引用(resource)
<mappers> <mapper resource="UserMapper.xml"/> </mappers>
b.使用完全限定资源定位符(绝对路径)(URL)
<mappers> <mapper resource="file:///app/sale/mappers/UserMapper.xml"/> </mappers>
c.使用映射器接口实现类的完全限定类名
<mappers> <mapper class="com.gupaoedu.mapper.UserMapper"/> </mappers>
d.将包内的映射器接口实现全部注册为映射器(最常用)
<mappers> <mapper class="com.gupaoedu.mapper"/> </mappers>
详细介绍Mybatis映射文件
- 标签
- cache – 该命名空间的缓存配置。
- cache-ref – 引用其它命名空间的缓存配置。
- resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
- sql – 可被其它语句引用的可重用语句块。
- insert – 映射插入语句。
- update – 映射更新语句。
- delete – 映射删除语句。
- select – 映射查询语句。
resultMap
是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。
<resultMap id="BaseResultMap" type="user">
<id property="id" column="id" jdbcType="INTEGER"/>
<result property="userName" column="user_name" jdbcType="VARCHAR" />
<result property="realName" column="real_name" jdbcType="VARCHAR" />
<result property="password" column="password" jdbcType="VARCHAR"/>
<result property="age" column="age" jdbcType="INTEGER"/>
<result property="dId" column="d_id" jdbcType="INTEGER"/>
</resultMap>
select
<select
id="selectPerson" <!--在命名空间中唯一的标识符,可以被用来引用这条语句 -->
parameterType="int" <!--将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。-->
resultType="hashmap" <!--期望从这条语句中返回结果的类全限定名或别名。 注意,如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。 resultType 和resultMap 之间只能同时使用一个。-->
resultMap="personResultMap" <!--对外部 resultMap 的命名引用。结果映射是 MyBatis 最强大的特性-->
flushCache="false" <!--将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。-->
useCache="true" <!--将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。-->
timeout="10" <!--这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。-->
fetchSize="256" <!--这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。 默认值为未设置(unset)(依赖驱动)。-->
statementType="PREPARED" <!--可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。-->
resultSetType="FORWARD_ONLY" <!--FORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或DEFAULT(等价于 unset) 中的一个,默认值为 unset (依赖数据库驱动)。-->
>
Mybatis最佳实践
动态sql
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据
不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,
MyBatis 显著地提升了这一特性的易用性。
如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相
识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。
- 标签
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
if
需要判断的时候,条件写在test中
<!-- if 的使用 -->
<select id="selectListIf" parameterType="user" resultMap="BaseResultMap" >
select
<include refid="baseSQL"></include>
from t_user
<where>
<if test="id != null">
and id = #{id}
</if>
<if test="userName != null">
and user_name = #{userName}
</if>
</where>
</select>
choose
需要选择一个条件的时候
<!-- choose 的使用 -->
<select id="selectListChoose" parameterType="user" resultMap="BaseResultMap" >
select
<include refid="baseSQL"></include>
from t_user
<where>
<choose>
<when test="id != null">
id = #{id}
</when>
<when test="userName != null and userName != ''">
and user_name like CONCAT(CONCAT('%',#{userName,jdbcType=VARCHAR}),'%')
</when>
<otherwise>
</otherwise>
</choose>
</where>
</select>
trim
需要去掉where、and、逗号之类的符号的时候
<!--
trim 的使用
替代where标签的使用
-->
<select id="selectListTrim" resultMap="BaseResultMap"
parameterType="user">
select <include refid="baseSQL"></include>
<!-- <where>
<if test="username!=null">
and name = #{username}
</if>
</where> -->
<trim prefix="where" prefixOverrides="AND |OR ">
<if test="userName!=null">
and user_name = #{userName}
</if>
<if test="age != 0">
and age = #{age}
</if>
</trim>
</select>
<!-- 替代set标签的使用 -->
<update id="updateUser" parameterType="User">
update t_user
<trim prefix="set" suffixOverrides=",">
<if test="userName!=null">
user_name = #{userName},
</if>
<if test="age != 0">
age = #{age}
</if>
</trim>
where id=#{id}
</update>
foreach
需要遍历集合的时候
<delete id="deleteByList" parameterType="java.util.List">
delete from t_user where id in
<!-- ( 1 , 2 , 3) -->
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item.id,jdbcType=INTEGER}
</foreach>
</delete>
动态SQL主要是用来解决SQL语句生成的问题。
批量插入
我们在项目中会有一些批量操作的场景,比如导入文件批量处理数据的情况(批量新增商户、批量修改商户信息),当数据量非常大,比如超过几万条的时候,在Java代码中循环发送SQL到数据库执行肯定是不现实的,因为这个意味着要跟数据库创建几万次会话。即使在同一个连接中,也有重复编译和执行SQL的开销。
例如循环插入10000条(大约耗时3秒钟):
public class Test03Batch {
public SqlSession session;
public void init() throws IOException {
// 1.获取配置文件
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
// 2.加载解析配置文件并获取SqlSessionFactory对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
// 3.根据SqlSessionFactory对象获取SqlSession对象
session = factory.openSession();
}
/**
* 循环插入10000
*/
@Test
public void test1() throws Exception{
init();
long start = System.currentTimeMillis();
UserMapper mapper = session.getMapper(UserMapper.class);
int count = 12000;
for (int i=2000; i< count; i++) {
User user = new User();
user.setUserName("a"+i);
mapper.insertUser(user);
}
session.commit();
session.close();
long end = System.currentTimeMillis();
System.out.println("循环批量插入"+count+"条,耗时:" + (end -start )+"毫秒");
}
}
在MyBatis里面是支持批量的操作的,包括批量的插入、更新、删除。我们可以直接传入一个List、
Set、Map或者数组,配合动态SQL的标签,MyBatis会自动帮我们生成语法正确的SQL语句。
改进
批量插入的语法是这样的,只要在values后面增加插入的值就可以了。
insert into tbl_emp (emp_id, emp_name, gender,email, d_id) values ( ?,?,?,?,? ), ( ?,?,?,?,? ),( ?,?,?,?,? )
在Mapper文件里面,我们使用foreach标签拼接 values部分的语句:
<!-- 批量插入
insert into t_user() values (),(),(),()
-->
<insert id="insertUserList" parameterType="java.util.List" >
insert into t_user(user_name,real_name)
values
<foreach collection="list" item="user" separator=",">
(#{user.userName},#{user.realName})
</foreach>
</insert>
@Test
public void test2() throws Exception{
init();
long start = System.currentTimeMillis();
UserMapper mapper = session.getMapper(UserMapper.class);
int count = 12000;
List<User> list = new ArrayList<>();
for (int i=2000; i< count; i++) {
User user = new User();
user.setUserName("a"+i);
list.add(user);
}
mapper.insertUserList(list);
session.commit();
session.close();
long end = System.currentTimeMillis();
System.out.println("循环批量插入"+count+"条,耗时:" + (end -start )+"毫秒");
}
可以看到,动态SQL批量插入效率要比循环发送SQL执行要高得多。最关键的地方就在于减少了跟
数据库交互的次数,并且避免了开启和结束事务的时间消耗。
批量删除
同理,在一次连接中解决
<delete id="deleteByList" parameterType="java.util.List">
delete from t_user where id in
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item.id,jdbcType=INTEGER}
</foreach>
</delete>
批量更新
批量更新的语法是这样的,通过case when,来匹配id相关的字段值
update t_user set user_name = case id when ? then ? when ? then ? when ? then ? end ,
real_name = case id when ? then ? when ? then ? when ? then ? end where id in ( ? , ? , ? )
所以在Mapper文件里面最关键的就是case when和where的配置。
需要注意一下open属性和separator属性。
<update id="updateUserList">
update t_user set
user_name =
<foreach collection="list" item="user" index="index" separator=" " open="case id" close="end">
when #{user.id} then #{user.userName}
</foreach>
,real_name =
<foreach collection="list" item="user" index="index" separator=" " open="case id" close="end">
when #{user.id} then #{user.realName}
</foreach>
where id in
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item.id,jdbcType=INTEGER}
</foreach>
</update>
@Test
public void test3() throws Exception{
init();
long start = System.currentTimeMillis();
UserMapper mapper = session.getMapper(UserMapper.class);
int count = 12000;
List<User> list = new ArrayList<>();
for (int i=2000; i< count; i++) {
User user = new User();
user.setId(i);
user.setUserName("a"+i);
list.add(user);
}
mapper.updateUserList(list);
session.commit();
session.close();
long end = System.currentTimeMillis();
System.out.println("批量更新"+count+"条,耗时:" + (end -start )+"毫秒");
}
BatchExecutor
当然MyBatis的动态标签的批量操作也是存在一定的缺点的,比如数据量特别大的时候,拼接出来的SQL语句过大。
MySQL的服务端对于接收的数据包有大小限制,max_allowed_packet 默认是 4M,需要修改默认配置或者手动地控制条数,才可以解决这个问题。
在我们的全局配置文件中,可以配置默认的Executor的类型(默认是SIMPLE)。其中有一种BatchExecutor。
<setting name="defaultExecutorType" value="BATCH" />
也可以在创建会话的时候指定执行器类型
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
Executor
- SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭
Statement对象。 - ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在
就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复
使用Statement对象。 - BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处
理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个
Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相
同。executeUpdate()是一个语句访问一次数据库,executeBatch()是一批语句访问一次数据库
(具体一批发送多少条SQL跟服务端的max_allowed_packet有关)。BatchExecutor底层是对
JDBC ps.addBatch()和ps. executeBatch()的封装。
关联查询
嵌套查询
我们在查询业务数据的时候经常会遇到关联查询的情况,比如查询员工就会关联部门(一对一),查询学生成绩就会关联课程(一对一),查询订单就会关联商品(一对多),等等。用户和部门的对应关系是1对1的关系。association针对一对一
<!--嵌套查询 1对1 1个用户对应一个部门-->
<resultMap id="nestedMap1" type="user">
<id property="id" column="id" jdbcType="INTEGER"/>
<result property="userName" column="user_name" jdbcType="VARCHAR" />
<result property="realName" column="real_name" jdbcType="VARCHAR" />
<result property="password" column="password" jdbcType="VARCHAR"/>
<result property="age" column="age" jdbcType="INTEGER"/>
<result property="dId" column="d_id" jdbcType="INTEGER"/>
<association property="dept" javaType="dept">
<id column="did" property="dId"/>
<result column="d_name" property="dName"/>
<result column="d_desc" property="dDesc"/>
</association>
</resultMap>
<select id="queryUserNested" resultMap="nestedMap1">
SELECT
t1.`id`
,t1.`user_name`
,t1.`real_name`
,t1.`password`
,t1.`age`
,t2.`did`
,t2.`d_name`
,t2.`d_desc`
FROM t_user t1
LEFT JOIN
t_department t2
ON t1.`d_id` = t2.`did`
</select>
测试结果
还有就是1对多的关联关系,嵌套查询,例如一个部门对应多个员工。collection是针对一对多的
<!-- 嵌套查询 1对多 1个部门有多个用户-->
<resultMap id="nestedMap2" type="dept">
<id column="did" property="dId"/>
<result column="d_name" property="dName"/>
<result column="d_desc" property="dDesc"/>
<collection property="users" ofType="user">
<id property="id" column="id" jdbcType="INTEGER"/>
<result property="userName" column="user_name" jdbcType="VARCHAR" />
<result property="realName" column="real_name" jdbcType="VARCHAR" />
<result property="password" column="password" jdbcType="VARCHAR"/>
<result property="age" column="age" jdbcType="INTEGER"/>
<result property="dId" column="d_id" jdbcType="INTEGER"/>
</collection>
</resultMap>
<select id="queryDeptNested" resultMap="nestedMap2">
SELECT
t1.`id`
,t1.`user_name`
,t1.`real_name`
,t1.`password`
,t1.`age`
,t2.`did`
,t2.`d_name`
,t2.`d_desc`
FROM t_user t1
RIGHT JOIN
t_department t2
ON t1.`d_id` = t2.`did`
</select>
延迟加载
全局配置见上面
1对1的延迟加载配置(就是association那里配置了一下懒加载使用的select,那里的column就是延迟第二次查询使用的查询条件)
<!--<!– 延迟加载 1对1 –>-->
<resultMap id="nestedMap1Lazy" type="user">
<id property="id" column="id" jdbcType="INTEGER"/>
<result property="userName" column="user_name" jdbcType="VARCHAR" />
<result property="realName" column="real_name" jdbcType="VARCHAR" />
<result property="password" column="password" jdbcType="VARCHAR"/>
<result property="age" column="age" jdbcType="INTEGER"/>
<result property="dId" column="d_id" jdbcType="INTEGER"/>
<association property="dept" javaType="dept" column="d_id" select="queryDeptByUserIdLazy">
</association>
</resultMap>
<select id="queryUserNestedLazy" resultMap="nestedMap1Lazy">
SELECT
t1.`id`
,t1.`user_name`
,t1.`real_name`
,t1.`password`
,t1.`age`
,t1.d_id
FROM t_user t1
</select>
<resultMap id="baseDept" type="dept">
<id column="did" property="dId"/>
<result column="d_name" property="dName"/>
<result column="d_desc" property="dDesc"/>
</resultMap>
<select id="queryDeptByUserIdLazy" parameterType="int" resultMap="baseDept">
select * from t_department where did = #{did}
</select>
注意:开启了延迟加载的开关,调用user.getDept()以及默认的(equals,clone,hashCode,toString)时才会发起第二次查询,其他方法并不会触发查询,比如blog.getName();
触发延迟加载的方法可以通过配置,默认
equals(),clone(),hashCode(),toString()。
@Test
public void test03() throws Exception{
init();
UserMapper mapper = session.getMapper(UserMapper.class);
List<User> users = mapper.queryUserNestedLazy();
for (User user : users) {
System.out.println(user.getUserName() );
System.out.println(user.getUserName() + "---->"+user.getDept());
}
}
可以看到你如果调用了user.getDept的方法,就说明你需要关联查询另一张表的信息了,由于前面association配置了懒加载,这里就会自动用deptId再去查询dept的信息
1对多的延迟加载
<!--<!– 1对多 延迟加载 –>-->
<resultMap id="nestedMap2Lazy" type="dept">
<id column="did" property="dId"/>
<result column="d_name" property="dName"/>
<result column="d_desc" property="dDesc"/>
<collection property="users" ofType="user" column="did" select="queryUserByDeptLazy">
</collection>
</resultMap>
<select id="queryDeptNestedLazy" resultMap="nestedMap2Lazy">
SELECT
t2.`did`
,t2.`d_name`
,t2.`d_desc`
FROM
t_department t2
</select>
<select id="queryUserByDeptLazy" resultMap="BaseResultMap" >
select * from t_user where d_id = #{dId}
</select>
@Test
public void test02() throws Exception{
init();
UserMapper mapper = session.getMapper(UserMapper.class);
//查询一个部门拥有的所有用户信息
List<Dept> depts = mapper.queryDeptNestedLazy();
for (Dept dept : depts) {
System.out.println(dept.getdId());
System.out.println(dept.getUsers());
}
}
同样可以看到,只有当你需要User信息时,即调用dept.getUsers方法的时候才会再去查询
分页操作
逻辑分页
MyBatis里面有一个逻辑分页对象RowBounds,里面主要有两个属性,offset和limit(从第几条开
始,查询多少条)。我们可以在Mapper接口的方法上加上这个参数,不需要修改xml里面的SQL语句。
接口中定义
public List<User> queryUserList(RowBounds rowBounds);
测试类
@Test
public void test01() throws Exception{
init();
UserMapper mapper = session.getMapper(UserMapper.class);
RowBounds rowBounds = new RowBounds(1,3);
List<User> users = mapper.queryUserList(rowBounds);
for (User user : users) {
System.out.println(user);
}
}
RowBounds的工作原理其实是对ResultSet的处理。它会舍弃掉前面offset条数据,然后再取剩下的
数据的limit条。
源码
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext();
ResultSet resultSet = rsw.getResultSet();
this.skipRows(resultSet, rowBounds);
while(this.shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
ResultMap discriminatedResultMap = this.resolveDiscriminatedResultMap(resultSet, resultMap, (String)null);
Object rowValue = this.getRowValue(rsw, discriminatedResultMap, (String)null);
this.storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
很明显,如果数据量大的话,这种翻页方式效率会很低(跟查询到内存中再使用subList(start,end)没什么区别)。所以我们要用到物理翻页。
物理分页
物理翻页是真正的翻页,它是通过数据库支持的语句来翻页。
第一种简单的办法就是传入参数(或者包装一个page对象),在SQL语句中翻页。
<select id="selectUserPage" parameterType="map" resultMap="BaseResultMap">
select * from t_user limit #{curIndex} , #{pageSize}
</select>
第一个问题是我们要在Java业务代码里面去计算起止序号;第二个问题是:每个需要翻页的Statement都要编写limit语句,会造成Mapper映射器里面很多代码冗余。
那我们就需要一种通用的方式,不需要去修改配置的任何一条SQL语句,我们只要传入当前是第几页,每页多少条就可以了,自动计算出来起止序号。
我们最常用的做法就是使用翻页的插件,比如PageHelper。
// pageSize每一页几条
PageHelper.startPage(pn, 10);
List<Employee> emps = employeeService.getAll();
// navigatePages 导航页码数
PageInfo page = new PageInfo(emps, 10);
return Msg.success().add("pageInfo", page);
PageHelper是通过MyBatis的拦截器实现的,插件的具体原理我们后面的课再分析。简单地来说,它会根据PageHelper的参数,改写我们的SQL语句。比如MySQL会生成limit语句,Oracle会生成rownum语句,SQL Server会生成top语句。