MyBatis
MyBatis
1. 介绍
MyBatis最初是Apache的一个开源项目iBatis, 2010年6月这个项目由Apache Software Foundation迁移到了Google Code。随着开发团队转投Google Code旗下,iBatis3.x正式更名为MyBatis。代码于2013年11月迁移到GithubiBatis一词来源于internet和abatis的组合,是一个基于Java的持久层框架。框架包括SQL Maps和Data Access Objects(DAO)是支持定制化
SQL、存储过程以及高级映射的优秀的持久层框架避免了几乎所有的
JDBC代码和手动设置参数以获取结果集可以使用简单的
XML或注解用于配置和原始映射,将接口和java的普通数据对象POJO映射成数据库中的记录是一个半自动的
ORM(Object Relation Mapping)框架与其他java持久化技术的区别
JDBC和代码耦合度高,维护不易且实际开发需求中SQL有变化,频繁修改的情况多见Hibernate和JPA简便,开发效率高,程序中的长难复杂SQL需要绕过框架,内部自动生产的SQL,不容易做特殊优化,大量字段映射到POJO会比较困难MyBatis轻量级,性能出色,SQL和Java编码分开,开发效率稍逊于Hibernate
2. 入门
2.1 引入依赖
在
pom.xml中添加相关依赖<dependencies> <!-- MyBatis 本体 --> <dependency> <groupId>org.MyBatis</groupId> <artifactId>MyBatis</artifactId> <version>3.5.19</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>9.3.0</version> </dependency> <!-- Junit 测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> </dependency> <!-- log4j 日志 --> <!-- log4j --> <!-- <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> --> <!-- log4j2,2.17之前存在漏洞 --> <!-- <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j</artifactId> <version>2.24.3</version> </dependency> --> <!-- 部分引入 --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.24.3</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.24.3</version> </dependency> </dependencies>
2.2 配置文件
2.2.1 创建与导入
- 创建配置文件
通过
MyBatis配置:比如命名为MyBatis-config.xml,并添加到src/main/resources目录下springboot整合:如果使用springboot整合,则一般不需要创建额外配置文件,整合到springboot的配置文件application.ymlspringboot导入:如果有复杂的原生配置,比如插件等,可以分离到外部xml文件中通过MyBatis配置<?xml version="1.0" encoding="UTF-8" ?> <!-- 配置文件约束,描述mapper是xml的根标签 --> <!DOCTYPE configuration PUBLIC "-//MyBatis.org//DTD Config 3.0//EN" "http://MyBatis.org/dtd/MyBatis-3-config.dtd"> <configuration> <!-- 连接环境,标明默认使用development环境 --> <environments default="development"> <!-- 数据库连接环境,可以有多个,id表示环境的唯一标识 --> <environment id="development"> <!-- 事务管理方式 JDBC:使用JDBC方式管理,要求所有事务手动提交 MANAGED:表明使用spring或其他的声明式事务管理方式管理 --> <transactionManager type="JDBC" /> <!-- 数据源配置 POOLED:使用数据库连接池缓存连接 UNPOOLED:不使用数据库连接池缓存连接 JNDI:使用上下文的数据源 --> <dataSource type="POOLED"> <!-- 驱动 --> <property name="driver" value="com.mysql.cj.jdbc.Driver" /> <!-- 数据库地址 --> <!-- xml中&需要转义,& --> <property name="url" value="jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=utf8" /> <!-- 用户名 --> <property name="username" value="root" /> <!-- 密码 --> <property name="password" value="123456" /> </dataSource> </environment> </environments> <mappers> <!-- 引入映射文件,定义了SQL操作 --> <mapper resource="mappers/UserMapper.xml" /> </mappers> </configuration>通过springboot整合spring: datasource: url: jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver MyBatis: mapper-locations: classpath:mapper/*.xml # XML映射文件位置 type-aliases-package: com.example.demo.entity # 实体类别名包 configuration: map-underscore-to-camel-case: true # 开启驼峰命名自动映射 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQLspringboot导入配置MyBatis: config-location: classpath:MyBatis-config.xml mapper-locations: classpath:mapper/*.xml
2.2.2 参数
在配置文件中参数之间有顺序要求,依次是
properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?properties:引入其他的配置文件,在其他文件中管理配置通过配置文件管理数据库配置# 为了防止属性名称引入时冲突,建议添加文件名前缀,比如jdbc.* jdbc.driverClassName=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=123456<!-- 在MyBatis配置文件中引入其他配置文件 --> <!-- 引入对应名称的配置文件 --> <properties resource="jdbc.properties"></properties> <!-- 用于创建数据库配置 --> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"></transactionManager> <dataSource type="POOLED"> <!-- 使用${}方式读取对应属性 --> <property name="driver" value="${jdbc.driverClassName}"></property> <property name="url" value="${jdbc.url}"></property> <property name="username" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> </dataSource> </environment> </environments>:::
settings:设置MyBatis全局参数,比如日志打印,自动映射驼峰命名等,所有配置可以从官方文档中查看<settings> <!-- 将下划线自动映射为ToCamelCase,也就是查询结果中 user_id -> UserId --> <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 配置日志,如果使用slf4j,则需要配置slf4j --> <setting name="logImpl" value="SLF4J"/> <!-- 开启延迟加载 --> <setting name="lazyLoadingEnabled" value="true"/> <!-- 避免在访问对象任何方法时,加载所有的需要延迟加载的内容,3.4.1后的默认false --> <!-- <setting name="aggressiveLazyLoading" value="false"/> --> </settings>typeAliases:为类型设置别名,避免写冗长的全类名<typeAliases> <!-- 在映射文件中使用User可以指代com.pojo.User --> <!-- 别名不区分大小写,可以使用user或User,效果相同 --> <!-- alias可不设置,默认为类名,即User --> <typeAlias alias="User" type="com.pojo.User"></typeAlias> </typeAliases> <!-- 为包以内的所有类设置默认别名 --> <package name="com.pojo"></package> </typeAliases>plugins:插件,见插件environments:数据库环境,可以有多个,通过default指定默认的环境<!-- 连接环境,标明默认使用development环境 --> <environments default="development"> <!-- 数据库连接环境,可以有多个,id表示环境的唯一标识 --> <environment id="development"> <!-- 事务管理方式 JDBC:使用JDBC方式管理,要求所有事务手动提交 MANAGED:表明使用spring或其他的声明式事务管理方式管理 --> <transactionManager type="JDBC" /> <!-- 数据源配置 POOLED:使用数据库连接池缓存连接 UNPOOLED:不使用数据库连接池缓存连接 JNDI:使用上下文的数据源 --> <dataSource type="POOLED"> <!-- 驱动 --> <property name="driver" value="com.mysql.cj.jdbc.Driver" /> <!-- 数据库地址 --> <!-- &需要转义,& --> <property name="url" value="jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=utf8" /> <!-- 用户名 --> <property name="username" value="root" /> <!-- 密码 --> <property name="password" value="123456" /> </dataSource> </environment> </environments>mappers:引入映射文件,其中定义了SQL操作<mappers> <!-- 引入单个映射文件 --> <mapper resource="mapper/UserMapper.xml"></mapper> <!-- 引入一个文件夹中所有映射文件 --> <!-- 要求: 1. 映射接口所在的包名与映射文件所在resources中的目录一致 2. 映射文件名与映射接口名一致 --> <!-- 比如对应的接口为com.mapper.UserMapper,那么对应的映射文件为com/mapper/UserMapper.xml --> <package name="com.mapper"></package> </mappers>
2.3 映射
2.3.1 面向接口编程
MyBatis中数据库语句是放入xml映射文件中的,将java实体类和数据库中的表进行映射,也方便之后修改,使用接口使用这些语句MyBatis的mapper接口相当于是DAO,区别在于在MyBatis我们不需要提供实现类mapper是MyBatis的面向接口编程方法,里面的每个方法都会对应一个SQL语句- 具体的查询语句
xml编写见之后章节
对应的语句放在对应的映射文件中
- 推荐命名和对应的接口一致,放入
resources/mappers目录
- 推荐命名和对应的接口一致,放入
面向接口编程的两个一致
- 映射文件中的
namespace属性与接口全类名一致 - 映射文件中
SQL语句的id属性与接口方法名称一致
将语句映射成方法public interface UserMapper { /** * 获取用户信息 (通过id查询) * @param id 用户id * @return 用户信息 */ public User queryUserById(int id); /** * 插入一条数据 * @param user 用户信息 * @return 插入的行数 */ public int addUser(User user); }<xml version="1.0" encoding="UTF-8"> <!-- 配置文件约束,描述mapper是xml的根标签 --> <!DOCTYPE mapper PUBLIC "-//MyBatis.org//DTD Mapper 3.0//EN" "http://MyBatis.org/dtd/MyBatis-3-mapper.dtd"> <!-- 和映射接口对应 --> <mapper namespace="com.mapper.UserMapper"> <!-- 查询,对应方法 --> <select id="queryUserById" resultType="com.pojo.User"> select * from user where id = #{id} </select> <!-- 插入,对应方法 --> <insert id="addUser" parameterType="User"> insert into user(id, name, age) values(#{param1}, #{param2}, #{param3}) </insert> </mapper>- 映射文件中的
2.3.2 使用接口
- 使用接口的步骤
获取配置文件输入流等格式的配置数据
使用配置创建
SqlSessionFactoryBuilder对象使用构建类的
build方法创建SqlSessionFactory对象通过
openSession获取SqlSession对象sqlSession.getMapper方法获取接口实现类对象调用接口方法
使用的
JDBC事务管理方式要求所有事务手动提交,也可以在获取sqlSession时传入一个true,自动提交。sqlSession = sqlSessionFactory.openSession(true)
public class UserMapperTest {
@test
public void testQueryUserById() throws IOException {
// 获取配置文件输入流
InputStream inputStream = Resources.getResourceAsStream("MyBatis-config.xml");
// 创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 通过创建类的build方法创建工厂对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
// 获取SqlSession对象,打开会话,此方法支持一个参数,参数为true表示自动提交事务
SqlSession sqlSession = sqlSessionFactory.openSession();
// 获取UserMapper接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 插入数据
User user = new User(1, "张三", 18);
int insert = userMapper.addUser(user);
System.out.println(insert);
// 使用JDBC管理需要手动提交事务
sqlSession.commit();
// 调用查询方法
User user = userMapper.queryUserById(1);
System.out.println(user);
}
}2.4 集成其他功能
2.4.1 集成Log4j
MyBatis支持使用log4j进行日志记录,需要创建log4j配置文件log4j2<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="STDOUT" target="SYSTEM_OUT"> <PatternLayout pattern="%-5level %d{MM-dd HH:mm:ss,SSS} %msg (%F:%L) \n"/> </Console> </Appenders> <Loggers> <Logger name="java.sql" level="debug"> <AppenderRef ref="STDOUT"/> </Logger> <Logger name="org.apache.ibatis" level="info"> <AppenderRef ref="STDOUT"/> </Logger> <Root level="debug"> <AppenderRef ref="STDOUT"/> </Root> </Loggers> </Configuration>log4j<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> <appender name="STDOUT" class="org.apache.log4j.ConsoleAppender"> <param name="Encode" value="UTF-8" /> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{yyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n" /> </layout> </appender> <logger name="java.sql"> <level value="debug" /> </logger> <logger name="org.apache.ibatis"> <level value="info" /> </logger> <root> <level value="debug" /> <appender-ref ref="STDOUT" /> </root> </log4j:configuration>
2.4.2 集成logback
logback是slf4j日志门面的实现引入依赖
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.5.18</version> </dependency>创建logback.xml文件
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- 文件输出(可选) --> <!-- <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>logs/mybatis.log</file> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> --> <!-- 根日志 --> <root level="INFO"> <appender-ref ref="STDOUT"/> <appender-ref ref="FILE"/> </root> <!-- 打印 SQL 语句 + 参数 + 结果 --> <logger name="java.sql" level="DEBUG"/> <logger name="java.sql.ResultSet" level="TRACE"/> <logger name="com.example.mapper" level="TRACE"/> </configuration>在
MyBatis全局配置文件中配置使用slf4j<settings> <setting name="logImpl" value="SLF4J"/> </settings>
3. 查询
3.1 简单查询
3.1.1 创建查询
常见的简单查询方法:在映射
xml文件使用#{}或${}获取字段值- 通过某个字段查询:
#{}或${}中的名称可以任意,比如#{aaa}如果此字段为数组,会规定名称为
array;如果此字段为List,会规定名称为collection和List,推荐使用@Param("名称")在方法参数前命名,避免混淆 - 通过多个字段查询,每个字段都不是动态的(基本类型)
- 不使用注解
@Param,#{}或${}中的名称必须按param1,param2,param3...或arg0,arg1,arg2... - 使用注解
@Param,#{}或${}中的名称可以与函数声明的注解@Param中定义的参数名称一致(代替了arg的键),也可以用param1,param2,param3...
- 不使用注解
- 通过集合对象查询:
#{}或${}中的名称必须与集合的键名称一致 - 通过实体类对象查询:
#{}或${}中的名称必须与实体类属性名称一致(实际上是和get和set方法一致)
建议除了集合和实体类对象外,其他都使用
@Param定义名称${} 和 #{} 的区别
MyBatis中获取参数值的两种方式${}本质是使用字符串拼接查询语句的方式,若为字符串类型或日期类型的字段进行赋值时,需要手动加引号#{}本质使用PreparedStatement占位符赋值的方式,此时为字符串类型或日期类型的字段进行赋值时,会自动添加引号- 一般使用
#{},因为#{}可以自动添加引号,同时防止注入,但有的时候,不希望String类型的字段自动添加引号,比如使用变量设置表名,此时使用${}
- 通过某个字段查询:
public interface UserMapper {
/**
* 获取用户信息 (通过id查询)
* @param id 用户id
* @return 用户信息
*/
public User queryUserById(int id);
/**
* 获取用户信息 (筛选start-end岁)
* @param start 开始年龄
* @param end 结束年龄
* @return 用户信息
*/
public List<User> queryUserByAge(int start, int end);
// 参数命名
// public List<User> queryUserByAge(@Param("start") int start, @Param("end") int end);
/**
* 通过集合中的参数获取用户信息,包括用户名和密码
* @param Map<String, Object> 集合
* @return 用户信息
*/
public List<User> queryUserByParam(Map<String, Object> map);
/**
* 查询是否存在和User对应的用户
* @param user 用户信息
* @return 用户信息
*/
public User queryUserByUser(User user);
}<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.mapper.UserMapper">
<select id="queryUserById" resultType="com.pojo.User">
<!-- 如果方法只是单个值,这个值名称可以任意 -->
select * from user where id = #{aaa}
</select>
<select id="queryUserByAge" resultType="com.pojo.User">
<!-- 所有参数通过集合存储,通过键名获取
键名为 param1, param2, ... (也支持 arg0 等) -->
select * from user where age between #{param1} and #{param2}
<!-- 如果参数有命名,使用对应名称获取参数值 -->
<!-- select * from user where age between #{start} and #{end} -->
</select>
<select id="queryUserByParam" resultType="com.pojo.User">
<!-- 通过集合中的键参数获取用户信息 -->
<!-- map.put("name", "张三") -->
<!-- map.put("password", "123456") -->
select * from user where name = #{name} and password = #{password}
</select>
<select id="queryUserByUser" resultType="com.pojo.User">
<!-- 通过实体类对象获取用户信息 -->
<!-- 调用对应的 get 方法获取参数值,需要和 get方法命名一致 -->
select * from user where name = #{name} and password = #{password}
</select>
</mapper>3.1.2 查询返回值
- 查询返回值类型定义
returnType:提供查询返回值的类型,表明结果映射到哪个实体类中,实体类的属性命名需要和表中字段一致,或map-underscore-to-camel-case已启用,能通过修改命名方法自动转换- 如果是一行数据,可以使用实体类类型、实体类
List列表接收 - 如果是多行数据,需要使用实体类
List列表或map集合(使用@MapKey注解设置哪个列作为键,整行数据map作为值)接收 - 如果返回单行单列,可以使用其他的基本类型接收,比如
java.lang.Integer(有别名,Integer和int),可在官方文档查看所有别名
返回的数据还可以使用
map<string, Object>接收,代替实体类public interface UserMapper { // 使用实体类作为returnType,比如 com.pojo.User // <select id="queryUserById" resultType="com.pojo.User"> public User queryUserById(int id); // <select id="queryAllUser" resultType="com.pojo.User"> public List<User> queryAllUser(); // 使用基本类型作为returnType // <select id="queryUserCount" resultType="int"> public Integer queryUserCount(); // 使用map<string, Object>作为returnType // <select id="queryUserMapById" resultType="map"> public Map<String, Object> queryUserMapById(int id); // <select id="queryAllUserMap" resultType="map"> public List<Map<String, Object>> queryAllUserMap(); // 使用map<string, Object>接收所有的数据 // <select id="queryAllUserMap" resultType="map"> @MapKey("id") public Map<String, Object> queryAllUserMap(); }- 如果是一行数据,可以使用实体类类型、实体类
resultMap:设置自定义映射关系- 查询的字段名和实体属性名不一致,需要转换时需要使用
resultMap在xml中显式映射不一致也可以通过在语句中设置查询
AS别名来解决
<!-- 创建映射关系,在 mapper 标签内创建,type 是对应实体类 --> <!-- 如果使用resultMap,推荐将所有字段显式设置在这里 --> <resultMap id="userResultMap" type="com.pojo.User"> <!-- property 属性名 column 查询的字段列名 --> <!-- id 设置主键 --> <id property="id" column="id" /> <!-- result 设置非主键字段 --> <result property="username" column="username" /> <result property="password" column="password" /> <result property="email" column="email" /> </resultMap> <!-- 使用 useresultMap 映射结果 --> <!-- User getUserById(int id) --> <select id="getUserById" resultMap="UserResultMap"> select * from user where id = #{param1} </select>- 处理连接中的一对一或多对一关系也需要使用
resultMap显式映射,这种关系常见于获取一个实体对应的另一个实体
<!-- 如果使用resultMap,推荐将所有字段显式设置在这里 --> <!-- 方法一:级联属性赋值 --> <resultMap id="EmpAndDeptMapOne" type="com.pojo.Emp"> <!-- property 属性名 column 查询的字段列名 --> <!-- id 设置主键 --> <id property="id" column="id" /> <!-- result 设置非主键字段 --> <result property="empName" column="empname" /> <!-- 使用属性.属性方式处理连接关系 --> <result property="dept.deptId" column="dept_id" /> <result property="dept.deptName" column="dept_name" /> <result property="dept.deptLoc" column="dept_loc" /> </resultMap> <!-- 方法二:使用association处理连接关系 --> <resultMap id="empAndDeptMapTwo" type="com.pojo.Emp"> <id property="id" column="id" /> <result property="empName" column="empname" /> <!-- 使用association映射连接关系 --> <association property="dept" javaType="com.pojo.Dept"> <id property="deptId" column="dept_id" /> <result property="deptName" column="deptname" /> <result property="deptLoc" column="dept_loc" /> </association> </resultMap> <!-- 方法三:通过分步查询,避免部分多表连接 --> <resultMap id="empAndDeptResultMap" type="Emp"> <id property="id" column="id" /> <result property="empName" column="empname" /> <!-- 使用association连接新的查询,使用dept_id作为查询条件 --> <association property="dept" select="com.mapper.DeptMapper.getDeptById" column="dept_id"> </association> </resultMap> <!-- Emp getEmpAndDept(@Param("eid") Integer eid) --> <select id="EmpDeptMap" resultMap="EmpAndDeptMap"> select * from emp left join dept on emp.dept_id = dept.id where emp.id = #{eid} </select>分步查询的优点
可以实现延迟加载,就是访问到这个属性之后再获取数据,功能默认关闭,需要
- 在全局的
settings中设置lazyLoadingEnabled为true(延迟加载开关)、aggressiveLazyLoading(在调用此对象任意方法时强制获取所有数据,3.4.1之后默认关闭false) - 为
association添加fetchType属性,lazy延迟加载或eager立即加载
- 处理连接中的一对多关系也需要使用
resultMap,即一个实体需要获取相关的其他实体的列表
<!-- 方法一:使用collection处理连接关系 --> <resultMap id="empInDeptMap" type="com.pojo.Emp"> <id property="id" column="id" /> <result property="deptName" column="dept_name" /> <!-- 使用collection映射连接关系 --> <collection property="emps" ofType="com.pojo.Emp"> <id property="id" column="emp_id" /> <result property="empName" column="empname" /> </collection> </resultMap> <!-- 方法二:使用分步查询,避免部分多表连接 --> <resultMap id="deptAndEmpResultMap" type="com.pojo.Emp"> <id property="id" column="id" /> <result property="deptName" column="dept_name" /> <!-- 使用collection连接新的查询,使用dept_id作为查询条件 --> <collection property="emps" select="com.mapper.EmpMapper.getEmpByDeptId" column="dept_id"> <id property="id" column="id" /> <result property="empName" column="empname" /> </collection> </resultMap> <!-- Emp getDeptById(@Param("did") Integer did) --> <select id="getDeptById" resultMap="empInDeptMap"> select * from emp left join dept on emp.dept_id = dept.id where dept.id = #{did} </select>- 查询的字段名和实体属性名不一致,需要转换时需要使用
3.2 特殊查询
3.2.1 模糊查询
使用字符串模糊查询,需要注意
#{}方法是添加占位符完成注入的,'%#{param1}%'写法最终不能实现模糊查询效果<select id="queryUserLike" resultType="User"> <!-- 方法1:使用${}拼接 --> select * from user where username like '%${value}%' <!-- 方法2(推荐):如果数据库支持相邻字符串连接,使用#{}拼接 --> select * from user where username like '%'#{value}'%' <!-- 方法3:使用concat函数拼接 --> select * from user where username like concat('%',#{value},'%') </select>
3.2.2 动态创建sql
动态创建语句是
MyBatis的一种根据特定条件拼接语句的功能if标签:根据test中的条件表达式,决定标签内的内容是否需要拼接表达式中的条件和
java中的相同,由于&不能出现在xml中,所以添加了and代替&&<!-- List<Emp> getEmpsByCondition(Emp emp) --> <select id="getEmpsByCondition" resultType="Emp"> <!-- 1=1 防止 where 后直接为 and 报错 --> select * from emp where 1=1 <if test="empName != null and empName != ''"> and emp_name=#{empName} </if> <if test="gender != null and gender != ''"> and gender= #{gender} </if> <if test="email != null and email != ''"> and email= #{email} </if> </select>
Where标签:当标签内有内容时,会自动生成where语句,并且自动删除多余前缀的and和or<select id="getUser" resultType="User"> select * from user <where> <if test="id != null"> id= #{id} </if> <if test="username != null and username != ''"> and username = #{username} </if> <if test="password != null and password != ''"> and password = #{password} </if> </where> </select>trim标签:在内容中添加或删除某些前后缀,如果标签中没有内容,则不会有任何效果prefix:添加的前缀suffix:添加的后缀prefixOverrides:删除的前缀suffixOverrides:删除的后缀
支持删除中使用
|删除多个可选的前缀或后缀<select id="selectUser" resultType="User"> select * from user <!-- 使用 trim 拼接 where 条件,并删除多余的 AND 或 OR --> <trim prefix="where" prefixOverrides="and|or"> <if test="username != null">username = #{username} and</if> <if test="password != null">password = #{password} and</if> <if test="age != null">age = #{age}</if> </trim> </select>choose标签:相当于if {...} else if {...} else {...}when标签:第一个when相当于if,其他的when相当于else if,至少要有一个otherwise标签:相当于else,最多只能有一个<select id="selectUser" resultType="User"> SELECT * FROM user WHERE <choose> <when test="id != null">id = #{id}</when> <when test="username != null">username = #{username}</when> <otherwise>age = #{age}</otherwise> </choose> </select>
<foreach>标签:相当于for循环collection属性:指定遍历的Array或Listitem属性:表明遍历得到的内容如何称呼separator属性:指定循环的分隔符,会在分隔前后添加空格open属性:指定循环的开头close属性:指定循环的结尾<!-- int deleteUser(@Param("ids") int[] ids) --> <!-- 使用 in 实现批量删除 --> <delete id="deleteUser"> delete from user where id in <foreach item="id" collection="ids" open="(" close=")" separator=","> #{id} </foreach> </delete> <!-- 使用 where or 实现批量删除 --> <delete id="deleteUser"> delete from user where <foreach item="id" collection="ids" separator="or"> id = #{id} </foreach> </delete> <!-- int addUser(@Param("users") List<User> users); --> <insert id="addUser"> insert into user(username,password) values <foreach item="user" collection="users" separator=","> (#{user.username},#{user.password}) </foreach> </insert>
sql标签:将一段sql语句抽取出来,方便管理,使用时再进行拼接sql声明片段,并提供唯一的idinclude:引用sql片段,通过refid获取片段<sql id="userColumns"> id,username,password </sql> <select id="getUserById" resultType="User"> select <include refid="userColumns" /> from user where id = #{id}
4. 增加删除修改
4.1 增加
添加数据
单数据添加
批量数据添加
<insert id="addUser"> insert into user(username,password) values(#{username},#{password}) </insert> <!-- 批量添加数据 --> <insert id="addUserByList"> insert into user(username,password) values <foreach item="user" collection="users" separator=","> (#{user.username},#{user.password}) </foreach> </insert>public interface UserMapper { int addUser(User user); int addUserByList(List<User> users); }类型定义
增加、删除、修改操作支持定义
parameterType描述提供的参数类型,javaType描述表中字段的类型,但一般可以不写<insert id="addUser" parameterType="User"> insert into user(id, name, age) values(#{id, jdbcType=INTEGER}, #{name, jdbcType=VARCHAR}, #{age, jdbcType=INTEGER}) </insert>
获取自增的主键(主键回显):设置
useGeneratedKeys="true" keyProperty="id"将主键值填入User的id属性中<insert id="addUser" useGeneratedKeys="true" keyProperty="User"> insert into user(username,password) values(#{username},#{password}) </insert>public interface UserMapper { int addUser(User user); }
4.2 删除
- 删除数据
单数据删除
批量数据删除
<!-- 删除一条数据 --> <delete id="deleteUser"> delete from user where id = #{id} </delete> <!-- 批量删除数据,ids是字符串,内容例"1, 2, 3" --> <delete id="deleteSomeUser"> <!-- #{}会自动添加字符串引号,因此需要使用${} --> delete from user where id in (${ids}) </delete> <!-- 使用 in 实现批量删除 --> <delete id="deleteUserByArray"> delete from user where id in <foreach item="id" collection="ids" open="(" close=")" separator=","> #{id} </foreach> </delete>public interface UserMapper { public int deleteUser(int id); public int deleteSomeUser(string ids); public int deleteUserByArray(int[] ids); }
4.3 修改
修改数据
<update id="updateUser"> update user set username = #{param1},password = #{param2} where id = #{param3} </update>public interface UserMapper { public int updateUser(String username, String password, int id); }
5. 缓存
5.1 一级缓存
- 一级缓存:
MyBatis的一级缓存是默认开启的,通过一个session进行的相同查询会被MyBatis缓存(可重复读),一级缓存失效的情况:- 不同的
session进行查询 - 同一个
session进行查询,但两次查询的条件不同 - 同一个
session进行的两次查询中间,此session执行了任何一次增加、删除和修改操作 - 同一个
session进行的两次查询中间,执行了session.clearCache()方法清空了缓存
- 不同的
5.2 二级缓存
二级缓存:
MyBatis的二级缓存是指通过一个sqlSessionFactory创建的sqlSession对象查询时缓存查询结果,其他session查询相同的语句时,会从缓存中获取结果,从而实现缓存共享开启条件:
设置核心配置文件,全局配置属性
cacheEnabled=true,默认为true,不需要设置在映射文件中设置标签
<cache>(在mapper标签内)二级缓存在
Session关闭或提交后有效(查询操作不会触发自动提交,仅查询需要关闭才生效)查询的数据必须实现
Serializable接口,需要通过序列化接口拷贝缓存数据序列化接口
序列化接口本身没有任何方法,只是用于标识接口,告诉
JVM这个类的对象允许序列化(内存字节拷贝进行深度克隆),拷贝和clone等方法无关,并且对任意复杂引用都有效,此方法简单但速度更慢// org.apache.ibatis.cache.impl.PerpetualCache private Map<Object, Object> cache = new HashMap<>(); public void putObject(Object key, Object value) { // 值先序列化成字节数组,再反序列化回来 cache.put(key, deepCopy(value)); } private Object deepCopy(Object obj) { // 把对象写进内存流,再读出来 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(obj); // 序列化 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); // 反序列化 -> 全新对象 }
失效情况:两次查询之间存在增加、删除和修改操作会清除当前
Mapper映射文件的缓存,缓存范围最好和数据更新范围保持一致,避免脏读在
cache可以设置二级缓存的配置,支持设置:eviction属性:设置缓存回收的策略条件LRU:最近最少使用,默认FIFO:先进先出SOFT:移除基于垃圾收集器和软引用规则的对象WAKE:更积极移除基于垃圾收集器和弱引用规则的对象
flushInterval属性:设置缓存失效的间隔时间,单位毫秒,默认无失效,仅仅在调用增加、修改和删除方法时才会缓存失效size属性:设置缓存中最多保存的记录数readOnly属性:设置缓存是否只读,只读缓存不允许修改其中的返回内容,有性能优势,默认为falsetype属性:设置使用的缓存类型,由于接入其他的二级缓存机制
<mapper> <cache eviction="LRU" flushInterval="600000" size="1024"></cache> </mapper>
缓存查询顺序
- 先查询二级缓存,如果没有再查询一级缓存
- 如果一级缓存也没有,则查询数据库,并记录一级缓存
- 在session关闭或提交后,将一级缓存中的数据写入二级缓存
5.3 替换二级缓存
默认使用的是内部实现的二级缓存,如果希望和其他的缓存整合,可以设置
cache的type属性,比如使用EHCache<mapper> <cache type="org.mybatis.caches.ehcache.EhcacheCache"/> </mapper>
5.3.1 EHCache
添加依赖
<!-- EHCache --> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.3.0</version> </dependency> <!-- 需要使用支持 slf4j 的一个日志实现记录日志,比如 logback --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.5.18</version> </dependency>设置
EHCache缓存全局配置文件,见EHCache<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"> <!-- 磁盘缓存路径 --> <diskStore path="/tmpdir/mybatis-cache"/> <defaultCache maxElementsInMemory="1000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="true"/> </ehcache>在映射文件中设置缓存类型
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
6 逆向工程
- 正向工程:先创建
java类,再根据类创建数据库表 - 逆向工程:先创建数据库表,由框架负责管理,再根据表创建需要的
java类
MyBatis可以让框架访问数据库表,并根据表生成java实体类、Mapper接口和Mapper映射文件添加插件:
<!-- 添加Mybatis等项目依赖 --> <dependencies> <!-- ... --> </dependencies> <!-- 控制Maven在构建过程中相关配置 --> <build> <!-- 构建过程中用到的插件 --> <plugins> <!-- 具体插件,逆向工程的操作是以构建过程中插件形式出现的 --> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.0</version> <!-- 插件的依赖 --> <dependencies> <!-- 逆向工程的核心依赖 --> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.2</version> </dependency> <!-- 数据库连接池 --> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.2</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>9.3.0</version> </dependency> </dependencies> </plugin> </plugins> </build>添加配置文件,必须为
generatorConfig.xml,并添加在resources目录下<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!-- targetRuntime: 执行生成的逆向工程的版本 MyBatis3Simple: 生成基本的CRUD MyBatis3: 生成带条件的CRUD --> <context id="DB2Tables" targetRuntime="MyBatis3Simple"> <!-- 数据库的连接信息 --> <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/mybatis" userId="root" password="123456"> </jdbcConnection> <!-- javaBean的生成策略--> <javaModelGenerator targetPackage="com.atguigu.mybatis.bean" targetProject=".\src\main\java"> <!-- 以 . 分隔为子文件夹 --> <property name="enableSubPackages" value="true" /> <property name="trimStrings" value="true" /> </javaModelGenerator> <!-- SQL映射文件的生成策略 --> <sqlMapGenerator targetPackage="com.atguigu.mybatis.mapper" targetProject=".\src\main\resources"> <property name="enableSubPackages" value="true" /> </sqlMapGenerator> <!-- Mapper接口的生成策略 --> <javaClientGenerator type="XMLMAPPER" targetPackage="com.atguigu.mybatis.mapper" targetProject=".\src\main\java"> <property name="enableSubPackages" value="true" /> </javaClientGenerator> <!-- 逆向分析的表 --> <!-- tableName设置为*号,可以对应所有表,此时不写domainObjectName --> <!-- domainObjectName属性指定生成出来的实体类的类名 --> <!-- 列名中的下划线命名会被转换成驼峰命名法 --> <table tableName="t_emp" domainObjectName="Emp"/> <table tableName="t_dept" domainObjectName="Dept"/> </context> </generatorConfiguration>执行插件
mybatis-generator如果后续需要更新,建议删除原有的生成文件,避免文件追加有错误
效果说明
使用
MyBatis3Simple生成的Mapper接口中包含简单的增删改查方法使用
MyBatis3生成的结果中,创建了新的Example对象,可以使用createCriteria()方法创建查询条件,此方法返回值有andNameEqualTo()等方法设置查询条件。Mapper接口中包含大量的方法。以selective结尾的是多字段的条件语句,略过参数为null的列;以Example结尾的是以Example对象进行查询语句,传入对应对象进行操作;其他的语句参数都是必须的,如果参数中有null,也会放入语句中public class EmpTest { @test public void testMyBatisGen() { InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); SqlSession sqlSession = new SqlSessionFactoryBuilder().build(is).openSession(true); EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); EmpExample empExample = new EmpExample(); //创建条件对象,通过andXXX方法为SQL添加查询添加,每个条件之间是and关系 empExample.createCriteria().andEnameLike("a").andAgeGreaterThan(20).andDidIsNotNull(); //将之前添加的条件通过or拼接其他条件 empExample.or().andSexEqualTo("男"); List<Emp> list = mapper.selectByExample(empExample); for (Emp emp : list) { System.out.println(emp); } Emp newEmp = new Emp(1, "小王", 23, "男", null, 1); // null 值不更新 mapper.updateByPrimaryKeySelective(emp); // 更新所有字段 mapper.updateByPrimaryKey(emp); } }
7. MyBatis插件
7.1 分页
插件:
com.github.pagehelper:pagehelper-spring-boot-starter添加依赖
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>6.1.0</version> </dependency>引入插件
<plugins> <plugin interceptor="com.github.pagehelper.PageHelper"> </plugin> </plugins>使用
public class PageHelperTest { @Test public void testPageHelp() throws Exception { InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); SqlSession sqlSession = new SqlSessionFactoryBuilder().build(is).openSession(true); EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); // 在查询之前添加,参数1 为页码,参数2 为每页显示的条数 // page 对象中包含page信息和之后查询的结果 Page<Object> page = PageHelper.startPage(1, 5); // 最多查询5条数据,查询之前会先查询总数 List<Emp> empList = mapper.selectByExample(null); empList.forEach(emp -> System.out.println(emp)); // 获取分页相关的数据,5表示导航栏需要5个附近的页数 (包括当前页) PageInfo<Emp> pageInfo = new PageInfo<>(empList, 5); System.out.println(pageInfo); // pageNum:当前页的页码 // pageSize:每页显示的条数 // size:当前页显示的真实条数 // total:总记录数 // pages:总页数 // prePage:上一页的页码 // nextPage:下一页的页码 // isFirstPage/isLastPage:是否为第一页/最后一页 // hasPreviousPage/hasNextPage:是否存在上一页/下一页 // navigatePages:导航分页的页码数 // navigatepageNums:导航分页的页码,[1,2,3,4,5] // list:page对象 } }
8. 执行原理
8.1 sql语句执行
- 创建实现类:通过
MapperProxy代理,判断是否有对应类的实现,如果没有则创建并存储对应的实现类 - 区分方法类型:实现类中判断语句的类型,在此过程中判断的是
sqlCommand(包含语句类型type和全类名.执行函数名name)- 如果是查询类型,判断返回值类型,执行对应的查询方法
- 如果通过
@Param注解设置了参数名,会使用设置参数名,会转换方法参数名
- 获取参数
map:ParamNameResolver初始化时获取参数名称和对应的值,并将结果存储到Map中,如果是map类型和实体类类型,也处理成Map- 初始化变量时,获取参数字典
names - 没有参数,返回的map是null
- 有一个参数且没有注解,通过
wrapToMapIfCollection处理成Map类型 - 其他情况,遍历存储了所有参数的字典
names(键是arg1或@Param设置的参数名,值是参数值),并额外拼接param+(index + 1)作为键,放入map中
- 初始化变量时,获取参数字典
- 执行方法:调用
sqlSession的不同方法执行,使用获取到的参数map
