一.概述
Mybatis是一个持久层框架,用来简化Java程序对数据库的操作。它的本质是一个ORM(对象关系映射)框架,但是它是半自动,可控的,更加适合需要些SQL的场景。
其核心功能主要用来帮你把Java对象和数据库中的数据之间进行映射转换。
- 写好的SQL(在XML或者注解中)
- Mybatis通过配置把SQL语句和java方法关联起来
- Mybatis帮助你把参数传给SQL,把执行的结果封装成对象返回
所谓持久层:就是用于程序中的数据持久化(保存)到数据库中,以及从数据库读取数据的那一层的代码。
大多数的Java web项目都采用三层架构
- 表现层(Controller) - 接收请求,返回响应
- 业务层(Service) - 编写业务逻辑
- 持久层(DAO/Mapper)- 负责数据库操作
二.入门
1.创建一个springboot工程
引入Mybatis的依赖
我们在创建好的项目中点开我们的pom.xml,我们会看到我们导入的依赖
之后我们去resources目录下,创建一个application.yml的文件(注意名字和后缀一定要是这个,而且一定要在这个目录下)
原因: Spring Boot 默认会自动加载 resources 目录下的 application.yml 或者是application.properties,用来配置例如:数据库连接,端口号,日志等级,自定义配置等等的信息
# 数据库连接配置
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis_study?characterEncoding=utf8&useSSL=false
username: root
password:
driver-class-name: com.mysql.cj.jdbc.Driver
#这里的用户名和密码一定要写自己的用户名和密码
2.数据准备
创建一个数据库并且在数据库中创建我们的用户表,并且插入一些数据。
drop database if exists mybatis_test;
create database mybatis_test default character set utf8mb4;
create table `userinfo`(
`id` int(11) not null auto_increment,
`username` varchar(127) not null,
`password` varchar(127) not null,
`age` tinyint(4) not null ,
`gender` tinyint(4) default '0' comment '1-男 2-女 0-默认',
`phone` varchar(15) default null,
`delete_flag` tinyint(4) default 0 comment '0-正常,1-删除',
`create_time` datetime default now(),
`update_time` datetime default now(),
primary key (`id`)
)engine = innodb default charset = utf8mb4;
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone) VALUES ( 'admin','admin',18,1,'18612340001');
INSERT INTO mybatis_test.userinfo(username, `password`, age, gender,phone ) VALUES('zhangsan','zhangsan',18,1,'18612340002');
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone ) VALUES ('lisi','lisi',18,1,'18612340003');
INSERT INTO mybatis_test.userinfo ( username,`password`, age, gender,phone ) VALUES ('wangwu','wangwu',18,1,'18612340004');
接着我们在刚刚创建好的项目的软件包下新创建两个软件包,分别是mapper和model,在mapper包下创建UserInfoMapper接口,在model包下创建UserInfo实体类
接着我们在UserInfoMapper中,右键
生成我们的测试类UserInfoMapperTest,并且在测试类上已经添加了注解 @SpringBootTest,代表该测试类已经与SpringBoot整合。
接着我们在UserInfoMapper接口下,添加注解@Mapper表示这个接口是一个 MyBatis 的 Mapper(即持久层接口),需要被扫描和注册为 Bean。
然后我们在测试类中声明一个对象private UserInfoMapper userInfoMapper;并且添加注解@Autowired
注意:@Autowired 是 Spring 提供的注解,用来实现 依赖注入(DI),也就是说它能自动把一个 Bean(组件)注入到你声明的变量中。
其作用是:自动从 Spring 容器中找到类型匹配的 Bean,并赋值给你定义的变量(比如 userInfoMapper)
3.编写SQL语句
我们在UserInfoMapper接口里面编写SQL语句,添加注解@Select,这个注解就代表着select查询,用于书写查询语句, 下面一行代码是方法,这个方法虽然看起来没有实现,但是实际上Mybatis在运行的时候会为这个接口自动生成代理对象
-
启动时注册Mapper接口,MyBatis读取每个接口中的注解(比如@Select),然后为接口生成代理类
-
调用queryUserList()方法时,实际上调用了MyBatis生成的代理对象的方法
生成的代理对象会拿到@Select里面的SQL语句,执行SQL,并且把返回的结果封装成List<UserInfo>类型的对象返回
然后我们生成一个测试方法,与生成测试类同样的步骤,但是要记得选中需要生成的测试方法是哪个,这里我们生成了queryUserList()的测试方法
点击方法旁边的运行我们可以得到控制台返回的执行结果
4.参数传递
Mybatis会把你传入的方法参数绑定到SQL中的占位符上。
4.1 单参数传递
我们同样在持久层接口UserInfoMapper中写SQL语句,然后生成测试方法
我们可以看到测试结果只返回了id=1的数据。
4.2 多个参数
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'userId' not found. Available parameters are [arg1, arg0, param1, param2]
当我们把这个SQL语句去测试的时候我们会发现测试失败,并且报错了。
原因是:Mybatis在没有使用额外注解(如 @Param) 的情况下,是无法正确识别你写的userId和deleteFlag应该映射到SQL中的哪个变量名字。而且Mybatis在没有使用额外注解@param的时候会自动给参数命名为:第一个参数:param1,第二个参数:param2。同时也支持arg0,arg1
所以我们的sql语句就有了多种写法。
第一种:我们使用额外注解@param
@Select("select * from userinfo where id = #{userId} and delete_flag =#{deleteFlag}")
UserInfo queryUserInfoByDF(@Param("userId") Integer userId,@Param("deleteFlag")Integer deleteFlag);
第二种:我们使用自动命名参数
@Select("select * from userinfo where id = #{param1} and delete_flag =#{param2}")
UserInfo queryUserInfoByDF(Integer userId,Integer deleteFlag);
两种方法在测试用例中均能正确执行,返回同样的结果,但是并不推荐使用第二种,因为这种命名方式,容易让人产生误解,不易于理解。
4.3 实体类传参
注意:我们用实体类传参的时候,#{} 里的字段名必须和实体类的属性名完全一致,否则会报错或查询失败。MyBatis 会根据你传入的对象,调用它的getter方法来获取值,所以 #{} 中的名字其实是实体类属性的名字(不是数据库字段的名字)。
5.增删改查
我们在application.yml中配置Mybatis的日志打印,配置如下
mybatis:
configuration: # 配置打印 MyBatis日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
5.1 新增
我们使用了对象来进行参数的传递
- 如果我们没有使用@param进行重命名(上述写法),直接使用属性名就可以了
- 如果我们使用了注解进行重名,那么我们就要使用对象.属性名来获取参数了
@Insert("insert into userinfo(username,`password`,age,gender,phone) " +
"values(#{userInfo.username},#{userInfo.password},#{userInfo.age},#{userInfo.gender},#{userInfo.phone})")
Integer insertByParam(@Param("userInfo") UserInfo userInfo);
5.2 删除
@Delete("delete from userinfo where id = #{id}")
Integer delete(@Param("id") Integer id);
@Test
void delete() {
userInfoMapper.delete(7);
}
5.3 修改
修改我们这里写两种传参方式,多参数传参以及实体类传参。
第一种:多参数传参
@Update("update userinfo set password = #{password} where id = #{id}")
Integer update(@Param("password") String password,@Param("id") Integer id);
@Test
void update() {
userInfoMapper.update("zhaoliu",6);
}
我们可以看一下数据库是否发生改变
第二种:实体类传参
@Update("update userinfo set username = #{username},password = #{password},age = #{age} where id = #{id}")
Integer updateByOb(UserInfo userInfo);
@Test
void updateByOb() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("zhaoliu666666");
userInfo.setPassword("1234566666666");
userInfo.setAge(99);
userInfo.setId(6);
userInfoMapper.updateByOb(userInfo);
}
在这里我们进行一个对比,当我们把userInfo.setId(6);注销前后对于程序运行的成败,以及数据库的变化有没有影响。
我们把选择setId注销掉之后,代码能正常运行,但是我们在运行日志可以看到,这样的代码虽然可以运行成功,但是update=0,表示数据库中0条内容被更新了。
5.4 查询
我们重新回到查询语句观察一下控制台打印的日志
我们发现Mysql返回的结果中后三个字段都有返回值,但是通过Mybatis查询返回的结果中,我们发先后面三个字段均返回NULL空值,而且我们发现后三个字段的值也和数据库定义的字段值不一样。正是因为存在不一样的字段,导致不能够映射成功。
MyBatis 在映射查询结果时,会根据字段名自动匹配实体类的属性名,映射规则如下:
查询结果中的字段名 ↔ 实体类中的属性名
如果不一致,就会映射失败,返回 null
解决方法有三种:
第一种,我们通过改别名成功映射,我们对之前的代码进行一些修改
@Select("select id,username,`password`,age,gender,phone," +
"delete_flag as deleteFlag,create_time as createTime,update_time as updateTime from userinfo")
List<UserInfo> queryUserList();
第二种,通过注解来映射
@Results(id = "BaseResult",value = {
@Result(column = "delete_flag",property = "deleteFlag"),
@Result(column = "create_time",property = "createTime"),
@Result(column = "update_time",property = "updateTime")
})
@ResultMap(value = "BaseResult")
@Select("select * from userinfo where id = #{param1} and delete_flag = #{param2}")
UserInfo queryUserInfoByDF(Integer userId,Integer deleteFlag);
@Results用于定义结果映射规则,通常在@Select注解的方法上面使用,或者结合@ResultMap复用已定义的映射
@Result 定义单个字段的映射关系:
property:Java 对象的属性名
column:数据库查询结果的列名
@ResultMap 用于复用已定义的 @Results 映射,避免重复编写相同的映射规则。
第三种,配置制动驼峰转换(在application.yml中)
map-underscore-to-camel-case: true #配置驼峰自动转换 eg:delete_flag <-> deleteFlag
以上三种方式的mybatis的返回结果与mysql的返回结果一致。
6.Mybatis的XML的实现
6.1实现流程
- 引入mybatis依赖(这个我们在创建项目的时候就已经引入了)
- 配置数据库(已经进行过配置了在application.yml文件中)
- 配置mapper的路径
- 使用xml实现
首先我们要创建一个接口在mapper包下,名称为UserInfoXmlMapper,并且添加注解@mapper以及生成测试类UserInfoXmlMapperTest,并在测试类中添加注解,并且创建UserInfoXmlMapper对象。
然后再resources软件包下创建mapper软件包,在里面创建UserInfoXmlMapper.xml,在该文件中我们需要写入如下配置
<?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.mybatis.mybatis_study.mapper.UserInfoXmlMapper">
</mapper>
需要注意的是,namespace中的路径必须是你UserInfoMapperXml的路径
上述步骤都完成之后就可以使用xml文件来进行实现了。
6.2查询
我们在UserInfoXmlMapper中首先先声明一个方法
List<UserInfo> queryUserList();
然后我们可以使用ALT+SHIFT快捷键生成对应的xml
<select id="queryUserList" resultType="com.mybatis.mybatis_study.model.UserInfo">
select id,username,password,gender,age,phone,
delete_flag as deleteFlag,create_time as createTime,update_time as updateTime
from userinfo
</select>
注意中间的SQL的语句并非生成的,而是之后我们自己添加的。
然后我们生成测试类
@Test
void queryUserList() {
log.info(userInfoXmlMapper.queryUserList().toString());
}
得到结果
我们可以发现,我们在每一个SQL语句中都要使用as来解决字段值与属性值映射的问题,那么有没有一种映射配置能够让我们去配置一次所有的select都可以使用呢?
我们可以自定义一个结果映射使用(resultMap)
<resultMap id="BaseMap" type="com.mybatis.mybatis_demo.model.UserInfo">
<id property="id" column="id"></id>
<result property="deleteFlag" column="delete_flag"></result>
<result property="createTime" column="create_time"></result>
<result property="updateTime" column="update_time"></result>
<!--property与column没有先后顺序-->
</resultMap>
<select id="queryUserList2" resultMap="BaseMap">
select id,username,password,gender,age,phone,
delete_flag,create_time,update_time
from userinfo
</select>
第一行中的id,这个是映射的唯一标识符,其他的SQL语句可以通过它引用结果映射规则,例如在接下来的select就有体现。
第二行中的id,这个是主键字段,result是普通字段的映射。其中的property与column没有先后顺序,分别对应属性值和字段值。
生成测试类运行后,可以得到和上面一样的结果。
6.3新增修改删除
由于新增修改删除的xml形式与其注解形式相差不大,我们在这里仅进行代码呈现。
Integer insert(UserInfo userInfo);
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into userinfo(username,`password`,age,gender,phone)
values (#{username},#{password},#{age},#{gender},#{phone})
</insert>
@Test
void insert() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("lisi");
userInfo.setAge(15);
userInfo.setPassword("123456");
userInfo.setGender(1);
Integer result = userInfoXmlMapper.insert(userInfo);
log.info("resylt:" + result + "id:" + userInfo.getId());
}
Integer update(@Param("password") String password, @Param("id") Integer id);
<update id="update">
update userinfo set password = #{password} where id = #{id}
</update>
@Test
void update() {
userInfoXmlMapper.update("admin",2);
}
Integer delete(Integer id);
<delete id="delete">
delete from userinfo where id = #{id}
</delete>
@Test
void delete() {
userInfoXmlMapper.delete(6);
}
三.进阶
1.$和#的区别
在Mybatis中,${}和#{}是最常见的两个参数占位符,但它们的含义完全不同,用错的话会导致SQL注入风险或者语法错误。
表达方式 | 类型 | 是否预编译 | 是否安全 | 用于场景 |
#{} | 参数占位 | 是 | 安全 | 常用与传入参数的值 |
${} | 字符串拼接 | 否 | 危险 | 常用与传入表名,列名等 |
我们在UserInfoMapper中分别使用两种占位符来观察二者的区别
@Select("select * from userinfo where id = ${userId}")
UserInfo queryUserInfo2(Integer userId);
@Select("select * from userinfo where id = #{userId}")
UserInfo queryUserInfoById(Integer userId);
我们可以观察到即使两个sql语句的输出结果相同,但是我们的sql语句却不一样,前者将变量直接拼进SQL语句中,然后一次性发送给给数据库执行。后者是预编译,SQL是在数据库端线编译SQL结构(参数?占位)在传入参数。
对于预编译SQL来说
- 参数用?占位
- SQL编译一次,可以多次执行(从而提高效率)
- 参数自动转义防止SQL注入
对于即时SQL来说,它最大的缺点就在于存在SQL注入风险,但是使用动态SQL以及列名拼接需要使用它。
我们首先看这样一组测试
@Select("select * from userinfo where username = ${username}")
List<UserInfo> queryUserInfoByName2(String username);
@Test
void queryUserInfoByName2() {
log.info(userInfoMapper.queryUserInfoByName2("admin").toString());
}
运行发现程序竟然报错了,我们观察sql语句,发现我们虽然传入了admin用户名,但是它的格式并不属于字符串,因为少了引号,所以我们在传入字符串类型的参数是,需要在select语句中加入引号 @Select("select * from userinfo where username = '${username}'")
接下来我们来看使用$存在的一种sql注入的风险
@Select("select * from userinfo where username = #{username}")
UserInfo queryUserInfoByName(String username);
@Select("select * from userinfo where username = '${username}'")
List<UserInfo> queryUserInfoByName2(String username);
@Test
void queryUserInfoByName() {
log.info(userInfoMapper.queryUserInfoByName("admin").toString());
}
@Test
void queryUserInfoByName2() {
log.info(userInfoMapper.queryUserInfoByName2("'or 1 = '1").toString());
}
我们可以看到在在第二个查询语句中,我们并没有传入某一个合法的用户名,而是传入了'or 1 = '1这样一个字符串,我们可以发现我们的查询语句发生了改变,我们构造一了一个永远为真的查询语句,因为1 = ‘1’永远成立,从而我们得到了所有的用户信息,这就是sql注入。
2.动态SQL
动态 SQL 指的是 SQL 内容在运行时根据条件动态生成,例如:某些字段为空就不拼接,某些字段值不同拼接不同语句等。
2.1 核心标签
标签 | 作用说明 |
<if> | 条件判断,满足才拼接 |
<where> | 自动处理 AND/OR 和 WHERE 关键字 |
<set> | 用于 UPDATE 中自动加逗号 |
<foreach> | 循环(通常用于 IN 查询) |
<trim> | 自定义拼接前缀/后缀/去掉多余内容 |
2.2 举例说明
1.我们通过名字和年龄来查找用户信息
List<UserInfo> queryUserByWhere(@Param("userName") String userName,@Param("age") Integer age);
<select id="queryUserByWhere" resultType="com.mybatis.mybatis_study.model.UserInfo">
select * from mybatis_study.userinfo
<where>
<if test="userName != null">
username = #{userName}
</if>
<if test="age != null">
and age = #{age}
</if>
</where>
</select>
我们分别测试三种情况,分别是不传参数,只传入名字,或只传入年龄观察预编译的sql语句有什么不同
我们能够看到三种不一样的sql语句,通过传入参数的不同,where标签和if标签自动帮我们处理了sql语句,使其符合规范。
2.我们向userinfo表中插入数据
这里我们需要对trim里面的几个参数解释下其作用:
属性 | 作用 |
prefix | 前缀,比如 (,拼接在 SQL 开头 |
suffix | 后缀,比如 ),拼接在 SQL 结尾 |
prefixOverrides | 去掉前缀中多余的内容(比如多余的 , 或 AND) |
suffixOverrides | 去掉后缀中多余的内容(比如多余的 ,) |
我们可以在预编译语句中清楚地看到我trim里面的属性帮助我们拼接了(括号),并且去除了多余的逗号(比如age,gender和phone后面的逗号)
3.我们修改userinfo表中指定用户的用户信息
Integer update2(UserInfo userInfo);
<update id="update2">
update mybatis_study.userinfo
<set>
<if test="username != null">
username = #{username},
</if>
<if test="password">
password = #{password},
</if>
<if test="age">
age = #{age}
</if>
</set>
where id = #{id}
</update>
@Test
void update2() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("admin");
userInfo.setPassword("123456");
userInfo.setId(1);
userInfoXmlMapper.update2(userInfo);
}
这里我们选择修改编号为1的用户的username以及password,我们侧重于观察<set>标签的作用,我们看到控制台打印的日志
我们观察到,我们只传入两个需要修改的参数,并没有传入age参数,<set>标签帮我们去除了整个代码块最后的逗号。
4.我们对userinfo表中的数据进行批量删除
Integer batchDelete(@Param("ids") List<Integer> ids);
<delete id="batchDelete">
delete from mybatis_study.userinfo where id in
<foreach collection="ids" separator="," open="(" close=")" item="id">
#{id}
</foreach>
</delete>
@Test
void batchDelete() {
userInfoXmlMapper.batchDelete(Arrays.asList(7,8,9));
}
5.include标签
<include> 标签是 MyBatis 提供的一个强大的 SQL 片段复用机制,用于将重复的 SQL 片段提取出来,然后在多个地方引用,提高 SQL 映射文件的可维护性。
使用 <sql> 标签定义可重用的 SQL 片段:
<sql id="userColumns">
id, username, email, create_time
</sql>
<select id="selectUsers" resultType="User">
SELECT
<include refid="userColumns"/>
FROM users
</select>
这两个片段的sql语句效果是完全一样的。
四.总结
MyBatis 是一个半自动化的持久层框架,它通过 XML 或注解方式将 SQL 语句与 Java 方法绑定,自动处理参数传递和结果集映射,既保留了 SQL 的灵活性,又简化了数据库操作。其核心优势在于动态 SQL 能力和可定制的对象-关系映射,适合需要精细控制 SQL 的场景。
学习 MyBatis 需要掌握基础配置、CRUD 操作、参数传递方式以及 XML 和注解两种实现方式,同时要理解 #{}
和 ${}
的区别以避免 SQL 注入风险。动态 SQL 标签(如<if>,<where>,<set>,<foreach>)和 <include>标签能显著提升代码复用性和可维护性,使 MyBatis 成为处理复杂数据库交互的有力工具。