MyBatis 是一个开源的、基于 Java 的持久层框架,它简化了数据库操作的复杂性,同时又提供了灵活的 SQL 映射机制。
数据库中有一张 User 表,下面将使用 MyBatis 对其进行查询操作:
x1create database test01 charset utf8;2
3create table if not exists user(4 id int(11) unsigned not null auto_increment comment '用户编号'5 ,username varchar(32) not null comment '用户名称'6 ,birthday varchar(38) not null comment '用户生日'7 ,sex char(2) not null comment '性别,0男1女'8 ,address varchar(255) null comment '用户地址'9 ,primary key(id)10 )engine=innodb default charset=utf8 comment='用户表';11
12insert into user(username, birthday, sex, address) values('hyx', '1999-09-13', '0', '深圳市南山区');13commit;14
该表对应的实体类和 DAO 接口如下:
211// 实体类2package org.example.model;3import java.io.Serializable;4import java.sql.Date;56public class User implements Serializable {7 private Integer id;8 private String username;9 private Date birthday;10 private String sex;11 private String address;12}13
14// DAO 接口15package org.example.dao;16import org.example.model.User;17import java.util.List;18public interface UserDao {19 List<User> findAll();20}21
如果使用传统的 JDBC 或 JdbcTemplate ,那么下一步应该实现 DAO 接口,编写具体的 CURD 方法。
但是,使用 MyBatis 后,你无需自己手写这些繁琐的实现类,这些 MyBatis 都会使用动态代理技术帮你生成。
引入 MyBatis 相关的依赖如下:
361 2<project xmlns="http://maven.apache.org/POM/4.0.0"3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">5 <modelVersion>4.0.0</modelVersion>6
7 <groupId>org.example</groupId>8 <artifactId>demo01</artifactId>9 <version>1.0-SNAPSHOT</version>10
11 <dependencies>12 <!-- MyBatis 依赖-->13 <dependency>14 <groupId>org.mybatis</groupId>15 <artifactId>mybatis</artifactId>16 <version>3.4.5</version>17 </dependency>18 19 <!-- MySql JDBC 驱动-->20 <dependency>21 <groupId>mysql</groupId>22 <artifactId>mysql-connector-java</artifactId>23 <version>5.1.6</version>24 <scope>runtime</scope>25 </dependency>26
27 <!-- Junit 用于测试-->28 <dependency>29 <groupId>junit</groupId>30 <artifactId>junit</artifactId>31 <version>4.10</version>32 <scope>test</scope>33 </dependency>34 </dependencies>35
36</project>如果你使用的是Oracle或SqlServer数据库,应该切换如下驱动:
121<dependency>2 <groupId>com.microsoft.sqlserver</groupId>3 <artifactId>sqljdbc4</artifactId>4 <version>4.0</version>5</dependency>6
7<dependency>8 <groupId>com.oracle</groupId>9 <artifactId>ojdbc14</artifactId>10 <version>10.2.0.5.0</version>11</dependency>12
在类路径下新建src\main\resources\mybatis-config.xml配置文件,用来配置 MyBatis 的相关属性和行为:
241 23 4 5<configuration>6 <!-- 环境配置 -->7 <environments default="dev">8 <environment id="dev">9 <transactionManager type="JDBC"></transactionManager>10 <!-- 数据源配置 -->11 <dataSource type="POOLED">12 <property name="driver" value="com.mysql.jdbc.Driver"/>13 <property name="url" value="jdbc:mysql://42.192.223.129:3306/test01"/>14 <property name="username" value="root"/>15 <property name="password" value="root"/>16 </dataSource>17 </environment>18 </environments>19
20 <!-- 映射文件扫描 -->21 <mappers>22 <mapper resource="org/example/dao/UserDao.xml"/>23 </mappers>24</configuration>如果你使用的是Oracle或SqlServer数据库,JDBC连接四要素应修改如下:
91 <property name="driver" value="oracle.jdbc.driver.OracleDriver"/>2 <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:ORCLONE"/>3 <property name="username" value="kbssfms"/>4 <property name="password" value="kbssfms"/>5
6 <property name ="driverClassName" value ="com.microsoft.sqlserver.jdbc.SQLServerDriver"></property>7 <property name ="url" value ="jdbc:sqlserver://127.0.0.1:1433;DatabaseName=kbssfms"></property>8 <property name ="username" value ="sa"></property>9 <property name ="password" value ="Sa147741"></property>
在类路径下新建src\main\resources\org\example\dao\UserDao.xml配置文件,用于配置执行的SQL语句和结果集映射方式。
111 23 4 5<mapper namespace="org.example.dao.UserDao">6
7 <!-- 查询所有用户 -->8 <select id="findAll" resultType="org.example.model.User">9 SELECT * FROM user10 </select>11</mapper>
编写一个简单的测试类进行测试:
461package org.example.dao.test;2
3import org.apache.ibatis.io.Resources;4import org.apache.ibatis.session.SqlSession;5import org.apache.ibatis.session.SqlSessionFactory;6import org.apache.ibatis.session.SqlSessionFactoryBuilder;7import org.example.dao.UserDao;8import org.example.model.User;9
10import java.io.InputStream;11import java.util.List;12
13public class MybatisTest {14
15 public static void main(String[] args) throws Exception {16 //1.读取主配置文件17 InputStream in = Resources.getResourceAsStream("mybatis-config.xml");18
19 //2.创建 SqlSessionFactoryBuilder20 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();21
22 //3. 构建 SqlSessionFactory23 SqlSessionFactory factory = builder.build(in);24
25 //4. 打开会话26 SqlSession session = factory.openSession();27
28 //5. 获取代理对象29 UserDao userDao = session.getMapper(UserDao.class);30
31 //6. 使用代理对象执行查询所有方法32 List<User> users = userDao.findAll();33 for (User user : users) {34 System.out.println(user);35 }36
37 //7. 释放资源38 if (session != null) {39 session.close();40 }41 if (in != null) {42 in.close();43 }44
45 }46}运行结果如下:

注意:
上述案例的完整工程代码可参考附件 mybatis-demo/demo01 工程!
SqlSessionFactoryBuilder使用了构建者模式,用于解析配置文件和构建SqlSessionFactory,支持的构建方法如下:
81// 方式1:基于配置文件构建(mybatis-config.xml及引用的 Mapper 配置)2SqlSessionFactory build(InputStream inputStream)3SqlSessionFactory build(InputStream inputStream, String environment)4SqlSessionFactory build(InputStream inputStream, Properties properties)5SqlSessionFactory build(InputStream inputStream, String env, Properties props)6
7// 方式2:基于Java配置构建8SqlSessionFactory build(Configuration config)方式1已经在入门案例中介绍,方式2基于Java配置构建方式示例如下:
181// 获取数据源2DataSource dataSource = BaseDataTest.createBlogDataSource();3
4// 创建一个环境(使用JDBC事务管理器)5Environment environment = new Environment("development", new JdbcTransactionFactory(), dataSource);6
7// 创建一个配置8Configuration configuration = new Configuration(environment);9configuration.setLazyLoadingEnabled(true);10configuration.setEnhancementEnabled(true);11configuration.getTypeAliasRegistry().registerAlias(Blog.class);12configuration.getTypeAliasRegistry().registerAlias(Post.class);13configuration.getTypeAliasRegistry().registerAlias(Author.class);14configuration.addMapper(BoundBlogMapper.class);15configuration.addMapper(BoundAuthorMapper.class);16
17// 构建SqlSessionFactory18SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(configuration);
SqlSessionFactory使用了工厂模式,用于管理配置信息和创建SqlSession,支持的创建方法如下:
191// 默认创建方法2// 事务不自动提交、使用默认事务隔离级别、使用SIMPLE执行器、从当前环境配置的DataSource 实例中获取 Connection 对象3SqlSession openSession() 4
5// 支持设置是否自动提交、事务隔离级别、连接等6SqlSession openSession(boolean autoCommit)7SqlSession openSession(Connection connection)8SqlSession openSession(TransactionIsolationLevel level) 9
10// 支持选择不同的执行器11// SIMPLE:每个语句的执行创建一个新的预处理语句12// REUSE: 复用预处理语句13// BATCH: 批量执行更新语句14SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level)15SqlSession openSession(ExecutorType execType)16SqlSession openSession(ExecutorType execType, boolean autoCommit)17SqlSession openSession(ExecutorType execType, Connection connection)18Configuration getConfiguration();19
SqlSession是 MyBatis 的核心组件之一,用于执行 SQL 语句、管理事务以及获取映射器对象(Mapper)等,主要方法如下:
481// 1. 语句执行方法2<T> T selectOne(String statement, Object parameter)3<E> List<E> selectList(String statement, Object parameter)4<T> Cursor<T> selectCursor(String statement, Object parameter) // 游标形式:可以借助迭代器实现数据的惰性加载5<K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey)6int insert(String statement, Object parameter) // insert、update 以及 delete 方法返回的值表示受该语句影响的行数7int update(String statement, Object parameter)8int delete(String statement, Object parameter)9
10// 2. 语句执行方法(不需要参数的重载形式)11<T> T selectOne(String statement)12<E> List<E> selectList(String statement)13<T> Cursor<T> selectCursor(String statement)14<K,V> Map<K,V> selectMap(String statement, String mapKey)15int insert(String statement)16int update(String statement)17int delete(String statement)18
19// 3. select方法的高级版本:允许你限制返回行数的范围,或是提供自定义结果处理逻辑,通常在数据集非常庞大的情形下使用。20<E> List<E> selectList (String statement, Object parameter, RowBounds rowBounds)21<T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds)22<K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowbounds)23void select (String statement, Object parameter, ResultHandler<T> handler)24void select (String statement, Object parameter, RowBounds rowBounds, ResultHandler<T> handler)25
26// 4. 批量更新方法:当使用 BATCH 执行器时,可以使用这个方法清除(执行)缓存在 JDBC 驱动类中的批量更新语句。27List<BatchResult> flushStatements()28
29// 5. 事务控制方法:在使用由 Connection 实例控制的 JDBC 事务管理器时,用于控制事务作用域。30// 注意:这些方法不适用于外部事务管理器。31void commit()32void commit(boolean force)33void rollback()34void rollback(boolean force)35
36// 6. 清空本地缓存(会话缓存)37void clearCache()38
39// 7. 关闭会话40void close()41 42// 8. 获取 Configuration 实例43Configuration getConfiguration()44 45// 9. 获取映射器46// 映射器相对上述CURD方法,最大的优点是符合类型安全,并且对IDE和单元测试也更加友好。47<T> T getMapper(Class<T> type)48
映射器通过SqlSession的getMapper方法获取,本质上是一个 DAO 接口的动态代理类,内部保存了当前SqlSession对象的引用,主要用于执行SQL语句,并通过返回值获得执行结果。
注意:
用于创建映射器的 DAO 接口,不能出现方法重载,否则会出现命名空间冲突。
映射器方法的参数名默认按位置命名为
param1、param2...等,可以使用@Param注解进行修改。
MyBatis支持在映射器上通过注解进行映射配置,示例如下:
41public interface UserDao {2 ("SELECT * FROM USER")3 List<User> findAll();4}此时需修改核心配置mybatis-config.xml,通过注解进行扫描:
41<!-- 通过注解进行扫描 -->2<mappers>3 <mapper class="org.example.dao.UserDao"/>4</mappers>注意
注意删除XML中该方法对应的映射配置,同一个映射器方法,不能同时存在两种配置方式。
MyBatis按照SLF4J -> COMMONS_LOGGING -> LOG4J2 -> LOG4J -> JDK_LOGGING的顺序查找日志实现,可在配置文件中进行修改:
61<!-- mybatis-config.xml -->2<configuration>3 <settings>4 <setting name="logImpl" value="LOG4J"/>5 </settings>6</configuration>此外,还可以配置为STDOUT_LOGGING和NO_LOGGING,或者其它实现了 org.apache.ibatis.logging.Log 接口的日志适配器。
MyBatis-Spring是一个适配模块,可以将 MyBatis 无缝集成到 Spring 框架中,它可以:
从容器中查找数据源创建SqlSessionFactory。
创建线程安全的SqlSession或Mapper,并将其注册到容器中,支持直接注入使用。
使用 Mapper 接口执行SQL语句时,从Spring上下文获取SqlSession,从而参与Spring事务管理。
将 MyBatis 抛出的持久化异常PersistenceException统一转换为 Spring 的数据访问异常DataAccessException。
引入相关依赖如下:
51<dependency>2 <groupId>org.mybatis</groupId>3 <artifactId>mybatis-spring</artifactId>4 <version>2.0.7</version>5</dependency>
Spring需要一个SqlSessionFactory来保存配置信息和创建SqlSession,可以通过SqlSessionFactoryBean来创建并注册到Spring容器。
51<!-- 创建SqlSessionFactory -->2<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">3 <!-- 需要一个dataSource -->4 <property name="dataSource" ref="dataSource" />5</bean>等效的 Java 配置如下:
912public class MyBatisConfig {3 4 public SqlSessionFactory sqlSessionFactory() throws Exception {5 SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();6 factoryBean.setDataSource(dataSource());7 return factoryBean.getObject();8 }9}注意:
可以直接通过依赖注入或继承 SqlSessionDaoSupport 的方式来获得容器中的 SqlSessionFactory 实例。
不建议直接调用该实例的
openSession()方法获取SQL会话,因为通过该会话执行SQL时,不会参与到Spring管理的事务中,并且需要手动关闭该会话。
SqlSessionTemplate是SqlSession的实现类,与其它 Spring 模板类似,它将会话数据保存在线程变量中,是线程安全的。
可以通过如下配置来创建SqlSessionTemplate,以代替 MyBatis 原生的DefaultSqlSession,将其注入到新创建的 Mapper 中,从而让 Spring 介入到 MyBatis 的会话管理,控制事务的提交和异常转换等相关操作。
51<!-- 创建SqlSession -->2<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">3 <!-- 需要一个sqlSessionFactory -->4 <constructor-arg index="0" ref="sqlSessionFactory" />5</bean>等效的Java配置如下:
712public class MyBatisConfig {3 4 public SqlSessionTemplate sqlSession() throws Exception {5 return new SqlSessionTemplate(sqlSessionFactory());6 }7}SqlSessionTemplate创建完毕并注册到Spring容器后,可以通过注入的方式,来使用它,如下所示。
1012public class UserDaoImpl implements UserDao {3 4 private SqlSession sqlSession;5
6 public User getUser(String userId) {7 return sqlSession.selectOne("org.mybatis.spring.sample.mapper.UserMapper.getUser", userId);8 }9}10
注意:
SqlSessionTemplate 是通过实现+组合方式对 DefaultSqlSession 的装饰,通过它执行SQL语句时,会获取 Spring 的事务管理器创建或获取当前线程会话,从而参与到Spring管理的事务中。
请勿直接调用 SqlSessionTemplate 的 commit()、rollback()、close() 方法,这将会抛出 UnsupportedOperationException 异常。
SqlSessionTemplate是线程安全的,但是数据源等配置信息是各线程共享的,如果存在多个数据源,则需要创建多个实例。
一般来说,实际开发中,都会使用 MyBatis 的映射器来访问数据库,可以通过MapperFactoryBean来创建并注册到Spring容器。
71<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">2 <!--为UserMapper接口创建线程安全的映射器-->3 <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" />4 <!--通过sqlSessionFactory传入配置信息-->5 <property name="sqlSessionFactory" ref="sqlSessionFactory" />6</bean>7
等效的java配置如下:
912public class MyBatisConfig {3 4 public UserMapper userMapper() throws Exception {5 SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory());6 return sqlSessionTemplate.getMapper(UserMapper.class);7 }8}9
然后就可以将映射器注入到你的业务对象中进行使用了。
912public class FooServiceImpl implements FooService {3 4 private final UserMapper userMapper;5
6 public User doSomeBusinessStuff(String userId) {7 return this.userMapper.getUser(userId);8 }9}提示:
你也可以为 MapperFactoryBean 传入一个 SqlSessionTemplate,这样将会使用其内部的 SqlSessionFactory,而忽略传入的那个。
在 MyBatis-Spring 整合工程中,一般通过 SqlSessionFactoryBean 来创建 SqlSessionFactory 并注册到 Spring 容器,以便 MapperFactoryBean或继承 SqlSessionDaoSupport 的类注入使用,其主要配置说明如下:
configLocation 用来指定MyBatis配置文件路径。但这个配置文件并不是一个完整的MyBatis配置,它会忽略配置中的<environments>、<dataSource>和<transactionManager>等信息,但仍会解析<settings>和<typeAliases>等配置。
61<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">2 <property name="dataSource" ref="dataSource"/>3 4 <!-- 引用mybatis配置文件中的配置 -->5 <property name="configLocation" value="classpath:mybatis-config.xml"/>6</bean>
mapperLocations用来指定映射配置文件路径。很多时候,映射配置文件和映射器类不在同一资源路径下,这时可以通过该属性来指定,并且可以同时指定多个路径和递归搜索。
51<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">2 <property name="dataSource" ref="dataSource" />3 <!-- 加载在sample.config.mappers包和子包中的映射器配置文件 -->4 <property name="mapperLocations" value="classpath*:sample/config/mappers/**/*.xml" />5</bean>提示:
你也可以通过配置上面的 configLocation 属性,指定 MyBatis 配置文件,在其
<mappers>标签内指定映射器配置文件。
如果你使用了多种数据库,那么需要设置 databaseIdProvider 属性。
201<!-- 配置数据库厂商的databaseId -->2<bean id="databaseIdProvider" class="org.apache.ibatis.mapping.VendorDatabaseIdProvider">3 <property name="properties">4 <props>5 <prop key="SQL Server">mssql</prop>6 <prop key="DB2">db2</prop>7 <prop key="Oracle">oracle</prop>8 <prop key="MySQL">mysql</prop>9 </props>10 </property>11</bean>12
13<!-- 创建SqlSessionFactory -->14<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">15 <property name="dataSource" ref="dataSource" />16 17 <!--指定数据库厂商ID配置-->18 <property name="databaseIdProvider" ref="databaseIdProvider"/>19</bean>20
configuration属性能够在没有对应的 MyBatis 配置文件的情况下,直接设置 Configuration 实例。例如:
81<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">2 <property name="dataSource" ref="dataSource" />3 <property name="configuration">4 <bean class="org.apache.ibatis.session.Configuration">5 <property name="mapUnderscoreToCamelCase" value="true"/>6 </bean>7 </property>8</bean>
transactionFactoryClass属性用于自定义事务工厂,一般不做配置。
MyBatis-Spring 工程提供的 SqlSessionTemplate,将会替代原 MyBatis 的 DefaultSqlSession 注入到映射器中,并且在注入前经过动态代理,在调用其方法时被拦截,用于控制事务和转换异常等操作。下面是创建时可以使用的一些配置参数:
ExecutorType 用于指定 SqlSessionTemplate 的执行器类型,如下配置创建一个用于批处理的SqlSessionTemplate。
71<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">2 <constructor-arg index="0" ref="sqlSessionFactory" />3 4 <!--指定执行器类型为批处理执行器(编译1次,设置参数N次,执行1次)-->5 <constructor-arg index="1" value="BATCH" />6</bean>7
等效的java配置如下:
712public class MyBatisConfig {3 4 public SqlSessionTemplate sqlSession() throws Exception {5 return new SqlSessionTemplate(sqlSessionFactory(), ExecutorType.BATCH);6 }7}现在所有的映射语句可以进行批量操作了,可以在 DAO 中编写如下的代码。
1212public class UserService {3 4 private final SqlSession sqlSession;5
6 public void insertUsers(List<User> users) {7 for (User user : users) {8 sqlSession.insert("org.mybatis.spring.sample.mapper.UserMapper.insertUser", user);9 }10 }11}12
注意:
同一个事务只能使用一种执行器,如果你需要不同的执行器执行SQL,请拆分为多个事务或完全不使用事务。
SqlSessionDaoSupport 是一个抽象的支持类,用来为你提供 SqlSession。业务类继承它后,调用 getSqlSession() 方法就会得到一个 SqlSessionTemplate,之后可以用于执行 SQL 方法,并参与 Spring 事务管理。
51public class UserDaoImpl extends SqlSessionDaoSupport implements UserDao {2 public User getUser(String userId) {3 return getSqlSession().selectOne("org.mybatis.spring.sample.mapper.UserMapper.getUser", userId);4 }5}前提是你为业务对象注入了 SqlSessionFactory 或 SqlSessionTemplate,如果两个属性都被注入,那么SqlSessionFactory 将会被忽略。
31<bean id="userDao" class="org.mybatis.spring.sample.dao.UserDaoImpl">2 <property name="sqlSessionFactory" ref="sqlSessionFactory" />3</bean>
<mybatis:scan>标签与组件扫描所使用的<context:component-scan/>标签类似,通过base-package属性指定多个以逗号分割的包名,递归扫描发现的映射器,然后创建对应的 MapperFactoryBean 并注册到Spring中。
111<beans xmlns="http://www.springframework.org/schema/beans"2 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"3 xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"4 xsi:schemaLocation="5 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd6 http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">7
8 <!-- 扫描 org.example.dao 及其子包下的映射器 -->9 <mybatis:scan base-package="org.example.dao" factory-ref="sqlSessionFactory"/>10
11</beans>下面是<mybatis:scan>标签的一些配置参数:
factory-ref/template-ref:指定创建映射器时使用的 SqlSessionFactory 或 SqlSessionTemplate的Bean名称。
一般只在有多数据源时使用,因为默认情况下MapperFactoryBean会被注入默认的 SqlSessionFactory 或 SqlSessionTemplate。
annotation: 指定映射器应该具有的注解(如@Repository),如果扫描到的接口未包含该注解,则跳过解析。
marker-interface:指定映射器应该继承的接口,如果扫描到的接口未继承该接口,则跳过解析。
默认情况下,注册的映射器都以映射器接口的首字母小写非全限定类名作为名称,你可以通过 @Component 或 JSR-330 标准中 @Named 注解来进行修改。
注意:
<context:component-scan/>标签无法发现并注册映射器。因为映射器是一个接口,组建扫描时无法实例化。映射器接口的Bean名称默认为首字母小写类名,可通过 @Component 或 @Named 注解修改。
@MapperScan注解也可以用于批量创建Mapper,它会自动扫描basePackageClasses 或 basePackages包及其子包的映射器。
512("org.mybatis.spring.sample.mapper")3public class AppConfig {4 // ...5}注意:
如果未定义
basePackageClasses或basePackages,则会从声明@MapperScan注解类的包中进行扫描。同样的,@MapperScan 也可配置 sqlSessionFactory、sqlSessionTemplate、markerInterface、annotationClass 等属性。
MapperScannerConfigurer 是一个 BeanDefinitionRegistryPostProcessor,可以在容器创建的时候扫描映射器。
71<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">2 <property name="basePackage" value="org.mybatis.spring.sample.mapper" />3 4 <!-- 可指定 SqlSessionFactory 的BeanName -->5 <!-- 可指定 SqlSessionFactory 的BeanName -->6 <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />7</bean>如果有多个数据源,可以使用sqlSessionFactoryBeanName 或 sqlSessionTemplateBeanName 属性指定 sqlSessionFactory 或 sqlSessionTemplate 的 bean 名称(注意使用 vlaue 指定BenaName,而不是 ref 引用对象)。
为了支持 SpringBoot 2.2 的延迟初始化控制功能,新增了lazyInitialization属性,用于控制映射器的延迟初始化,默认为false。如果使用延迟初始化功能,开发人员需要了解以下限制,否则延迟初始化功能无法在您的应用程序上使用。
不能使用 <association>(@One) 和 <collection>(@Many) 引用其他映射器的语句
不能使用<include> 包含到其他映射器的片段
不能使用 <cache-ref>(@CacheNamespaceRef) 引用其他映射器的缓存
不能使用 <select resultMap="...">(@ResultMap) 引用其他映射器的结果映射
但是,你可以通过使用 @DependsOn 同时初始化依赖 bean 来使用它,如下所示:
41("vendorMapper")2public GoodsMapper {3 // ...4}
MyBatis 与 Spring 整合后,使用同一个事务管理器(默认为DataSourceTransactionManage),在事务开启时,会创建一个全新的 DefaultSqlSession ,当事务完成后,这个 session 会以合适的方式提交或回滚。
MyBatis使用与 JDBC 相同的事务管理器 DataSourceTransactionManager,并且配置方式一致,详情可以参考 Spring 事务相关讲解。
51<!-- 配置一个事务管理器 -->2<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">3 <!-- 需要一个数据源 -->4 <constructor-arg ref="dataSource" />5</bean>等效的java配置如下:
712public class DataSourceConfig {3 4 public DataSourceTransactionManager transactionManager() {5 return new DataSourceTransactionManager(dataSource());6 }7}注意:
为事务管理器指定的 DataSource 必须和用来创建 SqlSessionFactoryBean 的是同一个数据源,否则事务管理器就无法工作了。
你可以使用 @Transaction 注解或 AOP 方式来配置哪些方法应该进行事务控制,这和 Spring 事务章节所讲述的方式相同。也可以使用Spring 提供的编程式API来精细控制事务,如下案例所示。
1912public class UserService {3 4 private final PlatformTransactionManager transactionManager;5
6 public void createUser() {7 // 获取当前活动事务或创建新事物(由TransactionDefinition决定事务传播行为)8 TransactionStatus txStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());9 try {10 userMapper.insertUser(user);11 } catch (Exception e) {12 // 出异常回滚13 transactionManager.rollback(txStatus);14 throw e;15 }16 // 业务成功,提交事务17 transactionManager.commit(txStatus);18 }19}
MyBatis-Spring-Boot-Starter用于快速构建基于SpringBoot 和 Mybatis 的应用程序。
首先从官方文档查找合适的依赖版本。

如果你使用 Maven来管理项目,则可以通过下面配置来引入依赖。
51<dependency>2 <groupId>org.mybatis.spring.boot</groupId>3 <artifactId>mybatis-spring-boot-starter</artifactId>4 <version>2.2.2</version>5</dependency>
当容器中只有一个DataSource时(或存在主候选对象),MyBatis-Spring-Boot-Starter 会进行自动配置。自动配置包括以下一些内容:
使用默认 DataSource 配置SqlSessionFactory,使用该 SqlSessionFactory 配置SqlSessionTemplate。
配置MapperScannerConfigurer,用于批量创建Mapper。
配置Interceptor、TypeHandler、DatabaseIdProvider、LanguageDriver。
只需要将映射器接口加上@Mapper注解即可,容器启动时会自动扫描类路径下带@Mapper注解的映射器并注册到容器中。
512public interface CityMapper {3 ("SELECT * FROM CITY WHERE state = #{state}")4 City findByState(("state") String state);5}然后就可以通过注入的方式使用该映射器了。
1812public class SampleMybatisApplication implements CommandLineRunner {3 private final CityMapper cityMapper;4
5 public SampleMybatisApplication(CityMapper cityMapper) {6 this.cityMapper = cityMapper;7 }8
9 public static void main(String[] args) {10 SpringApplication.run(SampleMybatisApplication.class, args);11 }12
13 14 public void run(String... args) throws Exception {15 System.out.println(this.cityMapper.findByState("CA"));16 }17}18
同样,也可以通过注入的方式使用已配置好的SqlSessionFactory和SqlSessionTemplate。
1212public class CityDao {3 private final SqlSession sqlSession;4
5 public CityDao(SqlSession sqlSession) {6 this.sqlSession = sqlSession;7 }8
9 public City selectCityById(long id) {10 return this.sqlSession.selectOne("selectCityById", id);11 }12}注意:
如果需要修改扫描路径或扫描指定的注解/接口,可以结合 MyBatis-Spring 提供的
@MapperScan注解。
MyBatis-Spring-Boot-Starter还会检测容器中存在的Interceptor、TypeHandler、DatabaseIdProvider、LanguageDriver接口实现类,并将它们添加到 Configuration 中。
601// 添加 Interceptor、TypeHandler、DatabaseIdProvider23public class MyBatisConfig {4 5 MyInterceptor myInterceptor() {6 return MyInterceptor();7 }8 9 MyTypeHandler myTypeHandler() {10 return MyTypeHandler();11 }12 13 MyLanguageDriver myLanguageDriver() {14 return MyLanguageDriver();15 }16 17 VendorDatabaseIdProvider databaseIdProvider() {18 VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();19 Properties properties = new Properties();20 properties.put("SQL Server", "sqlserver");21 properties.put("DB2", "db2");22 properties.put("H2", "h2");23 databaseIdProvider.setProperties(properties);24 return databaseIdProvider;25 } 26}27
28// 添加 ThymeleafLanguageDriver2930public class MyBatisConfig {31 32 ThymeleafLanguageDriverConfig thymeleafLanguageDriverConfig() {33 return ThymeleafLanguageDriverConfig.newInstance(c -> {34 // ... customization code35 });36 }37}38
39// 添加 FreeMarkerLanguageDriverConfig4041public class MyBatisConfig {42 43 FreeMarkerLanguageDriverConfig freeMarkerLanguageDriverConfig() {44 return FreeMarkerLanguageDriverConfig.newInstance(c -> {45 // ... customization code46 });47 }48}49
50// 添加 Velocity语言驱动程序5152public class MyBatisConfig {53 54 VelocityLanguageDriverConfig velocityLanguageDriverConfig() {55 return VelocityLanguageDriverConfig.newInstance(c -> {56 // ... customization code57 });58 }59}60
MyBatis-Spring-Boot-Starter提供的SpringBootVFS继承自MyBatis框架的VFS(虚拟文件系统),用于从应用程序或应用服务器加载指定的类,如别名对应的类、类型处理器等。
在自动配置 SqlSessionFactory 时,将会默认使用,但在自定义 SqlSessionFactory 时,需要通过下面示例代码显式调用。
1112public class MyBatisConfig {3 4 public SqlSessionFactory masterSqlSessionFactory() throws Exception {5 SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();6 factoryBean.setDataSource(masterDataSource());7 factoryBean.setVfs(SpringBootVFS.class); 8 // ...9 return factoryBean.getObject();10 }11}
使用mybatis前缀来配置MyBatis的相关属性,部分属性说明如下,完整的属性列表及说明可参考MybatisProperties。
| 属性 | 说明 |
|---|---|
config-location | mybatis.xml配置文件的位置 |
mapper-locations | 映射器xml配置文件的位置 |
type-aliases-package | 需要注册类型别名的包,可用,、;、\t、\n做分隔 |
| check-config-location | 是否对mybatis.xml配置文件执行状态检查 |
| type-aliases-super-type | 注册别名时,仅当实现了该类时才注册 |
| type-handlers-package | 类型处理器所在包,用,、;、\t、'\n'做分隔 |
| executor-type | 默认的执行器类型,可选SIMPLE``REUSE``BATCH |
| default-scripting-language-driver | 默认脚本语言驱动程序类(mybatis-spring 2.0.2+) |
| configuration-properties | MyBatis配置的外部化属性,指定的属性可以用作MyBatis配置文件和映射器文件的占位符 |
| lazy-initialization | 是否启用映射器bean的延迟初始化(mybatis-spring 2.0.2+) |
| mapper-default-scope | 通过自动配置扫描的映射器Bean的默认范围(mybatis-spring 2.0.6+) |
| mybatis.inject-sql-session-on-mapper-scan | 是否注入SqlSessionTemplate,2.2.1+版本默认为true(否则注入SqlSessionFactory) |
| configuration.* | mybatis.xml的setting属性,注意不可与config-location属性一起使用 |
| scripting-language-driver.thymeleaf.* | Thymeleaf语言驱动特定属性 |
| scripting-language-driver.freemarker.* | FreeMarker语言驱动特定属性 |
| scripting-language-driver.velocity.* | Velocit语言驱动特定属性 |
下面是一个简单的使用案例。
81## application.yml2mybatis3 type-aliases-packagecom.example.domain.model4 type-handlers-packagecom.example.typehandler5 configuration6 map-underscore-to-camel-casetrue7 default-fetch-size1008 default-statement-timeout30
实现ConfigurationCustomizer接口可对 Configuration 进行全面的 Java 配置。
1312public class MyBatisConfig {3 4 ConfigurationCustomizer mybatisConfigurationCustomizer() {5 return new ConfigurationCustomizer() {6 7 public void customize(Configuration configuration) {8 // customize ...9 }10 };11 }12}13
实现SqlSessionFactoryBeanCustomizer接口可对SqlSessionFactoryBean进行全面的Java配置(2.2.2+版本)。
1312public class MyBatisConfig {3 4 SqlSessionFactoryBeanCustomizer sqlSessionFactoryBeanCustomizer() {5 return new SqlSessionFactoryBeanCustomizer() {6 7 public void customize(SqlSessionFactoryBean factoryBean) {8 // customize ...9 }10 };11 }12}13
通过 properties 标签定义属性名和属性值:
151<!-- 方式1:内部标签 -->2<!-- 方式2:通过 resource 属性引入类路径下的 properties 文件 -->3<!-- 方式3:通过 url 属性引入网络或本地 properties 文件 -->4<properties resource="org/mybatis/example/config.properties">5 <!-- 内部标签定义属性 -->6 <property name="username" value="dev_user"/>7 <property name="password" value="F2Fa3!33TYyg"/>8
9 <!-- 开启属性默认值特性 -->10 <property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/>11 <!-- 设置默认值分隔符(默认为冒号) -->12 <property name="org.apache.ibatis.parsing.PropertyParser.default-value-separator" value=":"/>13</properties>14
15
注意:
不同位置配置的属性优先级如下:直接通过 build() 方法传入的属性 > 引入文件中配置的属性 > 内部property标签定义的属性。
在 mybatis-config.xml 中使用属性:
61<dataSource type="POOLED">2 <property name="driver" value="${driver}"/>3 <property name="url" value="${url}"/>4 <property name="username" value="${username:root}"/> <!-- 属性默认值 -->5 <property name="password" value="${password}"/>6</dataSource>注意:
使用属性时可以设置默认值,但需要启用
enable-default-value开关
通过settings标签可设置各项参数,以调整 MyBatis 的默认行为:
1041<settings>2 <!-- 全局缓存开关 -->3 <setting name="cacheEnabled" value="true"/>4
5 <!-- 全局延迟加载开关,可通过 fetchType 属性覆盖 -->6 <setting name="lazyLoadingEnabled" value="false"/>7 8 <!-- 任一方法的调用都会加载该对象的所有延迟加载属性? -->9 <setting name="aggressiveLazyLoading" value="false"/>10 11 <!-- 允许单个语句返回多结果集(需要数据库驱动支持) -->12 <setting name="multipleResultSetsEnabled" value="true"/>13 14 <!-- 使用列标签代替列名 -->15 <setting name="useColumnLabel" value="true"/>16 17 <!-- 使用 JDBC 自动生成的主键(需要数据库驱动支持) -->18 <setting name="useGeneratedKeys" value="false"/>19 20 <!-- 自动映射行为 -->21 <!-- NONE:关闭自动映射 -->22 <!-- PARTIAL:只会自动映射没有定义嵌套结果映射的字段 -->23 <!-- FULL:会自动映射任何复杂的结果集(无论是否嵌套) -->24 <setting name="autoMappingBehavior" value="PARTIAL"/>25 26 <!-- 自动映射未知列行为 -->27 <!-- NONE:不做任何反应 -->28 <!-- WARNING:输出警告日志 -->29 <!-- FAILING:映射失败,抛出 SqlSessionException -->30 <setting name="autoMappingUnknownColumnBehavior" value="NONE"/>31 32 <!-- 默认执行器类型 -->33 <!-- SIMPLE:普通的执行器 -->34 <!-- REUSE:重用预处理语句(PreparedStatement) -->35 <!-- BATCH:不仅重用语句还会执行批量更新 -->36 <setting name="defaultExecutorType" value="SIMPLE"/>37 38 <!-- 语句执行超时时间 -->39 <setting name="defaultStatementTimeout" value="25"/>40 41 <!-- 结果集提取批次数(fetchSize) -->42 <setting name="defaultFetchSize" value="100"/>43 44 <!-- 结果集滚动策略,可选:DEFAULT(未设置)、FORWARD_ONLY、SCROLL_SENSITIVE、SCROLL_INSENSITIVE -->45 <setting name="defaultResultSetType" value="DEFAULT"/>46 47 <!-- “禁止”在嵌套语句中使用分页(RowBounds) -->48 <setting name="safeRowBoundsEnabled" value="false"/>49 50 <!-- “禁止”在嵌套语句中使用结果处理器(ResultHandler) -->51 <setting name="safeResultHandlerEnabled" value="false"/>52 53 <!-- 下划线列名映射小驼峰属性名 -->54 <setting name="mapUnderscoreToCamelCase" value="false"/>55 56 <!-- 本地缓存(一级缓存)范围,可选:SESSION、STATEMENT -->57 <!-- 注意:本地缓存用于防止循环引用和加速重复的嵌套查询 -->58 <setting name="localCacheScope" value="SESSION"/>59 60 <!-- 当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型,可选:NULL、VARCHAR、OTHER -->61 <setting name="jdbcTypeForNull" value="OTHER"/>62 63 <!-- 指定对象的哪些方法触发一次延迟加载 -->64 <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>65 66 <!-- 指定动态 SQL 生成使用的默认脚本语言 -->67 <setting name="defaultScriptingLanguage" value="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver"/>68 69 <!-- 指定 Enum 使用的默认 TypeHandler -->70 <setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumTypeHandler"/>71 72 <!-- 当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法 -->73 <setting name="callSettersOnNulls" value="false"/>74 75 <!-- 当返回行的所有列都是空时,是否返回一个空实例,默认是返回 null -->76 <setting name="returnInstanceForEmptyRow" value="false"/> 77 78 <!-- MyBatis 增加到日志名称的前缀 -->79 <setting name="logPrefix" value="mylog_"/> 80 81 <!-- MyBatis 所用日志的具体实现,未指定时将自动查找 -->82 <!-- 可选:SLF4J 、 LOG4J 、 LOG4J2 、 JDK_LOGGING 、 COMMONS_LOGGING 、 STDOUT_LOGGING 、 NO_LOGGING -->83 <setting name="logImpl" value="SLF4J"/> 84 85 <!-- Mybatis 创建可延迟加载对象所用到的代理工具,可选:CGLIB、JAVASSIST -->86 <setting name="proxyFactory" value="JAVASSIST"/> 87 88 <!-- 指定 VFS 的实现 -->89 <setting name="vfsImpl" value="com.huangyuanxin.mybatis.HyxVfs"/>90 91 <!-- 允许使用方法签名中的名称作为语句参数名称 -->92 <!-- 注意:项目必须采用 Java 8 编译,并且加上 -parameters 选项 -->93 <setting name="useActualParamName" value="true"/>94 95 <!-- 指定一个提供 Configuration 实例的类,用来加载被反序列化对象的延迟加载属性值 -->96 <!-- 注意: 这个类必须包含一个签名为 static Configuration getConfiguration() 的方法 -->97 <setting name="configurationFactory" value="com.huangyuanxin.mybatis.MyConfigurationFactory"/>98 99 <!-- 从SQL中删除多余的空格字符 -->100 <setting name="shrinkWhitespacesInSql" value="false"/>101 102 <!-- 默认的数据库厂商类型 -->103 <setting name="defaultSqlProviderType" value="com.huangyuanxin.mybatis.MySqlProvider"/>104</settings>
MyBatis默认定义了许多别名,其中基本类型一般为下划线+类型名,包装类型一般为类名小写。
| 别名(不区分大小写) | 映射的类型 |
|---|---|
| _byte | byte |
| _long | long |
| _short | short |
| _int | int |
| _integer | int |
| _double | double |
| _float | float |
| _boolean | boolean |
| string | String |
| byte | Byte |
| long | Long |
| short | Short |
| int | Integer |
| integer | Integer |
| double | Double |
| float | Float |
| boolean | Boolean |
| date | Date |
| decimal | BigDecimal |
| bigdecimal | BigDecimal |
| object | Object |
| map | Map |
| hashmap | HashMap |
| list | List |
| arraylist | ArrayList |
| collection | Collection |
| iterator | Iterator |
111<!-- 方式1:指定类 -->2<typeAliases>3 <typeAlias alias="Author" type="domain.blog.Author"/>4 <typeAlias alias="Blog" type="domain.blog.Blog"/>5</typeAliases>6
7<!-- 方式2:指定包 -->8<!-- 注意:别名默认为全小写的类名,可通过在类上加上 @Alias("author") 注解修改 -->9<typeAliases>10 <package name="domain.blog"/>11</typeAliases>
在MyBatis配置使用全类名的地方,都可以使用别名代替:
41<!-- 在 resultType 属性使用别名 -->2<select id="findById" parameterType="int" resultType="user">3 SELECT * FROM user WHERE id = #{id}4</select>
在设置SQL参数或从结果集取值时,需要类型处理器进行 JDBC 类型和 Java 类型的转换,MyBatis内置的类型处理器如下:
| 类型处理器 | Java 类型 | JDBC 类型 |
|---|---|---|
| BooleanTypeHandler | java.lang.Boolean、boolean | 数据库兼容的 BOOLEAN |
| ByteTypeHandler | java.lang.Byte、byte | 数据库兼容的 NUMERIC 或 BYTE |
| ShortTypeHandler | java.lang.Short、short | 数据库兼容的 NUMERIC 或 SMALLINT |
IntegerTypeHandler | java.lang.Integer、int | 数据库兼容的 NUMERIC 或 INTEGER |
LongTypeHandler | java.lang.Long、long | 数据库兼容的 NUMERIC 或 BIGINT |
| FloatTypeHandler | java.lang.Float、float | 数据库兼容的 NUMERIC 或 FLOAT |
| DoubleTypeHandler | java.lang.Double、double | 数据库兼容的 NUMERIC 或 DOUBLE |
BigDecimalTypeHandler | java.math.BigDecimal | 数据库兼容的 NUMERIC 或 DECIMAL |
StringTypeHandler | java.lang.String | CHAR, VARCHAR |
| ClobReaderTypeHandler | java.io.Reader | - |
| ClobTypeHandler | java.lang.String | CLOB, LONGVARCHAR |
| NStringTypeHandler | java.lang.String | NVARCHAR, NCHAR |
| NClobTypeHandler | java.lang.String | NCLOB |
| BlobInputStreamTypeHandler | java.io.InputStream | - |
| ByteArrayTypeHandler | byte[] | 数据库兼容的字节流类型 |
| BlobTypeHandler | byte[] | BLOB, LONGVARBINARY |
DateTypeHandler | java.util.Date | TIMESTAMP |
| DateOnlyTypeHandler | java.util.Date | DATE |
| TimeOnlyTypeHandler | java.util.Date | TIME |
SqlTimestampTypeHandler | java.sql.Timestamp | TIMESTAMP |
| SqlDateTypeHandler | java.sql.Date | DATE |
| SqlTimeTypeHandler | java.sql.Time | TIME |
| ObjectTypeHandler | Any | OTHER 或未指定类型 |
| EnumTypeHandler | Enumeration Type | VARCHAR 或任何兼容的字符串类型,用来存储枚举的名称(而不是索引序数值) |
| EnumOrdinalTypeHandler | Enumeration Type | 任何兼容的 NUMERIC 或 DOUBLE 类型,用来存储枚举的序数值(而不是名称)。 |
| SqlxmlTypeHandler | java.lang.String | SQLXML |
| InstantTypeHandler | java.time.Instant | TIMESTAMP |
LocalDateTimeTypeHandler | java.time.LocalDateTime | TIMESTAMP |
| LocalDateTypeHandler | java.time.LocalDate | DATE |
| LocalTimeTypeHandler | java.time.LocalTime | TIME |
| OffsetDateTimeTypeHandler | java.time.OffsetDateTime | TIMESTAMP |
| OffsetTimeTypeHandler | java.time.OffsetTime | TIME |
| ZonedDateTimeTypeHandler | java.time.ZonedDateTime | TIMESTAMP |
| YearTypeHandler | java.time.Year | INTEGER |
| MonthTypeHandler | java.time.Month | INTEGER |
| YearMonthTypeHandler | java.time.YearMonth | VARCHAR 或 LONGVARCHAR |
| JapaneseDateTypeHandler | java.time.chrono.JapaneseDate | DATE |
可以通过实现 org.apache.ibatis.type.TypeHandler 接口, 或继承org.apache.ibatis.type.BaseTypeHandler抽象类(推荐),来自定义类型处理器:
261// ExampleTypeHandler.java 2// 将java类型 String 映射到JDBC类型 VARCHAR3(JdbcType.VARCHAR)4public class ExampleTypeHandler extends BaseTypeHandler<String> {5
6 7 public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {8 ps.setString(i, parameter);9 }10
11 12 public String getNullableResult(ResultSet rs, String columnName) throws SQLException {13 return rs.getString(columnName);14 }15
16 17 public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {18 return rs.getString(columnIndex);19 }20
21 22 public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {23 return cs.getString(columnIndex);24 }25}26
自定义类型处理器需要在typehandlers标签进行配置:
81<!-- mybatis-config.xml -->2<typeHandlers>3 <!-- 注册自定义类型处理器 -->4 <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>5 6 <!-- 也可以使用自动查找类型处理器(注意:此时JDBC 的类型仅能通过注解方式来指定) -->7 <!-- <package name="org.mybatis.example"/> -->8</typeHandlers>注意:
可以通过
@MappedTypes注解或typeHandler标签的javaType="String"属性指定 Java 类型(优先)。可以通过
@MappedJdbcTypes注解或typeHandler标签的jdbcType="VARCHAR"属性指定 JDBC 类型(优先)。
@MappedJdbcTypes注解的includeNullJdbcType=true参数可以开启对 NULL 值的映射处理(默认关闭)。
泛型类型处理器可以处理某一类 java 类型和 JDBC 类型之间的转换:
141// GenericTypeHandler.java2public class GenericTypeHandler<E extends MyObject> extends BaseTypeHandler<E> {3
4 // 处理的具体类型5 private Class<E> type;6
7 // 构造函数:接收MyBatis传入的具体类型8 public GenericTypeHandler(Class<E> type) {9 if (type == null) throw new IllegalArgumentException("Type argument cannot be null");10 this.type = type;11 }12 13 ...14}
EnumTypeHandler 和 EnumOrdinalTypeHandler 都是泛型类型处理器,它会处理任意继承了 Enum 的类,用于枚举值和枚举值名称或枚举值顺序之间的转换。
181<!-- 方式1:全局指定枚举类型处理器(mybatis-config.xml),以自动处理 枚举类属性 的映射 -->2<typeHandlers>3 <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="java.math.RoundingMode"/>4</typeHandlers>5
6<!-- 方式2:在 resultMap 中单独指定枚举类型处理器,用于获取结果集数据 -->7<resultMap id="selectUser" type="org.apache.ibatis.submitted.rounding.User" >8 <id column="id" property="id"/>9 <result column="name" property="name"/>10 <result column="roundingMode" property="roundingMode" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>11</resultMap>12
13<!-- 方式2:在 行内参数映射 指定枚举类型处理器,用于设置SQL参数 -->14<insert id="insert2">15 insert into users2 (id, name, funkyNumber, roundingMode) values (16 #{id}, #{name}, #{funkyNumber}, #{roundingMode, typeHandler=org.apache.ibatis.type.EnumTypeHandler}17 )18</insert>
ObjectFactory用于实例化结果对象,一般通过无参构造来完成,如果配置了构造参数,则调用对应的有参构造。
如需修改 ObjectFactory 的默认行为,可以对其进行自定义:
231// ExampleObjectFactory.java2public class ExampleObjectFactory extends DefaultObjectFactory {3 4 // 处理无参构造5 public Object create(Class type) {6 return super.create(type);7 }8 9 // 处理有参构造10 public Object create(Class type, List<Class> constructorArgTypes, List<Object> constructorArgs) {11 return super.create(type, constructorArgTypes, constructorArgs);12 }13 14 // 配置ObjectFactory 15 public void setProperties(Properties properties) {16 super.setProperties(properties);17 }18 19 // 是否集合?20 public <T> boolean isCollection(Class<T> type) {21 return Collection.class.isAssignableFrom(type);22 }23}自定义 ObjectFactory 在 mybatis-config.xml 的objectFactory标签中进行配置:
31<objectFactory type="org.mybatis.example.ExampleObjectFactory">2 <property name="someProperty" value="100"/>3</objectFactory>
MyBatis 的插件可对如下一些方法进行拦截,并在其前后植入自定义代码:
StatementHandler (prepare, parameterize, batch, update, query)
ParameterHandler (getParameterObject, setParameters)
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ResultSetHandler (handleResultSets, handleOutputParameters)
制作插件需要实现 Interceptor 接口,并指定想要拦截的方法签名:
241// ExamplePlugin.java2// 拦截 Executor 类中参数列表为 MappedStatement.class,Object.class 的 update 方法3({4 (type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})5})6public class ExamplePlugin implements Interceptor {7 // 插件属性8 private Properties properties = new Properties();9 10 // 拦截逻辑11 public Object intercept(Invocation invocation) throws Throwable {12 // 放行13 Object returnObject = invocation.proceed();14 15 // 返回16 return returnObject;17 }18 19 // 设置插件属性20 public void setProperties(Properties properties) {21 this.properties = properties;22 }23}24
自定义插件在 mybatis-config.xml 的plugins标签中进行配置:
51<plugins>2 <plugin interceptor="org.mybatis.example.ExamplePlugin">3 <property name="someProperty" value="100"/>4 </plugin>5</plugins>
531/**2 * Mybatis插件:统计SQL执行时间3 *4 * @author huangyuanxin5 * @date 2021-07-306 */7(value = {8 (type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),9 (type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,10 RowBounds.class, ResultHandler.class}),11 (type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,12 RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})13public class MyBatisExcutorInterceptor implements Interceptor {14 15 public Object intercept(Invocation invocation) {16 // 获取参数传入的MappedStatement17 MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];18
19 // 获取MappedStatementId ==> com.**.dao.**Dao.selectByPrimaryKey20 String sqlId = mappedStatement.getId();21
22 // 记录开始时间23 long start = System.currentTimeMillis();24
25 try {26 // 放行27 return invocation.proceed();28 } catch (Exception e) {29 // SQL执行出现异常30 System.out.println(sqlId + "执行失败!");31 return null;32 } finally {33 // 结束时间34 long time = System.currentTimeMillis() - start;35
36 // 输出到控制台 或 打印日志37 System.out.println(sqlId + ":cost time " + time + " ms");38 }39 }40
41 42 public Object plugin(Object arg0) {43 // JDK动态代理,InvocationHandler为Plugin44 // Plugin拦截行为为调用被代理对象的intercept方法45 // 多个MyBatis拦截器则会被多次动态代理,包装多次!46 return Plugin.wrap(arg0, this);47 }48
49 50 public void setProperties(Properties arg0) {51 // nothing 该拦截器无需配置属性52 }53}
environments 标签定义了 SQL 执行环境相关的配置,并且支持多个 environment 子标签的定义。
201<environments default="development">2 3 <!--开发环境:development-->4 <environment id="development">5 <!-- 事务管理器 -->6 <transactionManager type="JDBC">7 <property name="..." value="..."/>8 </transactionManager>9 <!-- 数据库 -->10 <dataSource type="POOLED">11 <property name="driver" value="${driver}"/>12 <property name="url" value="${url}"/>13 <property name="username" value="${username}"/>14 <property name="password" value="${password}"/>15 </dataSource>16 </environment>17
18 <!-- XXX环境-->19 20</environments>注意:
尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择其中一个,默认环境通过
default属性指定。
在 MyBatis 中有两种类型的事务管理器:
JDBC:直接使用 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。
MANAGED:使用容器来管理事务的整个生命周期。从不提交或回滚一个连接,但默认情况下它会关闭连接。
注意:
在使用 MANAGED 事务管理器的时候, 一些容器并不希望连接被关闭,因此需要将
closeConnection属性设置为 false 。如果你正在使用 Spring + MyBatis,则没有必要配置事务管理器,因为 Spring 模块会使用自带的管理器来覆盖前面的配置。
如需自定义事务管理器,需要实现TransactionFactory和Transaction接口:
151// 事务管理器2public interface TransactionFactory {3 void setProperties(Properties props);4 Transaction newTransaction(Connection conn);5 Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);6}7
8// 事务9public interface Transaction {10 Connection getConnection() throws SQLException;11 void commit() throws SQLException;12 void rollback() throws SQLException;13 void close() throws SQLException;14 Integer getTimeout() throws SQLException; 15}
dataSource 标签使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。有三种内建的数据源类型:
UNPOOLED:这个数据源的实现会每次请求时打开和关闭连接。
driver 这是 JDBC 驱动的 Java 类全限定名(并不是 JDBC 驱动中可能包含的数据源类)。
url 这是数据库的 JDBC URL 地址。
username 登录数据库的用户名。
password 登录数据库的密码。
defaultTransactionIsolationLevel 默认的连接事务隔离级别。
defaultNetworkTimeout 等待数据库操作完成的默认网络超时时间(单位:毫秒)。
POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 除了上述提到 UNPOOLED 下的属性外,还有更多属性用来配置 POOLED 的数据源:
poolMaximumActiveConnections 最大活动连接数,默认值:10。
poolMaximumIdleConnections 最大空闲连接数。
poolMaximumCheckoutTime 在被强制返回之前,池中连接被检出时间,默认值:20000 毫秒。
poolTimeToWait 获取连接超时时间,超时后打印错误日志并尝试重新获取连接,默认值:20000 毫秒。
poolPingEnabled 是否启用侦测查询,默认值:false。
poolPingQuery 侦测查询语句,用来检验连接是否正常工作并准备接受请求,默认“NO PING QUERY SET”。
poolPingConnectionsNotUsedFor 侦测查询的频率,默认值:0(所有连接每一时刻都被侦测,一般与 poolTimeToWait 一致)。
poolMaximumLocalBadConnectionTolerance 坏连接容忍度底层设置,默认值:3(新增于 3.4.5)。
JNDI:这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。这种数据源配置只需要两个属性:
initial_context 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))。
data_source 这是引用数据源实例位置的上下文路径。
注意:
可通过
driver.前缀对数据库驱动进行配置,如:driver.encoding=UTF8,将会通过 DriverManager.getConnection(url, driverProperties) 方法进行传递。数据源配置是可选的,但如果要启用延迟加载特性,就必须配置数据源。
可通过实现 org.apache.ibatis.datasource.DataSourceFactory 接口或继承UnpooledDataSourceFactory 抽象类来使用第三方数据源实现:
91import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;2import com.mchange.v2.c3p0.ComboPooledDataSource;3
4// 定义C3P0数据源5public class C3P0DataSourceFactory extends UnpooledDataSourceFactory {6 public C3P0DataSourceFactory() {7 this.dataSource = new ComboPooledDataSource();8 }9}使用自定义的C3P0数据源如下:
61<dataSource type="org.myproject.C3P0DataSourceFactory">2 <property name="driver" value="org.postgresql.Driver"/>3 <property name="url" value="jdbc:postgresql:mydb"/>4 <property name="username" value="postgres"/>5 <property name="password" value="root"/>6</dataSource>
为了方便后续使用,可在 mybatis-config.xml 中对各数据库厂商配置别名:
91<!-- 默认使用 VendorDatabaseIdProvider 识别数据库厂商 -->2<!-- 左侧用于匹配 DatabaseMetaData#getDatabaseProductName() 返回的长字符串, 右侧是配置的别名 -->3<databaseIdProvider type="DB_VENDOR">4 <property name="MySQL" value="mysql"/>5 <property name="Oracle" value="oracle"/>6 <property name="SQL Server" value="sqlserver"/>7 <property name="PostgreSQL" value="postgresql"/>8 <property name="H2" value="h2"/>9</databaseIdProvider>
可以根据数据库厂商标识,针对不同数据库执行不同SQL:
221<!-- 默认SQL -->2<select id="selectUser" resultType="User">3 SELECT * FROM users WHERE id = #{id}4</select>5
6<!-- Mysql数据库特有SQL -->7<!-- 方式1:在select/update/insert/delete标签中使用 -->8<select id="selectUser" resultType="User" databaseId="mysql">9 SELECT * FROM users WHERE user_id = #{id}10</select>11<!-- 方式2:在if/when标签中使用 -->12<select id="selectUserById" resultType="User">13 SELECT * FROM users WHERE 14 <choose>15 <when test="_databaseId == 'mysql'">16 user_id = #{id}17 </when>18 <otherwise>19 id = #{id}20 </otherwise>21 </choose>22</select>注意:
databaseId配置只在同一资源文件生效,在不同资源文件,可能会先把通用的加载,再加载指定库的就会报错。
可以通过实现接口 org.apache.ibatis.mapping.DatabaseIdProvider 来自定义数据库厂商别名配置行为(替换DB_VENDOR):
151public class MyDatabaseIdProvider implements DatabaseIdProvider {2 private Properties properties;3
4 5 public void setProperties(Properties properties) {6 this.properties = properties;7 }8
9 10 public String getDatabaseId(DataSource dataSource) throws SQLException {11 // 自定义逻辑,根据数据源获取 databaseId12 String productName = dataSource.getConnection().getMetaData().getDatabaseProductName();13 return properties.getProperty(productName);14 }15}
251<!-- 使用相对于类路径的资源引用 -->2<mappers>3 <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>4 <mapper resource="org/mybatis/builder/BlogMapper.xml"/>5 <mapper resource="org/mybatis/builder/PostMapper.xml"/>6</mappers>7
8<!-- 使用完全限定资源定位符(URL) -->9<mappers>10 <mapper url="file:///var/mappers/AuthorMapper.xml"/>11 <mapper url="file:///var/mappers/BlogMapper.xml"/>12 <mapper url="file:///var/mappers/PostMapper.xml"/>13</mappers>14
15<!-- 使用映射器接口实现类的完全限定类名 -->16<mappers>17 <mapper class="org.mybatis.builder.AuthorMapper"/>18 <mapper class="org.mybatis.builder.BlogMapper"/>19 <mapper class="org.mybatis.builder.PostMapper"/>20</mappers>21
22<!-- 将包内的映射器接口实现全部注册为映射器 -->23<mappers>24 <package name="org.mybatis.builder"/>25</mappers>这些配置会告诉 MyBatis 去哪里找映射文件,剩下的细节就应该是每个 SQL 映射文件了,也就是接下来我们要讨论的。
select标签用于配置查询语句:
91<!-- 根据ID查询用户 -->2<select id="findById" resultType="org.example.model.User">3 SELECT * FROM user WHERE id = #{id}4</select>5
6<!-- 调用存储过程 -->7<select id="selectBlog" resultType="org.example.model.Blog" statementType="CALLABLE">8 {call getBlogs(#{id,jdbcType=INTEGER,mode=IN})}9</select>其可选的属性配置如下:
| 属性 | 描述 |
|---|---|
| parameterType | 参数类型,可自动推断,一般不填 |
| parameterMap | 参数映射,已废弃,推荐使用行内参数映射 |
resultType | 结果类型 |
resultMap | 结果映射 |
flushCache | 语句执行前,是否清空本地缓存和二级缓存,默认为false |
| useCache | 是否启用二级缓存,默认为true |
| timeout | 查询超时时间,默认为空,取决于驱动配置 |
| fetchSize | 结果集提取批次数,默认为空,取决于驱动配置 |
| statementType | 语句类型,可选:STATEMENT、PREPARED、CALLABLE |
| resultSetType | 结果集类型,可选:FORWARD_ONLY、SCROLL_SENSITIVE、SCROLL_INSENSITIVE,默认为空(DEFAULT) |
databaseId | 适用的数据库厂商 |
| resultOrdered | 结果是否被排序,默认为false,可能会用于优化性能 |
| resultSets | 设置多结果集名称列表,以逗号分隔 |
insert、update和delete标签用于配置更新语句:
141<!-- 插入 -->2<insert id="insertAuthor">3 insert into Author (id,username,password,email) values (#{id},#{username},#{password},#{email})4</insert>5
6<!-- 更新 -->7<update id="updateAuthor">8 update Author set password = #{password} where id = #{id}9</update>10
11<!-- 删除 -->12<delete id="deleteAuthor">13 delete from Author where id = #{id}14</delete>注意:
insert、update和 delete 标签仅有语义上的区别,且都可以用来执行 DDL 语句。
flushCache属性对更新标签的默认值为true,而对查询标签的默认值为false。
此外,更新标签还有几个额外的属性配置,用于自增列回写:
| 属性 | 描述 |
|---|---|
useGeneratedKeys | 在执行insert和update语句时,是否回写数据库自动生成的主键(getGeneratedKeys),默认为false |
| keyColumn | 主键列名,默认为第一列,复合主键以逗号分隔 |
| keyProperty | 主键列对应属性名,复合主键以逗号分隔,会被设置为getGeneratedKeys的返回值或通过selectKey标签设置 |
141<!-- 在插入时回写自增列(假设id列为自增列) -->2<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">3 insert into Author (username,password,email) values (#{username},#{password},#{email})4</insert>5
6<!-- 在批量插入时回写自增列 -->7<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">8 insert into Author (username, password, email) values9 10 <!-- 传入List<Author>类型参数进行批量插入,自动生成的主键将会依次回写到对应的id属性中 -->11 <foreach item="item" collection="list" separator=",">12 (#{item.username}, #{item.password}, #{item.email})13 </foreach>14</insert>
selectKey标签可用于生成主键,并设置到 insert 语句的参数中:
101<!-- 在插入前生成主键 -->2<insert id="insertAuthor">3 <!-- 生成主键 -->4 <selectKey keyProperty="id" resultType="int" order="BEFORE">5 select cast(random()*1000000 as integer) 6 </selectKey>7 8 <!-- 带主键插入 -->9 insert into Author (id, username, password, email) values (#{id}, #{username}, #{password}, #{email})10</insert>注意:
selectKey 标签支持 keyProperty、keyColumn、resultType、statementType等属性,用法与之前类似。
此外,selectKey 标签还可以通过
order属性选择在 insert 语句之前或之后执行(BEFORE或AFTER)。
sql标签用于定义可重用的SQL片段,以便在其它语句中使用:
111<!-- 定义用户表的列名 -->2<sql id="userColumns"> 3 ${alias}.id,${alias}.username,${alias}.password 4</sql>5
6<!-- 在selct/insert/update/delete/sql等标签中引用 -->7<select id="selectUsers" resultType="map">8 select <include refid="userColumns"> <property name="alias" value="t1"/> </include>9 from Author t110</select>11
注意:
引用可嵌套,但注意必须要能够在加载时推断出来(而非运行时)。
151-- 1. 仅指定参数名称(大多数情形使用)2and id = #{id}3and sex = #{user.sex} 4
5-- 2. 指定JDBC类型(一般在参数值可能为空时使用,避免因数据库驱动程序的差异导致的问题)6and email = #{email,jdbcType=VARCHAR}7
8-- 3. 指定类型处理器(在需要自定义类型转换逻辑时使用)9and age = #{age,typeHandler=MyTypeHandler}10
11-- 4. 指定全部属性(不常用)12-- "mode=IN" 可以指定参数为输入参数(IN)、输出参数(OUT)或输入输出参数(INOUT)13-- “numericScale=2” 可以指定数值类型的精度,如未指定,则使用数据库默认的精度14-- “javaType=NUMERIC” 可以显示指定Java类型,但一般无需指定,MyBatis可以根据参数对象的类型推断(使用HashMap作为参数除外)15and age = #{age, mode=OUT,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler,numericScale=2}注意:
默认情况下,MyBatis会按
parm1、parm2、parm3....的顺序对参数进行命名。如果采用 Java 8 及以上版本编译,并且加上
-parameters选项,则可直接使用形参名作为参数名。此外,也可以直接通过
@Parm("xxx")注解显示指定参数名称。如果只存在一个POJO参数,则可省略前缀,如参数列表为
xxx(User user),则#{user.sex}可简写为#{sex}。如果参数是SQL语句的一部分,无法作为预编译参数
?,则可使用${xxx}进行字符串替换,如:ORDER BY ${columnName},但需注意SQL注入问题。
使用select标签的resultType属性,可以进行简单结果映射:
141<!-- 1. 映射到包装类(适用于SQL返回单列结果集) -->2<select id="findTotal" resultType="int">3 select count(id) from user;4</select>5
6<!-- 2. 映射到Map类型(key为列名,value为列值) -->7<select id="selectUsers" resultType="map">8 select id, username, hashedPassword from some_table where id = #{id}9</select>10
11<!-- 3. 映射到JavaBean类型(同名属性映射,可支持下划线转驼峰) -->12<select id="selectUsers" resultType="com.someapp.model.User">13 select user_id as "id", username, hashedPassword from some_table where user_id = #{id}14</select>如果查询数据返回多行,需要将 Mapper 方法的返回类型修改为集合类型,但resultType属性配置无需修改:
612public interface UserMapper {3 List<Integer> findIds(UserQueryReq req);4 List<Map<String,Object> selectUsers(UserQueryReq req);5 List<User> selectUsers(UserQueryReq req);6}注意:
可以在
mybatis-config.xml中为JavaBean类型配置类型别名,这样resultType中就不用输入全限定类名了。在映射到JavaBean类型时,如果列名和属性名无法匹配,则可以通过
user_id as "id"方式为列起别名(SQL语法)。
使用select标签的resultMap属性,可以进行自定义结果映射:
151<!-- step1:定义一个自定义结果映射配置 -->2<resultMap id="userResultMap" type="User">3 <!-- 映射主键列 -->4 <!-- 主键列可用于快速比较对象,优化缓存和嵌套映射的性能 -->5 <id property="id" column="user_id" />6 7 <!-- 映射普通列 -->8 <result property="username" column="user_name"/>9 <result property="password" column="hashed_password"/>10</resultMap>11
12<!-- step2:使用 userResultMap 进行自定义结果映射 -->13<select id="selectUsers" resultMap="userResultMap">14 select user_id, user_name, hashed_password from some_table where id = #{id}15</select>注意:
默认情况下,MyBatis会自动映射未配置的列,可通过
resultMap标签的autoMapping="false"属性关闭自动映射。
resultMap标签支持指定extends="parentResultMap"属性,对另一个映射配置进行继承,然后再覆盖和扩展。子标签
id和result可以指定jdbcType和typeHandler,用于声明Java类型(映射到Map时)和自定义类型转换过程。
嵌套结果映射指对复杂的关联查询的结果集进行嵌套映射,下面以一对一关联查询映射为例:
451<!-- “博客+作者”一对一关联查询SQL -->2<select id="selectBlog" resultMap="blogResult">3 select B.id as blog_id,4 B.title as blog_title,5 B.author_id as blog_author_id,6 A.id as author_id,7 A.username as author_username,8 A.password as author_password,9 A.email as author_email,10 A.bio as author_bio11 from Blog B left outer join Author A on B.author_id = A.id12 where B.id = #{id}13</select>14
15<!-- 方式1:内联方式 -->16<resultMap id="blogResult" type="Blog">17 <id property="id" column="blog_id" />18 <result property="title" column="blog_title"/>19 20 <!-- 一对一映射 -->21 <association property="author" javaType="Author">22 <id property="id" column="author_id"/>23 <result property="username" column="author_username"/>24 <result property="password" column="author_password"/>25 <result property="email" column="author_email"/>26 <result property="bio" column="author_bio"/>27 </association>28</resultMap>29
30<!-- 方式2:引用方式 -->31<resultMap id="blogResult" type="Blog">32 <id property="id" column="blog_id" />33 <result property="title" column="blog_title"/>34 35 <!-- 引用其它映射配置,进行一对一映射 -->36 <association property="author" column="blog_author_id" javaType="Author" resultMap="authorResult"/>37</resultMap>38<!-- 被引用的映射配置 -->39<resultMap id="authorResult" type="Author">40 <id property="id" column="author_id"/>41 <result property="username" column="author_username"/>42 <result property="password" column="author_password"/>43 <result property="email" column="author_email"/>44 <result property="bio" column="author_bio"/>45</resultMap>一对多查询基本类似,只是多了一个ofType属性,用于指明集合元素的类型:
71<!-- 内联方式 -->2<collection property="posts" ofType="Post">3 <!-- 其它部分和一对一完全一致 -->4</collection>5
6<!-- 引用方式 -->7<collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>注意:
引用外部映射配置时,可以添加列别名前缀,如
columnPrefix="co_"。可以通过
notNullColumn属性配置非空列(以逗号分隔),只有在这些列非空时才会创建子对象。
嵌套Select查询指先查询主要字段,再通过N+1查询子对象字段,下面以一对一嵌套Select查询为例:
141<!-- “博客”单独查询 -->2<select id="selectBlog" resultMap="blogResult">3 SELECT * FROM BLOG WHERE ID = #{id}4</select>5
6<!-- 通过嵌套Select映射再查询“作者” -->7<resultMap id="blogResult" type="Blog">8 <association property="author" column="author_id" javaType="Author" select="selectAuthor" fetchType="lazy"/>9</resultMap>10
11<!-- 被引用的“作者”单独查询 -->12<select id="selectAuthor" resultType="Author">13 SELECT * FROM AUTHOR WHERE ID = #{id}14</select>一对多查询基本类似,只是多了一个ofType属性,用于指明集合元素的类型:
21<!-- 一对多嵌套Select查询 -->2<collection property="posts" column="id" ofType="Post" select="selectPostsForBlog"/>注意:
column标签可通过column="{prop1=col1,prop2=col2}方式给嵌套Select查询传递多个参数。嵌套Select查询一般通过
fetchType="lazy"方式设置懒加载,以防止过多的N+1查询。
某些数据库允许存储过程返回多个结果集,或一次性执行多个语句,每个语句返回一个结果集:
51CREATE PROCEDURE getBlogAndAuthor(IN id INT)2BEGIN3 SELECT * FROM BLOG WHERE ID = id;4 SELECT * FROM AUTHOR WHERE ID = id;5END;在MyBatis中,可以对多结果集进行映射,下面以一对一多结果集映射为例:
231<!-- 调用存储过程,返回多个结果集 -->2<select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">3 {call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}4</select>5
6<!-- 多结果集映射配置 -->7<resultMap id="blogResult" type="Blog">8 <!-- 映射第1结果集 -->9 <id property="id" column="id" />10 <result property="title" column="title"/>11 12 <!-- 映射第2结果集 -->13 <!-- resultSet:从哪个结果集查询数据 -->14 <!-- column:使用第1结果集的哪个字段进行查询(多个字段以逗号分隔) -->15 <!-- foreignColumn:使用当前结果集的哪个字段进行匹配(多个字段以逗号分隔) -->16 <association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id">17 <id property="id" column="id"/>18 <result property="username" column="username"/>19 <result property="password" column="password"/>20 <result property="email" column="email"/>21 <result property="bio" column="bio"/>22 </association>23</resultMap>一对多查询基本类似,只是多了一个ofType属性,用于指明集合元素的类型:
41<!-- 一对多多结果集映射 -->2<collection property="posts" ofType="Post" resultSet="posts" column="id" foreignColumn="blog_id">3 <!-- 其它部分和一对一完全一致 -->4</collection>
一般情况下,MyBatis通过无参构造创建结果对象,然后再通过 getter/setter 方法进行赋值,如果需要指定有参构造方法,则可通过<constructor>标签实现。
71<!-- 指定构造方法进行映射 -->2<!-- public User(Integer id, String username, int age){...} -->3<constructor>4 <idArg column="id" javaType="int" name="id" />5 <arg column="username" javaType="String" name="username" />6 <arg column="age" javaType="_int" name="age" />7</constructor>注意:
如果映射配置顺序和构造方法字段顺序一致,则
name属性可以省略。此外,
idArg和arg标签还支持jdbcType、typeHandler、select、resultMap等一些属性。
鉴别器可根据某个列值进行不同的映射,类似于switch语句:
271<!-- 通过鉴别器映射不同类型的交通工具 -->2<resultMap id="vehicleResult" type="Vehicle">3 <!-- 交通工具的通用字段映射 -->4 <id property="id" column="id" />5 <result property="vin" column="vin"/>6 <result property="year" column="year"/>7 <result property="make" column="make"/>8 <result property="model" column="model"/>9 <result property="color" column="color"/>10 11 <!-- 使用 vehicle_type 值鉴别为不同交通工具 -->12 <discriminator javaType="int" column="vehicle_type">13 <!-- 交通工具1映射 -->14 <case value="1" resultType="carResult">15 <result property="doorCount" column="door_count" />16 </case>17 <!-- 交通工具2映射 -->18 <case value="2" resultType="truckResult">19 <result property="boxSize" column="box_size" />20 <result property="extendedCab" column="extended_cab" />21 </case>22 <!-- 交通工具3映射 -->23 <case value="3" resultType="vanResult">24 <result property="powerSlidingDoor" column="power_sliding_door" />25 </case>26 </discriminator>27</resultMap>注意:
case标签还支持通过resultMap属性引用自定义映射,并且引用的自定义映射可以配置extends="vehicleResult"属性。
下面是一个非常复杂的查询映射案例,可以作为学习参考。
651<!-- 把这个查询结果映射到一个复杂的Java对象。2 这个对象表示了一篇博客,它由某位作者所写,有很多的博文,每篇博文有零或多条的评论和标签。-->3<select id="selectBlogDetails" resultMap="detailedBlogResultMap">4 select5 B.id as blog_id,6 B.title as blog_title,7 B.author_id as blog_author_id,8 A.id as author_id,9 A.username as author_username,10 A.password as author_password,11 A.email as author_email,12 A.bio as author_bio,13 A.favourite_section as author_favourite_section,14 P.id as post_id,15 P.blog_id as post_blog_id,16 P.author_id as post_author_id,17 P.created_on as post_created_on,18 P.section as post_section,19 P.subject as post_subject,20 P.draft as draft,21 P.body as post_body,22 C.id as comment_id,23 C.post_id as comment_post_id,24 C.name as comment_name,25 C.comment as comment_text,26 T.id as tag_id,27 T.name as tag_name28 from Blog B29 left outer join Author A on B.author_id = A.id30 left outer join Post P on B.id = P.blog_id31 left outer join Comment C on P.id = C.post_id32 left outer join Post_Tag PT on PT.post_id = P.id33 left outer join Tag T on PT.tag_id = T.id34 where B.id = #{id}35</select>36
37<!-- 结果映射写法参考 -->38<resultMap id="detailedBlogResultMap" type="Blog">39 <constructor>40 <idArg column="blog_id" javaType="int"/>41 </constructor>42 <result property="title" column="blog_title"/>43 <association property="author" javaType="Author">44 <id property="id" column="author_id"/>45 <result property="username" column="author_username"/>46 <result property="password" column="author_password"/>47 <result property="email" column="author_email"/>48 <result property="bio" column="author_bio"/>49 <result property="favouriteSection" column="author_favourite_section"/>50 </association>51 <collection property="posts" ofType="Post">52 <id property="id" column="post_id"/>53 <result property="subject" column="post_subject"/>54 <association property="author" javaType="Author"/>55 <collection property="comments" ofType="Comment">56 <id property="id" column="comment_id"/>57 </collection>58 <collection property="tags" ofType="Tag" >59 <id property="id" column="tag_id"/>60 </collection>61 <discriminator javaType="int" column="draft">62 <case value="1" resultType="DraftPost"/>63 </discriminator>64 </collection>65</resultMap>
一级缓存也叫会话缓存或本地缓存(local cache),默认的缓存作用域为SESSION级别,可以在主配置文件的setting标签修改。
41<settings>2 <!-- 修改一级缓存作用域为STATEMENT级别(仅用于解决关联查询死循环问题)-->3 <setting name="localCacheScope" value="STATEMENT"/>4</settings>如果想要命中一级缓存,必须要满足下面两个条件:
同一个会话。
同一个CacheKey。即EnvironmentId、MappedStatementId、SQL语句、运行时参数和RowBounds都需一致。
同时需要注意,在下面一些情形会直接清空一级缓存。
会话提交、回滚和执行 DML 语句始终会清空一级缓存。
在主查询前,如果MS配置了flushCache=true也会刷新一级缓存。
在主查询后,如果缓存作用域为SATEMENT也会清空一级缓存。
另外需注意:
缓存作用域建议为SESSION级别, STATEMENT级别仅关联查询的子查询可用缓存。
如果开启了二级缓存,则会优先使用二级缓存,二级缓存未命中才会查询一级缓存。
1031/**2 * 一级缓存测试验证。3 * 注意:4 * 1. setting 中一级缓存的作用域(localCacheScope属性)为 SESSION(默认值),而非STATEMENT。5 * 2. 确保在 setting 和 mapper 中没有都开启二级缓存(默认关闭)。6 */7public class FirstCacheTest {8 SqlSessionFactory factory;9 SqlSession sqlSession;10
11 12 public void init() throws SQLException {13 factory = new SqlSessionFactoryBuilder().build(ExecutorTest.class.getResourceAsStream("/mybatis-config.xml"));14 sqlSession = factory.openSession();15
16 // 在此插入id为2和3的数据17 }18
19 // 1. 参数必须相同20 21 public void test01() {22 UserDao userMapper = sqlSession.getMapper(UserDao.class);23 User user01 = userMapper.findById(2);24 User user02 = userMapper.findById(2); // 从缓存取值25 System.out.println(user01 == user02); // true26
27 User user03 = userMapper.findById(3);28 System.out.println(user01 == user03); // false (注意是否有数据,否则null==null。下面例子不再提及,自行注意!)29 }30
31 // 2. MappedStatementId(类名.方法名)必须相同32 33 public void test02() {34 UserDao userMapper = sqlSession.getMapper(UserDao.class);35 User user01 = userMapper.findById(2);36 User user02 = userMapper.findByIdCopy(2);37 System.out.println(user01 == user02); // false38 }39
40 // 3. 同一个会话。这也是一级缓存被称为会话级缓存的原因之一。41 42 public void test03() {43 UserDao userMapper = sqlSession.getMapper(UserDao.class);44 User user01 = userMapper.findById(2);45
46 // 创建一个新的 SqlSession47 SqlSession sqlSessionAnother = factory.openSession();48 UserDao userMapperAnother = sqlSessionAnother.getMapper(UserDao.class);49 User user02 = userMapperAnother.findById(2);50 System.out.println(user01 == user02); // false51
52 // 调用方式可以多样,确保为同一个sqlSession即可。53 List user03 = sqlSession.selectList("org.example.dao.UserDao.findById", 2);54 System.out.println(user01 == user03.get(0)); // true55 }56
57 // 4. RowBounds 必返回行范围必须相同。默认为 RowBounds.DEFAULT。58 59 public void test04() {60 UserDao userMapper = sqlSession.getMapper(UserDao.class);61 User user01 = userMapper.findById(2);62
63 // 设置分页64 RowBounds rowBounds = new RowBounds(0, 10);65 List user02 = sqlSession.selectList("org.example.dao.UserDao.findById", 2, rowBounds);66 System.out.println(user01 == user02.get(0)); // false67
68 // 默认分页条件69 RowBounds rowBoundsDefault = RowBounds.DEFAULT;70 List user03 = sqlSession.selectList("org.example.dao.UserDao.findById", 2, rowBoundsDefault);71 System.out.println(user01 == user03.get(0)); // true72 }73
74 // 5. 会话未进行提交、回滚和清除缓存等操作。75 76 public void test05() {77 UserDao userMapper = sqlSession.getMapper(UserDao.class);78 User user01 = userMapper.findById(2);79 // sqlSession.commit();80 // sqlSession.rollback();81 sqlSession.clearCache();82 User user02 = userMapper.findById(2);83 System.out.println(user01 == user02); // false84 }85
86 // 6. 未调用 flushCache=true 的方法。增删改的flushCache默认为true,查默认为false。87 88 public void test06() {89 UserDao userMapper = sqlSession.getMapper(UserDao.class);90 User user01 = userMapper.findById(2);91
92 // 调用 flushCache 为true的方法,刷新缓存93 // userMapper.setName(3, "test06");94 User userFlush = userMapper.findByIdOptionsFlushCacheIsTrue(2);95 System.out.println(user01 == userFlush); // false 先刷新,再执行96
97 // 再次查询98 User user02 = userMapper.findById(2);99 System.out.println(user01 == user02); // false100 }101 102}103
MyBatis在查询一级缓存之前,还会优先查找二级缓存,二级缓存是应用级别的缓存,适用于不同会话之间。要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行配置即可。
81<!-- 开启当前Mapper的二级缓存2 - 当前Mapper的所有select语句的结果将会被缓存3 - 当前Mapper的所有 insert、update 和 delete 语句会刷新缓存4 - 使用最近最少使用的淘汰策略(LRU, Least Recently Used),size默认为1024。5 - 不会进行过期清理,即缓存未被淘汰将永久存在。6 - 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。7-->8<cache/> 使用二级缓存时必须注意一些相关事项:
请勿关闭全局缓存开关cacheEnabled,以及单个语句的缓存开关useCache。
缓存的数据只有在会话提交后才能被使用,防止脏读问题(注意:设置自动提交并不能使二级缓存立即生效)。
执行配置了flushCache=true的语句会在执行查询前刷新缓存(flushCache=true也会在DML语句和主查询前刷新一级缓存)。
注解缓存配置和XML缓存配置必须通过缓存引用才能共享(这是MyBatis的一个bug,两者不能同时配置)。
不能使用自定义的ResultHandler,否则将无法使用二级缓存。
如果二级缓存仍无法命中,请检查CacheKey是否一致。
下面是一个二级缓存的简单测试案例。
231// 提交或关闭会话后,二级缓存才能被其他会话命中2// 即使在打开会话时设置了自动提交,也需要手动提交后才会生效3// 其他命中条件:环境ID一致、StatementId一致(Sql一致)、执行参数一致、RowBounds一致。45public void cacheTest02() {6 System.out.println("================ 第一次执行 ================");7 SqlSession sqlSession01 = factory.openSession(true); // 设置自动提交并不能使二级缓存立即生效8 UserDao userMapper01 = sqlSession01.getMapper(UserDao.class);9 User user01 = userMapper01.findById(2); // 查询数据库10
11 System.out.println("================ 第二次执行 ================");12 SqlSession sqlSession02 = factory.openSession();13 UserDao userMapper02 = sqlSession02.getMapper(UserDao.class);14 User user02 = userMapper02.findById(2); // 也是查询数据库(看日志),因为 sqlSession01 还未提交15
16 sqlSession02.commit(); // 提交会话,sqlSession02 设置的二级缓存生效17 // sqlSession02.close(); // close()方法也会提交会话18
19 System.out.println("================ 第三次执行 ================");20 SqlSession sqlSession03 = factory.openSession();21 UserDao userMapper03 = sqlSession03.getMapper(UserDao.class);22 User user03 = userMapper03.findById(2); // 查询缓存23}
可以使用 select/update/insert/delete 标签的属性进行更细粒度的缓存控制。
useCache:为 true 时表示配置的 SQL 语句使用二级缓存,否则表示不进行缓存。默认值为true。
flushCache:为true时表示执行该语句前进行缓存刷新,否则表示无需刷新缓存。select 标签的默认值为false,其他为true。
二级缓存的行为可以通过 cache 标签的属性来修改。比如:
11<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>这个更高级的配置创建了一个 FIFO 缓存,每隔 60000 毫秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。 但你可能会想要在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 cache-ref 元素来引用另一个缓存。
101<!-- Mapper1.xml -->2<mapper namespace="xyz.coolblog.dao.Mapper1">3 <!-- Mapper1 与 Mapper2 共用一个二级缓存 -->4 <cache-ref namespace="xyz.coolblog.dao.Mapper2"/>5</mapper>6
7<!-- Mapper2.xml -->8<mapper namespace="xyz.coolblog.dao.Mapper2">9 <cache/>10</mapper>注意:
即使在同一个命名空间,如果是通过注解和XML分别配置,则两者不会共享,需要通过缓存引用进行关联。
通过cache 标签的type属性,可以配置其他第三方缓存的适配器,来代替MyBatis内置的二级缓存模块。
11<cache type="com.domain.something.MyCustomCache"/>
第三方缓存的适配器需要实现org.apache.ibatis.cache.Cache 接口,并提供一个接受 String 参数作为 id 的构造器。
91public interface Cache {2 String getId();3 int getSize();4 void putObject(Object key, Object value);5 Object getObject(Object key);6 boolean hasKey(Object key);7 Object removeObject(Object key);8 void clear();9}
如需对缓存适配器进行配置,可直接通过如下方式,MyBatis将会自动匹配和调用对应的 set 方法:
51<!-- 使用第三方缓存的适配器 -->2<cache type="com.domain.something.MyCustomCache">3 <!-- 缓存适配器属性配置,将会调用 setCacheFile(String file) 方法-->4 <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>5</cache>
如需对缓存适配器进行初始化,可实现 org.apache.ibatis.builder.InitializingObject 接口,将会在属性设置完毕后调用:
31public interface InitializingObject {2 void initialize() throws Exception;3}注意:
上一节中对缓存的配置(如清除策略、可读或可读写等),不能应用于自定义缓存。
<if> 标签用于在动态 SQL 中根据条件动态拼接 SQL 语句:
141<select id="findUsers" parameterType="map" resultType="User">2 SELECT * FROM user WHERE 1=13
4 <!-- 字符类型 -->5 <if test="name != null and name != ''">6 AND name LIKE CONCAT('%', #{name}, '%')7 </if>8
9 <!-- 数值类型 -->10 <if test="age != null and age > 0">11 AND age = #{age}12 </if>13
14</select>
<choose> 标签允许根据多个条件动态选择 SQL 片段,类似 Java 中的 switch 或 if-else if-else 的逻辑:
191<select id="findUsers" parameterType="map" resultType="User">2 SELECT * FROM user WHERE 1=13 <where>4 <choose>5 <when test="name != null and name != ''">6 AND name LIKE CONCAT('%', #{name}, '%')7 </when>8 <when test="age != null">9 AND age = #{age}10 </when>11 <when test="email != null and email != ''">12 AND email = #{email}13 </when>14 <otherwise>15 AND 1 = 1 <!-- 默认条件,可以省略 -->16 </otherwise>17 </choose>18 </where>19</select>注意:
<choose>标签中的<when>条件是按顺序检查的,一旦某个条件满足,后续的<when>和<otherwise>将被忽略。
trim标签用于移除指定前后缀,并在标签内容不为空时,添加新的前后缀。
141<!-- trim标签模板 -->2<trim prefixOverrides="要移除的前缀" suffixOverrides="要移除的后缀" prefix="新的前缀" suffix="新的后缀" >3 <!-- 标签内容 -->4</trim>5
6<!-- 基于trim标签实现where标签 -->7<trim prefix="WHERE" prefixOverrides="AND |OR ">8 <!-- 标签内容 -->9</trim>10
11<!-- 基于trim标签实现set标签 -->12<trim prefix="SET" suffixOverrides=",">13 <!-- 标签内容 -->14</trim>也可直接使用内置的where标签和set标签:
251<!-- where标签:移除开头的AND或OR,如果标签内容不为空,则在开头拼接WHERE -->2<select id="findUsers" parameterType="map" resultType="User">3 SELECT * FROM user4 <where>5 <!-- 字符类型 -->6 <if test="name != null and name != ''">7 AND name LIKE CONCAT('%', #{name}, '%')8 </if>9
10 <!-- 数值类型 -->11 <if test="age != null and age > 0">12 AND age = #{age}13 </if>14 </where>15</select>16
17<!-- where标签:移除末尾的逗号,如果标签内容不为空,则在开头拼接SET -->18<update id="updateAuthorIfNecessary">19 update Author20 <set>21 <if test="username != null">username=#{username},</if>22 <if test="password != null">password=#{password},</if>23 </set>24 where id=#{id}25</update>
<foreach> 标签用于处理集合或数组类型的参数,适合在批量操作或动态生成 IN 子句等场景中使用。
321<!-- foreach标签模板 -->2<foreach collection="集合或数组" item="当前项的变量名" index="当前项的索引变量名" 3 open="开始字符" separator="分隔符" close="结束字符">4 <!-- SQL 片段 -->5</foreach>6
7<!-- 批量插入 -->8<insert id="insertUsers" parameterType="list">9 INSERT INTO user (id, name, age) VALUES10 <foreach collection="list" item="user" separator=",">11 (#{user.id}, #{user.name}, #{user.age})12 </foreach>13</insert>14
15<!-- 动态生成IN子句 -->16<select id="findUsersByIds" parameterType="list" resultType="User">17 SELECT * FROM user WHERE id IN18 <foreach collection="list" item="id" open="(" separator="," close=")">19 #{id}20 </foreach>21</select>22
23<!-- MAP类型参数更新 -->24<update id="updateUsers" parameterType="map">25 UPDATE user26 <set>27 <foreach collection="map" item="value" index="key" separator=",">28 ${key} = #{value}29 </foreach>30 </set>31 WHERE id = #{id}32</update>注意:
<foreach>标签的collection属性可以是任何可迭代对象,如:集合、数组或 Map 类型。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。
当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。
bind 标签允许你在动态 SQL 中定义一个变量,并将其绑定到当前的上下文。
161<select id="findUsers" parameterType="map" resultType="User">2 SELECT * FROM user3 <where>4 <!-- 定义变量 -->5 <bind name="namePattern" value="'%' + name + '%'" />6 <bind name="emailPattern" value="'%' + email + '%'" />7 8 <!-- 使用变量 -->9 <if test="name != null and name != ''">10 AND name LIKE #{namePattern}11 </if>12 <if test="email != null and email != ''">13 AND email LIKE #{emailPattern}14 </if>15 </where>16</select>
要在带注解的映射器接口类中使用动态 SQL,可以使用 script 标签。
91({"<script>",2 "update Author",3 " <set>",4 " <if test='username != null'>username=#{username},</if>",5 " <if test='password != null'>password=#{password},</if>",6 " </set>",7 "where id=#{id}",8 "</script>"})9void updateAuthorValues(Author author);
前面的动态SQL标签由语言驱动 org.apache.ibatis.scripting.xmltags.XmlLanguageDriver(别名为 xml)提供支持。
如需自定义语言驱动,可以实现LanguageDriver接口:
51public interface LanguageDriver {2 ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);3 SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);4 SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);5}并在 mybatis-config.xml 文件中将它设置为默认语言:
61<typeAliases>2 <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>3</typeAliases>4<settings>5 <setting name="defaultScriptingLanguage" value="myLanguage"/>6</settings>或者,使用 lang 属性为特定的语句指定语言:
31<select id="selectBlog" lang="myLanguage">2 SELECT * FROM BLOG3</select>或者,在 mapper 接口上添加 @Lang 注解:
51public interface Mapper {2 (MyLanguageDriver.class)3 ("SELECT * FROM BLOG")4 List<Blog> selectBlog();5}注意:
可以使用 Apache Velocity 作为动态语言,更多细节请参考 MyBatis-Velocity 项目。
映射器注解也可用于映射配置,下面是所有映射器注解列表:
| 注解 | XML等价形式 | 注解 | XML等价形式 |
|---|---|---|---|
| @Insert | <insert> | @CacheNamespace | <cache> |
| @Update | <update> | @CacheNamespaceRef | <cacheRef> |
| @Delete | <delete> | @Flush | N/A |
| @Select | <delete> | @InsertProvider | <insert> |
| @Param | N/A | @UpdateProvider | <update> |
| @Results | <resultMap> | @DeleteProvider | <delete> |
| @Result | <result>、<id> | @SelectProvider | <delete> |
| @ConstructorArgs | <constructor> | @SelectKey | <selectKey> |
| @Arg | <arg>、<idArg> | @Options | N/A |
| @One | <association> | @Property | <property> |
| @Many | <collection> | @MapKey | N/A |
| @ResultType | N/A | @TypeDiscriminator | <discriminator> |
| @ResultMap | N/A | @Case | <case> |
531// 查询用户,返回对象2(id = "userResult", value = {3 (property = "id", column = "uid", id = true),4 (property = "firstName", column = "first_name"),5 (property = "lastName", column = "last_name")6})7("select * from users where id = #{id}")8User getUserById(Integer id);9
10// 查询用户,包含一对一的 Profile 和一对多的 Orders11("SELECT * FROM user WHERE id = #{id}")12({13 (property = "id", column = "id"),14 (property = "name", column = "name"),15 (property = "age", column = "age"),16 (property = "profile", column = "id", 17 one = (select = "com.example.mapper.ProfileMapper.getProfileByUserId")),18 (property = "orders", column = "id", 19 many = (select = "com.example.mapper.OrderMapper.getOrdersByUserId"))20})21User getUserById(("id") int id);22
23// 查询所有用户,返回MAP(KEY为用户id)24("SELECT * FROM user")25("id")26Map<Integer, User> getAllUsersMap();27
28// 插入之前生成主键29("insert into table3 (id, name) values(#{nameId}, #{name})")30(statement="call next value for TestSequence", keyProperty="nameId", 31 before=true, resultType=int.class)32int insertTable3(Name name);33
34// 插入之后查询主键35("INSERT INTO user (name, age) VALUES (#{name}, #{age})")36(statement = "SELECT LAST_INSERT_ID()", keyProperty = "id", before = false)37int insertUser(("name") String name, ("age") int age);38
39// 使用结果构造器40(id = "companyResults")41({42 (column = "cid", javaType = Integer.class, id = true),43 (column = "name", javaType = String.class)44})45("select * from company where id = #{id}")46Company getCompanyById(Integer id);47
48// 多数据支持49(value = "SELECT SYS_GUID() FROM dual", databaseId = "oracle") // oracle50(value = "SELECT uuid_generate_v4()", databaseId = "postgres") //postgres51("SELECT RANDOM_UUID()") //others52String generateId();53
MyBatis-Plus(简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
211<!-- 在 SpringBoot 项目中 -->2<dependency>3 <groupId>com.baomidou</groupId>4 <artifactId>mybatis-plus-boot-starter</artifactId>5 <version>3.5.2</version>6</dependency>7
8<!-- 在 SpringBoot3 项目中 -->9<dependency>10 <groupId>com.baomidou</groupId>11 <artifactId>mybatis-plus-spring-boot3-starter</artifactId>12 <version>最新版本</version>13</dependency>14
15<!-- 在 Spring 项目中 -->16<dependency>17 <groupId>com.baomidou</groupId>18 <artifactId>mybatis-plus</artifactId>19 <version>最新版本</version>20</dependency>21
注意:
使用 mybatis-plus 后,配置要使用 mybatis-plus 前缀开头:
41#mybatis:2# mapper-locations: classpath*:com/huangyuanxin/mapper/common/*.xml,classpath*:com/huangyuanxin/mapper/${database.type}/*.xml3mybatis-plus4mapper-locationsclasspath*com/huangyuanxin/mapper/common/*.xml,classpath*com/huangyuanxin/mapper/$database.type/*.xml
91("sys_user")2public class User {3 4 private Long id;5 ("nickname")6 private String name;7 private Integer age;8 private String email;9}
41// 继承BaseMapper23public interface UserMapper extends BaseMapper<User> {4}
在 SpringBoot 项目中
912("com.xxx.mapper")3public class Application {4
5 public static void main(String[] args) {6 SpringApplication.run(Application.class, args);7 }8
9}在 Spring 项目中
111
2<!-- 1. 配置 MapperScan -->3<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">4 <property name="basePackage" value="com.xxx.mapper"/>5</bean>6
7<!-- 2. 调整 SqlSessionFactory 为 MyBatis-Plus 的 SqlSessionFactory -->8<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">9 <property name="dataSource" ref="dataSource"/>10</bean>11
1612("org.example.mapper")3public class Application {4
5 public static void main(String[] args) {6 // 启动7 ConfigurableApplicationContext ctx = SpringApplication.run(Application.class, args);8
9 // 获取Mapper10 UserMapper userMapper = ctx.getBean(UserMapper.class);11
12 // 测试13 List<User> users = userMapper.selectList(null);14 System.out.println("users = " + users);15 }16}
41public interface BaseMapper<T> extends Mapper<T> {2
3 // 增删改查...4}
381// 测试查询2public static void test(UserMapper userMapper) {3 // 1. 通过ID查询(必须标注@Tableid注解)4 userMapper.selectById(1L);5
6 // 2. 通过ID批量查询(必须标注@Tableid注解)7 userMapper.selectBatchIds(Arrays.asList(1, 2, 3));8
9 // 3. 查询所有数据10 userMapper.selectList(null);11
12 // 4. 通过Map条件查询13 userMapper.selectByMap(new HashMap<String, Object>() {{14 put("age", 23);15 }});16}17
18// 测试增删改19public static void test(UserMapper userMapper) {20 // 需增删改的用户21 User user = new User(21L, "21客户", 23, "2030201403@qq.com");22
23 // 1. 插入24 userMapper.insert(user);25
26 // 2. 修改27 user.setAge(30);28 userMapper.updateById(user); // 通过ID修改(必须标注@Tableid注解)29
30 // 3. 删除31 userMapper.deleteById(user.getId()); // 通过ID删除(必须标注@Tableid注解)32 userMapper.deleteById(user); // 通过实体中的ID删除(必须标注@Tableid注解)33 userMapper.deleteBatchIds(Arrays.asList(1, 2, 3)); // 通过ID批量删除(必须标注@Tableid注解)34 userMapper.deleteByMap(new HashMap<String, Object>() {{ // 通过Map条件删除(必须标注@Tableid注解)35 put("age", 23);36 }});37}38
51public interface IService<T> {2
3 // 批量操作、合并操作...4}5
241// Service接口2public interface UserService extends IService<User> {3}4
5// Service实现67public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {8}9
10// Service测试11public static void test(UserService userService) {12 // 1. count查询13 long count = userService.count();14
15 // 2. 批量插入16 boolean saveBatch = userService.saveBatch(new ArrayList<User>() {17 {18 add(new User(101L, "101客户", 23, "2030201403@qq.com"));19 add(new User(102L, "102客户", 23, "2030201403@qq.com"));20 add(new User(103L, "103客户", 23, "2030201403@qq.com"));21 }22 });23}24
231public static void test(UserMapper userMapper) {2
3 // 1. 指定WHERE条件4 String name = "Xxx";5 List<User> userList = userMapper.selectList(new QueryWrapper<User>()6 .like(StringUtils.hasText(name), "name", name) // 根据用户是否输入来构造7 .between("age", 1, 30)8 .orderByDesc("id"));9
10 // 2. 指定查询列11 List<Map<String, Object>> userMapList = userMapper.selectMaps(new QueryWrapper<User>()12 .select("id", "name", "age"));13
14 // 3. 支持子查询15 List<User> userListByInSql = userMapper.selectList(new QueryWrapper<User>()16 .inSql("id", "select id from user where id < 30"));17
18 // 4. Lambda形式19 List<User> userListByLambda = userMapper.selectList(new LambdaQueryWrapper<User>()20 .like(StringUtils.hasText(name), User::getName, name)21 .between(User::getAge, 1, 30)22 .orderByDesc(User::getId));23}注意:
在 JDK8 版本,需要正确填写 QueryWrapper 的泛型参数,不可省略,否则编译会报错。
151public static void test(UserMapper userMapper) {2
3 // 1. 复杂条件更新4 int rows = userMapper.update(null, new UpdateWrapper<User>()5 .set("name", "hyx01")6 .eq("id", 1L)7 .isNotNull("email"));8
9
10 // 2. Lambda形式11 int rowsByLambda = userMapper.update(null, new LambdaUpdateWrapper<User>()12 .set(User::getName, "hyx01")13 .eq(User::getId, 1L)14 .isNotNull(User::getEmail));15}
512("t_user") // 标注实体类对应的表名3public class User {4 // field...5}提示:
也可通过
mybatis-plus.global-confi.db-config.table-prefix=t_来指定默认表前缀。
712public class User {3 ("id") // 标注表主键4 private Long id;5 6 // others field...7}注意:
MyBatis-Plus 默认将
id作为主键列,并在插入数据时,基于雪花算法的策略生成 id 值。MyBatis-Plus 不支持联合主键,需通过二次增强框架 MyBatis-Plus-Plus 实现。
主键生成策略支持雪花算法:
IdType.ASSIGN_ID和数据库自增:IdType.AUTO等。
1212public class User {3 ("id")4 private Long id;5 ("username") // 标注表字段6 private String name;7 8 // others field...9 // 同名或可兼容字段无需标注10 private Integer age;11 private String email;12}
注意:
驼峰与下划线风格的字段名与属性名可以相互兼容,例如属性 userName 可以兼容字段 user_name 。
812public class User {3 // others field...4 5 // 配置逻辑删除字段(0-正常记录 1-已删除记录)6 7 private Integer isDeleted;8}
812public class User {3 // others field...4
5 // 配置版本号字段,以支持乐观锁修改,防止并发问题6 7 private Integer version;8}注意:
该功能需要注册乐观锁插件:
OptimisticLockerInnerInterceptor。
1212public class MyBatisPlusConfig {3 4 public MybatisPlusInterceptor mybatisPlusInterceptor() {5
6 // 注册MyBatisPlus分页拦截器7 MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();8 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));9 10 return interceptor;11 }12}
171public static void test(UserMapper userMapper) {2
3 // 1. 简单分页查询4 Page<User> page = new Page<>(1, 5);5 userMapper.selectPage(page, null);6
7 // 2. 获取分页数据8 List<User> list = page.getRecords();9 list.forEach(System.out::println);10 System.out.println("当前页:" + page.getCurrent());11 System.out.println("每页显示的条数:" + page.getSize());12 System.out.println("总记录数:" + page.getTotal());13 System.out.println("总页数:" + page.getPages());14 System.out.println("是否有上一页:" + page.hasPrevious());15 System.out.println("是否有下一页:" + page.hasNext());16
17}
按 MyBatis 的方式编写 Mapper 接口,只需在第一个参数传递@Param("page") Page<User> page:
1412public interface UserMapper extends BaseMapper<User> {3
4 /**5 * 根据年龄查询用户列表,分页显示6 *7 * @param page 分页对象,xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位8 * @param age 年龄9 * @return10 */11 Page<User> selectPageVo(("page") Page<User> page, ("age") Integer age);12
13}14
XML配置无需特殊修改:
31<select id="selectPageVo" resultType="org.example.model.User">2 SELECT * FROM user WHERE age > #{age}3</select>查询时会自动根据分页参数进行分页处理:
151public static void test2(UserMapper userMapper) {2 // 1. XML分页查询3 Page<User> page = new Page<>(1, 5);4 userMapper.selectPageVo(page, 21);5
6 // 2. 获取分页数据7 List<User> list = page.getRecords();8 list.forEach(System.out::println);9 System.out.println("当前页:" + page.getCurrent());10 System.out.println("每页显示的条数:" + page.getSize());11 System.out.println("总记录数:" + page.getTotal());12 System.out.println("总页数:" + page.getPages());13 System.out.println("是否有上一页:" + page.hasPrevious());14 System.out.println("是否有下一页:" + page.hasNext());15}
一种通过 @EnumValue 注解标识枚举属性值,并在插入和查询时进行映射的方式,不推荐。