大家好,又见面了,我是你们的朋友全栈君。
菜鸟的mybatis实战教程
- 说明
- 零、mybatis对应xml文件模板
- 一、mybatis简介
- 二、mybatis简单入门
- (1)数据库创建
- (2)创建springboot项目
- (3)增删改查操作
- (4)总结
- 三、mybatis标签详解
- (1)增删查改
- < select >
- < insert>
- < update>
- < delete>
- (2)< resultMap>
- 数据库表和实体类映射详解
- (3)动态sql
- < if>
- < where>
- < set>
- < choose>
- (4)统计分组
- (1)增删查改
- 四、分页查询
- 五、复杂查询
- (1)一对多查询
- (2)多对一查询
- (3)多对多查询
- (4)< collection>和< association>
- 六、批量增删查改
- (1)批量查询用户
- (2)批量删除用户
- (3)批量插入用户
- (4)批量更新用户
- 七、mybatis底层原理分析
- (1)mybatis涉及到的核心类
- (2)SqlSessionFactory 的创建
- (3)SqlSession的创建
- (4)JDK动态代理映射Mapper
- (5)总结
说明
更新时间:2020/6/11 00:09,更新了mybatis的底层原理分析 更新时间:2020/5/26 22:26,更新了批量增删查改 更新时间:2020/5/23 00:03,更新了分页查询和复杂查询 更新时间:2020/5/21 22:48,更新了标签详解
对应文件:springboot_mybatis1
之前学习了mybatis之后没有做记录,在前几天做一个题库系统时,刚好要用到mybatis,因为之前做东西时用的jpa,mybatis没怎么去用,导致有些知识点想不起来,在使用mybatis出现了一些低级的错误,现将mybatis的学习笔记记录在这里,以便日后查看,本文会持续更新,不断地扩充
本文仅为记录学习轨迹,如有侵权,联系删除
零、mybatis对应xml文件模板
代码语言:javascript复制<?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="zsc.dao.IUserDao">
<!-- 配置查询结果的列名和实体类的属性名的对应关系 -->
<resultMap id="userMap" type="zsc.domain.User">
<!-- 主键字段的对应 -->
<id property="userId" column="id"></id>
<!-- 非主键字段的对应 -->
<result property="userName" column="username"></result>
<result property="userAddress" column="address"></result>
<result property="userSex" column="sex"></result>
<result property="userBirthday" column="birthday"></result>
</resultMap>
<!--查询所有用户-->
<select id="findAll" resultMap="userMap">
select * from user;
</select>
<!-- 保存用户 -->
<insert id="saveUser" parameterType="zsc.domain.User">
<!-- 配置插入操作后,获取插入的数据id -->
<selectKey keyProperty="userId" keyColumn="id" resultType="int" order="AFTER">
select last_insert_id();
</selectKey>
insert into user (username,address,sex,birthday) values(#{userName},#{userAddress},#{userSex},#{userBirthday});
</insert>
<!-- 更新用户 -->
<update id="updateUser" parameterType="zsc.domain.User">
update user set username=#{userName},address=#{userAddress},sex=#{userSex},birthday=#{userBirthday} where
id=#{userId};
</update>
<!-- 删除用户 -->
<delete id="deleteUser" parameterType="int">
delete from user where id = #{uid};
</delete>
<!-- 根据id查找用户 -->
<select id="findById" parameterType="int" resultMap="userMap">
select * from user where id = #{uid};
</select>
<!-- 根据用户名称模糊查找 -->
<select id="findByName" resultMap="userMap" parameterType="String">
select * from user where username like #{name};
</select>
<!-- 查找总用户数 -->
<select id="findTotal" resultType="int">
select count(id) from user;
</select>
<!-- 根据queryVo的条件查询用户 -->
<select id="findUserByVo" parameterType="zsc.domain.QueryVo" resultMap="userMap">
select * from user where username like #{user.userName}
</select>
</mapper>
一、mybatis简介
MyBatis是一流的持久性框架,支持自定义SQL,存储过程和高级映射。MyBatis消除了几乎所有的JDBC代码以及参数的手动设置和结果检索。MyBatis可以使用简单的XML或注释进行配置,并将图元,映射接口和Java POJO(普通的旧Java对象)映射到数据库记录。
个人理解mybatis就是在JDBC的基础上做了一层封装,具体概念可以参考mybatis官网:mybatis官网链接
二、mybatis简单入门
对于mybatis的入门,个人是觉得比较简单的,只要实现了对数据库的CURD操作,基本就可以说是入门了,这里用springboot mybatis的一个简单例子作为入门案例。
(1)数据库创建
代码语言:javascript复制--注意:先创建一个数据库,再执行下面的语句
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名称',
`birthday` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '生日',
`sex` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '性别',
`address` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 51 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, '灰太狼', '2020-05-20 21:04:20', '男', '狼堡');
INSERT INTO `user` VALUES (2, '喜羊羊', '2020-05-31 21:03:50', '男', '羊村');
INSERT INTO `user` VALUES (3, '美羊羊', '2020-05-23 21:04:41', '女', '羊村');
INSERThuanc INTO `user` VALUES (4, '懒羊羊', '2020-05-20 21:05:08', '男', '羊村');
INSERT INTO ch`user` VALUES (5, '潇洒哥', '2019-12-11 21:05:50', '男', '古古怪界');
INSERT INTO `user` VALUES (6, '黑大帅', '2019-11-01 21:06:18', '男', '古古怪界');
创建好的数据库如图:
(2)创建springboot项目
引入如下依赖,下面是本人使用mybatis时常用的一些依赖,像数据源springboot有自己的默认的数据源,但本人更喜欢用Druid这个数据源
代码语言:javascript复制 <!-- mybatis-spring-boot-starter :整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!--Druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<!--log4j:日志框架,建议引入-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
配置文件(application.yml)
代码语言:javascript复制#参考链接:https://www.cnblogs.com/hellokuangshen/p/12497041.html
#参考链接:https://www.cnblogs.com/hellokuangshen/p/12503200.html
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test4?userSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT+8
password: 123
username: root
#切换为druid数据源
type: com.alibaba.druid.pool.DruidDataSource
#Spring Boot 默认是不注入这些属性值的,需要自己绑定
#druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
#需要导入log4j依赖
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
#整合mybatis
#type-aliases-package:要扫描的实体类(与数据库表对应)的位置
#mapper-locations:写sql语句的地方,与待会要写持久层接口对应
mybatis:
type-aliases-package: com.zsc.domain
mapper-locations: classpath:mybatis/mapper/*.xml
(3)增删改查操作
创建实体类
编写持久层接口
编写sql语句
注意:这里有个重点,如果实体类的成员变量名称与数据表的字段名称一模一样的话可以不做数据库表和实体类的映射,mybatis会自动根据字段封装数据,如果有些字段跟数据库表字段名称不一致,则需要做数据库表和实体类的映射,否则会封装不上数据,这里建议把映射都做上去。
关于数据库表和实体类的映射
代码语言:javascript复制<!-- id是自己起的一个别名,type为要映射的实体类--> <resultMap id = "userMap" type = "com.zsc.domain.User">
<!-- <id></id>标签是主键的映射,<result></result>是非主键的字段映射--> <id column="id" property="id"></id> <result column="username" property="userName"></result> <result column="birthday" property="birthday"></result> <result column="sex" property="sex"></result> <result column="address" property="address"></result>
</resultMap>
运行测试
测试查询所有数据,其余测试就不在这里展示
(4)总结
代码语言:javascript复制步骤:
(1)导入相关依赖
(2)创建与数据库表一致的实体类,类的成员变量名字尽量与表字段名称一致,如果不一致,则需要做数据表和实体类的映射(在xml文件配置),这里建议每次都做一下映射
(3)创建持久层接口(UserMapper)
(4)创建与持久层接口对应的配置文件(xml文件),在里面写sql语句,实现增删改查
注意:文件的创建要根据application.yml里面的配置来创建
三、mybatis标签详解
所有的标签
最常用<insert>,<delete>,<select>,<update>四个标签,分别对应增删查改功能,
(1)增删查改
增删查改主要对应<insert>,<delete>,<select>,<update>四个标签
实体类
数据库表和实体类映射
< select >
属性 | 描述 |
---|---|
id | 在命名空间中唯一的标识符,可以被用来引用这条语句。 |
parameterType | 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler) 推断出具体传入语句的参数,默认值为未设置(unset)。 |
parameterMap | 这是引用外部 parameterMap 的已经被废弃的方法。请使用内联参数映射和 parameterType 属性。 |
resultType | 从这条语句中返回的期望类型的类的完全限定名或别名。 注意如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身。可以使用 resultType 或 resultMap,但不能同时使用。 |
resultMap | 外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂映射的情形都能迎刃而解。可以使用 resultMap 或 resultType,但不能同时使用。 |
flushCache | 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。 |
useCache | 将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。 |
timeout | 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖驱动)。 |
fetchSize | 这是一个给驱动的提示,尝试让驱动程序每次批量返回的结果行数和这个设置值相等。 默认值为未设置(unset)(依赖驱动)。 |
statementType | STATEMENT,PREPARED 或 CALLABLE 中的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。 |
resultSetType | FORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset) 中的一个,默认值为 unset (依赖驱动)。 |
databaseId | 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。 |
resultOrdered | 这个设置仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。 这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。 |
resultSets | 这个设置仅对多结果集的情况适用。它将列出语句执行后返回的结果集并给每个结果集一个名称,名称是逗号分隔的。 |
返回一般数据类型(Integrt,String类型等)
返回javabean(实体类)
返回List类型
返回值是 Map<String,Object>(返回一个用户时)
返回值Map<Integer,User>(返回多个用户时)
< insert>
< insert>,< delete>,< update>这三个标签的属性基本类似的
属性 | 描述 |
---|---|
id | 在命名空间中唯一的标识符,可以被用来引用这条语句。 |
parameterType | 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler) 推断出具体传入语句的参数,默认值为未设置(unset)。 |
parameterMap | 这是引用外部 parameterMap 的已经被废弃的方法。请使用内联参数映射和 parameterType 属性。 |
resultType | 从这条语句中返回的期望类型的类的完全限定名或别名。 注意如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身。可以使用 resultType 或 resultMap,但不能同时使用。 |
resultMap | 外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂映射的情形都能迎刃而解。可以使用 resultMap 或 resultType,但不能同时使用。 |
flushCache | 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。 |
useCache | 将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。 |
timeout | 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖驱动)。 |
fetchSize | 这是一个给驱动的提示,尝试让驱动程序每次批量返回的结果行数和这个设置值相等。 默认值为未设置(unset)(依赖驱动)。 |
statementType | STATEMENT,PREPARED 或 CALLABLE 中的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。 |
resultSetType | FORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset) 中的一个,默认值为 unset (依赖驱动)。 |
databaseId | 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。 |
resultOrdered | 这个设置仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。 这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。 |
resultSets | 这个设置仅对多结果集的情况适用。它将列出语句执行后返回的结果集并给每个结果集一个名称,名称是逗号分隔的。 |
< update>
< delete>
(2)< resultMap>
< resultMap>最简单的使用方式是用来做数据库表与实体类的映射
属性 | 描述 |
---|---|
property | 需要映射到JavaBean 的属性名称。 |
column | 数据表的列名或者标签别名。 |
javaType | 一个完整的类名,或者是一个类型别名。如果你匹配的是一个JavaBean,那MyBatis 通常会自行检测到。然后,如果你是要映射到一个HashMap,那你需要指定javaType 要达到的目的。 |
jdbcType | 数据表支持的类型列表。这个属性只在insert,update 或delete 的时候针对允许空的列有用。JDBC 需要这项,但MyBatis 不需要。如果你是直接针对JDBC 编码,且有允许空的列,而你要指定这项。 |
typeHandler | 使用这个属性可以覆写类型处理器。这项值可以是一个完整的类名,也可以是一个类型别名。 |
例如上面的例子:
代码语言:javascript复制<!-- id是自己起的一个别名,type为要映射的实体类-->
<resultMap id = "userMap" type = "com.zsc.domain.User">
<!-- <id></id>标签是主键的映射,<result></result>是非主键的字段映射-->
<id column="id" property="id"></id>
<result column="username" property="userName"></result>
<result column="birthday" property="birthday"></result>
<result column="sex" property="sex"></result>
<result column="address" property="address"></result>
</resultMap>
数据库表和实体类映射详解
上面的例子一对多查询说白了就是在User上增加了一个List< article>用于存储一对多存放的用户发表的多篇文章,重点要做其实就是数据库表和该User类的映射,这是重中之重。
在做数据库表和实体类的映射映射值大多数人都认为< resultMap>里面的相关标签的property属性对应实体类的成员变量名,名字必须一致,而column属性对应数据库表的字段,而且名字必须与数据库表的字段一致, 但实际上column属性只是在做映射时起的一个别名而已。
例如
关于< collection>标签,该标签主要用于“一对多”时的映射,例如List< article>的映射,这里主要有property属性和ofType属性,property对应实体类的属性,ofType对应哪一个实体类,< collection>标签里面的id标签和result标签跟上面讲的一样。
(3)动态sql
< if>
简单的条件判断
< where>
主要是用来简化sql语句中where条件判断的,能智能的处理 and or ,不必担心多余导致语法错误
< set>
set 标签是用在更新操作的时候,功能和 where 标签元素差不多,主要是在包含的语句前输出一个 set,然后如果包含的语句是以逗号结束的话将会把该逗号忽略,如果 set 标签最终返回的内容为空的话则可能会出错(update table where id=1)
< choose>
有些时候,我们不想用所有的条件语句,而只想从中择其一二。针对这种情况,MyBatis提供了choose元素,它有点像Java中的switch语句。下面的例子,先看第一个条件userName查找用户,如果满足改条件,就直接跳出choose,只选择这一个条件,其余条件都不看,如果最后所有条件都不满足,就采用< otherwise>的条件。
(4)统计分组
mybatis的统计分组个人用起来有点搞不懂,查了很多别人的很多博文,感觉也没那么方便,好像mybatis plus统计分组功能比较方便,其实不过不考虑性能的话,可以有很多的方式进行统计分组,比如将查到的数据直接用java自己处理,或者分多步骤查询数据库等,如果要考虑性能的话,有一种封装成map的方式,虽然也要自己做一下处理。
数据库user表
持久层接口(mapper或dao,这里是UserMapper)
代码语言:javascript复制//通过性别对数据进行分组
List<HashMap<String,Object>> listUserGroupBySex();
UserMapper.xml
代码语言:javascript复制 <select id="listUserGroupBySex" resultType="java.util.HashMap">
select sex as 'key',count(*) as 'value' from user GROUP BY sex;
</select>
运行结果
将查询到的数据转化为map
代码语言:javascript复制@Test
public void listUserGroupBySexTest() {
List<HashMap<String, Object>> list = userMapper.listUserGroupBySex();
list.forEach(System.out::println);
Map<String,Integer> mapBySex = new HashMap<>();//最终结果
//将查询到的List转换为map
if (list != null && !list.isEmpty()) {
for (HashMap<String, Object> hashMap : list) {
String key = null;
Integer value = null;
for (Map.Entry<String, Object> entry : hashMap.entrySet()) {
if ("key".equals(entry.getKey())) {
key = (String) entry.getValue();
} else if ("value".equals(entry.getKey())) {
//我需要的是int型所以做了如下转换,实际上返回的object应为Long。
value = ((Long)entry.getValue()).intValue();
}
}
mapBySex.put(key, value);
}
}
mapBySex.keySet().forEach(key-> System.out.println(key ":" mapBySex.get(key)));
}
最终处理结果
四、分页查询
mybatis自己没有集成分页工具,需要借助第三方插件,或者自己sql分页,这里直接借助第三方插件(PageHelp)
导入maven依赖:
代码语言:javascript复制<!--只配置当前这一个依赖 PageHelper不生效。-->
<!-- mybatis pager -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.10</version>
</dependency>
<!--还需要把这个依赖添加到pom当中 自动排序和分页就好使了-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-autoconfigure</artifactId>
<version>1.2.5</version>
</dependency>
application.yml配置
代码语言:javascript复制#分页插件
pagehelper:
helperDialect: mysql # 根据数据库配置
reasonable: true
supportMethodsArguments: true
params: count=countSql
配置后直接使用
持久层的查询语句不要以;结尾
注意这里有个两个重点: (1)只有紧接着PageHelper.startPage(pageNum,5); 后的那句sql才会进行分页,再下一句sql则不分页。 (2)持久层的查询语句结尾不要有任何类似“;”的结束符号,不然后面分页查拼接sql会报错
前端代码
代码语言:javascript复制<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div align="center">
<table border="1">
<tr>
<th>id</th>
<th>name</th>
<th>birthday</th>
<th>sex</th>
<th>address</th>
</tr>
<tr th:each="user:${pageInfo.getList()}">
<td th:text="${user.getId()}"></td>
<td th:text="${user.getUserName()}"></td>
<td th:text="${user.getBirthday()}"></td>
<td th:text="${user.getSex()}"></td>
<td th:text="${user.getAddress()}"></td>
</tr>
</table>
<p>当前 <span th:text="${pageInfo.pageNum}"></span> 页,总 <span th:text="${pageInfo.pages}"></span> 页,共 <span th:text="${pageInfo.total}"></span> 条记录</p>
<a th:href="@{/pageHelp}">首页</a>
<a th:href="@{/pageHelp(pageNum=${pageInfo.hasPreviousPage}?${pageInfo.prePage}:1)}">上一页</a>
<a th:href="@{/pageHelp(pageNum=${pageInfo.hasNextPage}?${pageInfo.nextPage}:${pageInfo.pages})}">下一页</a>
<a th:href="@{/pageHelp(pageNum=${pageInfo.pages})}">尾页</a>
</div>
</body>
</html>
分页结果
给出PageHelp常用参数
PageInfo.list | 结果集 |
---|---|
PageInfo.pageNum | 当前页码 |
PageInfo.pageSize | 当前页面显示的数据条目 |
PageInfo.pages | 总页数 |
PageInfo.total | 数据的总条目数 |
PageInfo.prePage | 上一页 |
PageInfo.nextPage | 下一页 |
PageInfo.isFirstPage | 是否为第一页 |
PageInfo.isLastPage | 是否为最后一页 |
PageInfo.hasPreviousPage | 是否有上一页 |
PageHelper.hasNextPage | 是否有下一页 |
五、复杂查询
(1)一对多查询
给出用户表 user 和文章表 article ,一个用户可以发表多篇文章,一篇文章只属于一个用户。
给定一个场景:要求查询一个用户的信息以及该用户发表的所有文章
实体类
代码语言:javascript复制import java.util.Date;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
//注意:这里的字段尽量跟数据库表的字段名一致
//当然,也可以不一致,后面需要做实体类和数据库表的映射
private Integer id;
private String userName;
private Date birthday;
private String sex;
private String address;
private List<Article> articles;//1个用户有多篇文章
}
持久层接口
代码语言:javascript复制/** * 项目的持久层,相当于dao层 */
//@Mapper : 表示本类是一个 MyBatis 的 Mapper
@Mapper
@Repository
public interface UserMapper {
//1对多的测试
List<User> listUserOneToMany(Integer id);
}
对应的UserMapper.xml文件(重点)
代码语言:javascript复制<?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.zsc.mapper.UserMapper">
<!-- id是自己起的一个别名,type为要映射的实体类-->
<resultMap id = "userMap" type = "com.zsc.domain.User">
<!-- <id></id>标签是主键的映射,<result></result>是非主键的字段映射-->
<id property="id" column="id" ></id>
<result property="userName" column="username"></result>
<result property="birthday" column="birthday"></result>
<result property="sex" column="sex"></result>
<result property="address" column="address"></result>
<!-- collection 是用于建立一对多中集合属性的对应关系 ofType 用于指定集合元素的数据类型 -->
<!--这里有个重点,column只能有一个id,不然查询数据时会出现只查询到一条数据的情况-->
<collection property="articles" ofType="com.zsc.domain.Article" column="user_id">
<!--记住一点,<collection>标签里的column并不是与数据库表字段对应,而且自己起的一个别名,
将数据库查询到的数据映射或者说是赋值给这个别名,再油这个别名映射到property对应的实体类属性上-->
<id property="id" column="aid"></id><!--column = "adi"是自己起的一个别名,防止有多个id-->
<result property="articleTitle" column="article_title"></result>
<result property="articleContent" column="article_content"></result>
<result property="userId" column="user_id"></result>
</collection>
</resultMap>
<!--1对多的测试-->
<select id="listUserOneToMany" resultMap="userMap">
SELECT
u.id,
u.username,
u.birthday,
u.sex,
u.address,
a.id as aid,
a.article_title,
a.article_content,
a.user_id
from user u,article a
where u.id = a.user_id and u.id = #{
id};
</select>
</mapper>
注意:有个重点,一对多的情况,要映射List的时候,采用collection表签,且用ofType属性,property固定写法,虽然一对多还有其他的查询方式,但如果时采用这种查方式建议采用上面的方法。
(2)多对一查询
同样给出用户表 user 和文章表 article ,一个用户可以发表多篇文章,一篇文章只属于一个用户。
给定一个场景:要求查询一篇文章的信息以及该文章的作者
这次实体类的主角就应该文章类了
代码语言:javascript复制import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Article {
private Integer id;
private String articleTitle;
private String articleContent;
private Integer userId;
private User user;//1篇文章只能属于一个用户
}
持久层接口
代码语言:javascript复制import java.util.List;
/** * 项目的持久层,相当于dao层 */
//@Mapper : 表示本类是一个 MyBatis 的 Mapper
@Mapper
@Repository
public interface ArticleMapper {
List<Article> listArticle(Integer id);
}
对应的ArticleMapper.xml文件(重点)
代码语言:javascript复制<?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.zsc.mapper.ArticleMapper">
<!-- id是自己起的一个别名,type为要映射的实体类-->
<resultMap id = "articleMap" type = "com.zsc.domain.Article">
<!-- <id></id>标签是主键的映射,<result></result>是非主键的字段映射-->
<id property="id" column="id"></id>
<result property="articleTitle" column="article_title"></result>
<result property="articleContent" column="article_content"></result>
<result property="userId" column="user_id"></result>
<!-- collection 是用于建立一对多中集合属性的对应关系 ofType 用于指定集合元素的数据类型 -->
<!--这里有个重点,column只能有一个id,不然查询数据时会出现只查询到一条数据的情况-->
<association property="user" javaType="com.zsc.domain.User">
<id property="id" column="uid"></id>
<!--记住一点,<collection>标签里的column并不是与数据库表字段对应,而且自己起的一个别名,
将数据库查询到的数据映射或者说是赋值给这个别名,再油这个别名映射到property对应的实体类属性上-->
<result property="userName" column="username"></result>
<result property="birthday" column="birthday"></result>
<result property="sex" column="sex"></result>
<result property="address" column="address"></result>
</association>
</resultMap>
<select id="listArticle" resultMap="articleMap">
SELECT
a.id,
a.article_title,
a.article_content,
a.user_id,
u.id as uid,
u.username,
u.birthday,
u.sex,
u.address
from user u,article a
where u.id = a.user_id and a.id = #{
id};
</select>
</mapper>
对于多对一的关系,跟一对多的区别,这里指代码的写法上,就是文章类的成员变量有一个private User user;
而用户类则是一个List,所以,这里主要处理Article类里面的成员变量user的映射,这里的映射不是采用collection标签,而是采用association标签,property对应实体类属性,javaType对应哪一个实体类。
注意:这里的映射不是采用collection标签,而是采用association标签,property对应实体类属性,javaType对应哪一个实体类。
查询结果
(3)多对多查询
多对多查询在mybatis里实现的方式跟一对多的方式差不多,只不过是要涉及到3张表的联合查询,同样要处理List,下面给出案例。
要求查询一条评论的所有信息包括谁发过这条评论
实体类
代码语言:javascript复制import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Comment {
private Integer id;
private String commentContent;
private List<User> userList;//1条评论可以属于多个用户
}
持久层接口
代码语言:javascript复制import java.util.List;
/** * 项目的持久层,相当于dao层 */
//@Mapper : 表示本类是一个 MyBatis 的 Mapper
@Mapper
@Repository
public interface CommentMapper {
List<Comment> listComment(Integer id);
}
持久层对应的CommentMapper.xml(重点)
代码语言:javascript复制<?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.zsc.mapper.CommentMapper">
<!-- id是自己起的一个别名,type为要映射的实体类-->
<resultMap id = "commentMap" type = "com.zsc.domain.Comment">
<id property="id" column="id"></id>
<result property="commentContent" column="comment_content"></result>
<!-- collection 是用于建立一对多中集合属性的对应关系 ofType 用于指定集合元素的数据类型 -->
<!--这里有个重点,column只能有一个id,不然查询数据时会出现只查询到一条数据的情况-->
<collection property="userList" ofType="com.zsc.domain.User">
<id property="id" column="uid"></id>
<!--记住一点,<collection>标签里的column并不是与数据库表字段对应,而且自己起的一个别名,
将数据库查询到的数据映射或者说是赋值给这个别名,再油这个别名映射到property对应的实体类属性上-->
<result property="userName" column="username"></result>
<result property="birthday" column="birthday"></result>
<result property="sex" column="sex"></result>
<result property="address" column="address"></result>
</collection>
</resultMap>
<select id="listComment" resultMap="commentMap">
select * from user u,comment c,user_comment uc
where u.id = uc.user_id
AND c.id = uc.comment_id
AND uc.comment_id = #{
id}
</select>
</mapper>
这里的查询涉及到3个表的查询
查询结果
同理也可以查询User表,因为是多对多的关系,所以在user类中需要添加private List<Comment> commentList;
,然后持久层对应的UserMapper.xml文件在做一个List的映射,进行3表联立的查询即可
修改后的实体类
代码语言:javascript复制@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
//注意:这里的字段尽量跟数据库表的字段名一致
//当然,也可以不一致,后面需要做实体类和数据库表的映射
private Integer id;
private String userName;
private Date birthday;
private String sex;
private String address;
private List<Article> articles;//1个用户有多篇文章
private List<Comment> commentList;//1个用户有多个评论
}
持久层接口
代码语言:javascript复制 //多对多的测试
List<User> listUserManyToMany(Integer id);
对应的UserMapper.xml及运行结果
(4)< collection>和< association>
其实不论是一对多、多对一、还是多对多,都需要collection标签或是association标签来做实体类与数据库表的映射。 collection主要用来做如下相似情况的实体类的映射
即一个类里面,有一个List集合,集合里面的类型是自定义类型的情况,这时该类与数据库表的映射如下
其中List<自定义类型>的映射必须用collection标签,property属性对应自定义类的属性,ofType属性对应哪一个自定义类型
association主要用来做如下相似情况的实体类的映射
像这种类中,有一个自定义的User类的成员变量时,这时该类与数据库表的映射如下
其中Article类中的自定义类型(User user)的映射必须用association标签,property属性对应自定义类的属性,javaType属性对应哪一个自定义类型
六、批量增删查改
在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况 下,该属性的值是不一样的,主要有一下3种情况:
1.如果传入的是单参数且参数类型是一个List的时候,collection属性值为list 2.如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array 3.如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map
属性 | 描述 |
---|---|
collection | 表示迭代集合的名称,可以使用@Param注解指定,如下图所示 该参数为必选 |
item | 表示本次迭代获取的元素,若collection为List、Set或者数组,则表示其中的元素;若collection为map,则代表key-value的value,该参数为必选 |
open | 表示该语句以什么开始,最常用的是左括弧’(’,注意:mybatis会将该字符拼接到整体的sql语句之前,并且只拼接一次,该参数为可选项 |
close | 表示该语句以什么结束,最常用的是右括弧’)’,注意:mybatis会将该字符拼接到整体的sql语句之后,该参数为可选项 |
separator | mybatis会在每次迭代后给sql语句append上separator属性指定的字符,该参数为可选项 |
index | 在list、Set和数组中,index表示当前迭代的位置,在map中,index代指是元素的key,该参数是可选项。 |
(1)批量查询用户
(2)批量删除用户
(3)批量插入用户
(4)批量更新用户
关于mybatis的批量更新个人觉得是比较让人恶心的,经过查询,发现目前主要有两种方式,一种是通过接收传进来的参数list进行循环着组装sql,这种跟自己在java中用for循环一条一条插入是一样的,另一种是通过 case when语句变相的进行批量更新,基于效率的考虑,建议采用第二种方式。
第一种方式,这里采用别的博主的代码,这种方式有一个重点就是需要在db链接url后面带一个参数 &allowMultiQueries=true,这个很重要,具体如下。
代码语言:javascript复制 <!-- 批量更新第一种方法,通过接收传进来的参数list进行循环着组装sql -->
<update id="updateBatch" parameterType="java.util.List" >
<foreach collection="list" item="item" index="index" open="" close="" separator=";">
update standard_relation
<set >
<if test="item.standardFromUuid != null" >
standard_from_uuid = #{
item.standardFromUuid,jdbcType=VARCHAR},
</if>
<if test="item.standardToUuid != null" >
standard_to_uuid = #{
item.standardToUuid,jdbcType=VARCHAR},
</if>
<if test="item.gmtModified != null" >
gmt_modified = #{
item.gmtModified,jdbcType=TIMESTAMP},
</if>
</set>
where id = #{
item.id,jdbcType=BIGINT}
</foreach>
</update>
第二种是个人建议用的,因为个人觉得第一种的方式还不如在java代码中用for循环实现,原理一样的,而且效率还低。所以个人喜欢第二种。
代码语言:javascript复制<!--通过foreach批量更新用户-->
<update id="updateUserByList">
update user
<trim prefix="set" suffixOverrides=",">
<trim prefix="username = case" suffix="end,"><!--对应userMap中的column属性-->
<foreach collection="list" item="item" index="i">
<if test="item.userName != null">
when id = #{
item.id} then #{
item.userName}
</if>
</foreach>
</trim>
<trim prefix="birthday = case" suffix="end,"><!--对应userMap中的column属性-->
<foreach collection="list" item="item" index="i">
<if test="item.birthday != null">
when id = #{
item.id} then #{
item.birthday}
</if>
</foreach>
</trim>
<trim prefix="sex = case" suffix="end,"><!--对应userMap中的column属性-->
<foreach collection="list" item="item" index="i">
<if test="item.sex != null">
when id = #{
item.id} then #{
item.sex}
</if>
</foreach>
</trim>
<trim prefix="address = case" suffix="end,"><!--对应userMap中的column属性-->
<foreach collection="list" item="item" index="i">
<if test="item.address != null">
when id = #{
item.id} then #{
item.address}
</if>
</foreach>
</trim>
</trim>
where
<foreach collection="list" separator="or" item="item" index="i">
id = #{
item.id}
</foreach>
</update>
七、mybatis底层原理分析
一直想尝试着去解读mybatis源码,想弄清楚它的底层原理,在之前学习这个框架的时候就有看过一些mybatis底层原理分析的相关视频与文章,当时学完也没有记录整个运行的流程,现在重新学习并记录一下mybatis的底层原理,下面是自己对于mybatis底层原理的个人理解。
首先给出mybatis的整体的运行流程
从图中就可以知道mybatis底层的大致运行原理,每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的, SqlSessionFactory再通过生成SqlSession,让SqlSession通过底层提供的Executor(执行器)执行sql,最后返回结果,整个过程看起来很简单,但实际上上面的图省略了很多东西,像SqlSessionFactory的构建,mapper文件的获取等都没有描述出来,除此之外还有很多底层的细节没有展示出来,为了能看到这些底层东西,可以通过一个简单的demo,用debug的方式来进入底层的代码进行解读。
这个是按照mybatis官网的快速入门的xml方式的一个查询用户的demo,在这个demo中,我已经将整个完整的步骤都标注好了,一共偶7个步骤,就可以查询到所有用户
(1)mybatis涉及到的核心类
类名 | 职责 |
---|---|
SqlSession | 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能 |
Executor | MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的 维护 |
StatementHandler | 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合 |
ParameterHandler | 负责对用户传递的参数转换成JDBC Statement 所需要的参数 |
ResultSetHandler | 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合 |
TypeHandler | 负责java数据类型和jdbc数据类型之间的映射和转换 |
MappedStatement | MappedStatement维护了一条<select |
SqlSource | 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回 |
BoundSql | 表示动态生成的SQL语句以及相应的参数信息 |
Configuration | MyBatis所有的配置信息都维持在Configuration对象之中 |
(2)SqlSessionFactory 的创建
注意:SqlSessionFactory 的创建可以理解为配置文件的初始化,即将配置文件配置的数据拿到,这里会将拿到的数据封装到它内部的Configuration对象中
首先是SqlSessionFactory 的创建,可以看到SqlSessionFactory是由SqlSessionFactoryBuilder的build方法生成的,build方法需要传入一个配置文件,这个配置文件记录了数据库连接的相关配置,然后build方法会去解析配置文件
XMLConfigBuilder会在内部初始化Configuration对象,Configuration对象用来存储解析的xml数据,我们可以在XMLConfigBuilder中的parse()方法完成对配置文件数据的读取并封装到Configuration对象的内部属性中
到这里SqlSessionFactory的创建基本差不多结束了
(3)SqlSession的创建
SqlSession是由SqlSessionFactory的创建的,就像是工厂生产产品一样,SqlSessionFactory可以生产多个SqlSession,SqlSession 的实例不是线程安全的,因此是不能被共享的
打上断点进行debug,如下图首先进入openSession()方法,再进入openSessionFromDataSource方法,并且经过中间一系列的调用,初始化了执行器,是否开启事务,并最终返回DefaultSqlSession
关于上图其实有很多其余的东西可以去探究的,第一是SqlSession接口,它有两个实现类:DefaultSqlSession(默认)和SqlSessionManager(弃用,不做介绍),可以看到其实SqlSession对象的获取,返回的也是DefaultSqlSession
然后是执行器,执行器可以获取sqlSession对象时设置.,Mybatis 共有三种执行器
执行器 | 说明 |
---|---|
SIMPLE | 默认的执行器, 对每条sql进行预编译->设置参数->执行等操作 |
BATCH | 批量执行器, 对相同sql进行一次预编译, 然后设置参数, 最后统一执行操作 |
REUSE | REUSE 执行器会重用预处理语句 |
(4)JDK动态代理映射Mapper
在使用了这么久的mybatis的这段时间,不知道大家有没有这样一个疑问,我们只是声明了一个接口XXXMapper,没有编写任何实现类,Mybatis就能返回接口实例,并调用接口方法返回数据库数据,看起来很神奇,这是为什么呢?这就是下面要解决的问题
用debug的方式进入可以看到mapperRegistry.getMapper(type, sqlSession)这个方法,这里涉及到一个类MapperRegistry,从名字就可以看出来MapperRegistry 类是用来注册Mapper接口与获取代理类实例的工具类,那么,它是如何注册接口和获取代理类实例的呢,用debug的方式进入源码如下图
MapperRegistry类主要有两个变量,一个是全局的配置文件,一个是Map,用于存储MapperProxyFactory对象,MapperProxyFactory是创建Mapper代理对象的工厂 ,在MapperRegistry中主要做两件事,接口的注册与代理实例类的获取,步骤如下: (1)先执行addMapper函数,也就是先注册Mapper接口到一个map里面,以Mapper接口的type为key,MapperProxyFactory为value。 (2)然后在getMapper的时候,找到Mapper接口类型所对应的MapperProxyFactory对象,然后执行MapperProxyFactory对象的newInstance(SqlSession)函数。
MapperRegistry的getMapper方法里面最后会去调用MapperProxyFactory类的newInstance方法,MapperProxyFactory是创建Mapper代理对象的工厂,在MapperProxyFactroy类中有如下两个方法,newInstance(SqlSession sqlSession) 则表示创建了MapperProxy对象,MapperProxy实现了JDK动态代理的接口,InvocationHandler
代码语言:javascript复制 @SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
//创建了一个代理类并返回
//关于Proxy的API 可以查看java官方的API
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] {
mapperInterface }, mapperProxy);
}
//在这里传入sqlSession 创建一个Mapper接口的代理类
public T newInstance(SqlSession sqlSession) {
//在这里创建了MapperProxy对象 这个类实现了JDK的动态代理接口 InvocationHandler
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
//调用上面的方法 返回一个接口的代理类
return newInstance(mapperProxy);
}
上述代码中核心代码就是
这句代码创建了MapperProxy对象,这个类实现了JDK的动态代理接口 InvocationHandler,并将MapperProxy对象传给newInstance(mapperProxy)返回一个接口的代理类
到这里基本久可以解决一开始的问题了,为什么可以调用一个没有实现方法的接口XXXMapper并且返回数据,实际上这个接口通过jdk的动态代理,生成了一个代理类,并且实现了接口里面的方法,所以才可以调用该接口并且返回数据
那么重点就是实现了jdk动态代理的类在哪里,这就需要进入MapperProxy这个代理对象的类,SqlSession调用执行器进行CURD操作都封装在这个类中了
在上面的图中,如果学过代理模式的话,应该知道这个,就是在这个方法里面对被代理对象进行方法增强,图中进行了一个小小的封装,把增强的方法都封装在这个类中cachedInvoker(method).invoke(proxy, method, args, sqlSession);并且调用了这个类中的方法,就是下图这个方法
看到这里让人恍然大悟,原来SqlSession调用执行器执行CURD操作都封装在了这里,点进去就可以看到执行器的CURD操作的代码了,SqlSession的增删改查都是在MapperMethod类里面处理的
可以看到第一个进去会先调用getType()方法判断增删改查类型,然后再调用execute方法,通过增删改查类型进行相应操作
MapperMethod类里面有俩个成员:SqlCommand类和MethodSignature类。从名字上我们大概的能想到一个可能跟SQL语句有关系,一个可能跟要执行的方法有关系。这里重点有一个类SqlCommand,上面代码使用一个内部类SqlCommand来封装底层的增删改查操作,确切来讲这一部分的内容跟XxxMapper的XML配置文件里面的select节点、delete节点等有关。我们都会知道节点上有id属性值。那么MyBatis框架会把每一个节点(如:select节点、delete节点)生成一个MappedStatement类。要找到MappedStatement类就必须通过id来获得。有一个细节要注意:代码用到的id = 当前接口类 XML文件的节点的ID属性。
MapperMethod类可以自己点进去看,这里就不再展开。
(5)总结
整个流程分析下来,如果单从整体来看的话,理解起来其实不难,但如果深入到里面的细节的话,就有些复杂了,上面的分析也只是简单的看了一下源码,并没有多深入,尤其是jdk动态代理那一块,我在debug的时候发现它会进入到jdk内部的代码,一些代码自己也没怎么看懂,上面展示的那部分mybatis的源码自己也就能大概看懂,部分深入一点的自己也是看不懂,总之还得继续学习吧,下面做个总结
这个流程图是基于自己的理解画的原理图,关于mybatis自己还会不停的学习,随时会更新学到的新知识。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/146018.html原文链接:https://javaforall.cn