文章目录
- 一. 增, 删, 改 操作
- 1.1 修改操作
- 1.2 删除操作
- 1.3 添加操作
- 1.3.1 返回受影响行数
- 1.3.2 返回 id
- 二. 查询操作
- 2.1 单表查询
- 2.1.1 参数占位符 ${} 和 #{}
- 2.1.2 SQL 注入问题
- 2.1.3 ${} 的优点
- 2.1.4 Like 查询
- 2.2 多表查询
- 2.2.1 返回类型 resultType
- 2.2.2 返回字典映射 resultMap
- 2.2.3 多表联查
- 查询文章表的详情
- **查询一个用户的所有文章**
- 三. 复杂查询-动态SQL
- 3.1 if 标签
- 3.2 trim 标签
- 3.3 where 标签
- 3.4 set 标签
- 3.5 foreach 标签
一. 增, 删, 改 操作
1.1 修改操作
实现修改密码功能 , 返回修改行数(affected rows).
首先确定修改密码需要传入的参数 , 用户id , 旧密码 , 新密码
Interface 中声明方法:
// 修改密码
int updatePassword(@Param("id") Integer id,
@Param("password") String password,
@Param("newPassword") String newPassword);
xml 中实现方法:
Tips: update 标签中无需添加 resultType , 因为增删改 , 这类操作无需返回结果集 , 只需知道是否成功或 id 即可 , resultType 是指定返回结果类型的属性 , 它告诉 MyBatis 如何将结果集转换为 Java 对象.
<update id="updatePassword">
update userinfo set password=#{newPassword}
where id=#{id} and password=#{password}
</update>
单元测试:
@Transactional //正常执行不污染数据库
@Test
void updatePassword() {
int result = userMapper.updatePassword(1, "admin", "123456");
System.out.println("修改:" + result);
}
Tips: @Transactional (事务) , 加上这一条注解可以防止数据库被修改 , 执行 SQL 语句时开启一个事务 , 等待执行完毕再 Rollback (回滚).
1.2 删除操作
删除用户 , 返回受影响的行数
Interface 中声明方法:
// 删除密码
int delById(@Param("id") Integer id);
xml 中实现方法:
<delete id="delById">
delete from userinfo where id=#{id}
</delete>
单元测试:
@Transactional
@Test
void delById() {
int id = 1;
int result = userMapper.delById(1);
System.out.println("删除结构" + result);
}
1.3 添加操作
1.3.1 返回受影响行数
Interface 中声明方法:
// 添加用户
int addUser(UserEntity user);
xml 中实现方法:
<insert id="addUser">
insert into userinfo(username, password) values(#{username},#{password})
</insert>
单元测试:
@Test
void addUser() {
UserEntity user = new UserEntity();
user.setUsername("张三");
user.setPassword("11111");
int result = userMapper.addUser(user);
System.out.println(result);
}
1.3.2 返回 id
Interface 中声明方法与之前一致:
int addUserGetId(UserEntity user);
只需在 insert 标签中 , 添加 userGenerateKeys=“true” 生成主键 , keyProperty=“id” 设置主键为 id.
<insert id="addUserGetId" useGeneratedKeys="true" keyProperty="id">
insert into userinfo(username, password) values(#{username},#{password})
</insert>
单元测试:
此时 MyBatis 会自动将 getId 加入对象中.
@Test
void addUserGetId() {
UserEntity user = new UserEntity();
user.setUsername("李四");
user.setPassword("22222");
int result = userMapper.addUserGetId(user);
System.out.println("添加行数" + result);
System.out.println("Id 为: " + user.getId());
}
二. 查询操作
2.1 单表查询
实现根据用户 id , 查询用户信息的功能.
首先在 Interface 中声明方法:
Tips: 如果不加 @Param 部分电脑会找不到参数 , 因此统一加上 @Param
// 根据 id 查询用户对象 , @Param 相当于给参数起名, 如果名称不一致会报错
UserEntity getUserById(@Param("id") Integer id);
在 xml 中实现 SQL 命令
<select id="getUserById" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where id=${id}
</select>
为了不污染数据库 , 使用单元测试来测试代码.
@SpringBootTest //声明当前单元测试的类是运行在 SpringBoot 当中的.
class UserMapperTest {
@Autowired //属性注入
private UserMapper userMapper;
@Test
void getUserById() {
UserEntity user = userMapper.getUserById(1);
System.out.println(user);
}
}
执行结果如下 , 与数据库中一致
2.1.1 参数占位符 ${} 和 #{}
- ${} 字符直接替换
- #{} 占位符预编译处理
直接替换是指 , MyBatis 在处理 ${} 时 , 会直接替换为变量的值.
预处理是指 , MyBatis 在处理 #{} 会替换成 ? , 调用 PreparedStatement 的 set 方法来赋值.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LrF7S6xc-1686045145549)(C:/Users/86178/AppData/Roaming/Typora/typora-user-images/image-20230511093437168.png)]
例如在日常生活中 , 各种商场每天早上开门之前 , 先让各个岗位的员工进入 , 等员工全部就位之后才开门营业. 这是为了防止部分顾客 零元购 , 这样的操作就相当于预处理 , 如果有顾客浑水摸鱼早早进入商场 , 就会被打卡系统识别. 如果是直接替换 , 那么顾客可以随时进入商场且没有任何监管机制 , 这时就可能出现异常.
2.1.2 SQL 注入问题
登录是 SQL 注入经常出现的场景 , 下面我们实现一个博客系统的登录功能来介绍 SQL 注入.
Interface 中声明登录方法:
@Mapper
public interface UserMapper {
// 登录方法
UserEntity login(UserEntity user);
}
xml 中实现方法:
由于username 和 password 是String 类型的变量 , 而 ${} 会直接替换变量 , 为了保证 SQL 语句正确 , 我们手动加上单引号.
<select id="login" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username='${username}' and password='${password}'
</select>
Tips: 方法中传入参数的一个对象 , 在 xml 中实现时 , MyBatis 会自动映射属性名.
单元测试:
@Test
void login() {
String username = "admin";
String password = "admin";
UserEntity inputUser = new UserEntity();
inputUser.setUsername(username);
inputUser.setPassword(password);
UserEntity user = userMapper.login(inputUser);
System.out.println(user);
}
运行结果正确
但是如果我们在登录时输入: " ’ or 1='1";
@Test
void login() {
String username = "admin";
String password = " ' or 1='1";
UserEntity inputUser = new UserEntity();
inputUser.setUsername(username);
inputUser.setPassword(password);
UserEntity user = userMapper.login(inputUser);
System.out.println(user);
}
依然查询到了用户的信息 , 这就是所谓的 SQL 注入.
为什么会出现这样的错误? 这是因为把程序输入参数当做 SQL 指令去执行了.
SQL 中自动隐式类型转换 , 因此 1=‘1’ 一定是正确的.
如何防止 SQL 注入?
由于 #{} 可以预执行 , 那么在它看来花括号中的参数就是一个值 , 相等就能查到 , 不等就查不到 , 不会存在拼接到 SQL 语句中的问题.
<select id="login" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username=#{username} and password=#{password}
</select>
#{} 不仅可以防止 SQL 注入 , 还可以根据参数类型自动加单引号 , 那么 ${} 岂不是一无是处 , 结果并非如此. 很多字符串拼接的场景非它不可 , 因此为了数据安全 , 不到万不得已不要使用 , 必须使用的话 , 一定要保证输入参数是可枚举的 , 在其替换变量之前就做检查.
2.1.3 ${} 的优点
我们日常在浏览 , 淘宝 , 京东这样的电商平台时 , 有时需要按各种属性排序 , 实现这样的功能时 , 可选参数有很多 , 因此无法写死 , 需要根据后续用户的选项来拼接.
Interface 中声明:
List<UserEntity> getAllBySort(@Param("Sort") String Sort);
xml 中实现:
<select id="getAllBySort" resultType="com.example.demo.entity.UserEntity">
select * from userinfo order by id ${Sort}
</select>
单元测试执行:
@Test
void getAllBySort() {
List<UserEntity> userSort = userMapper.getAllBySort("desc");
for(UserEntity user: userSort){
System.out.println(user);
}
}
使用 ${} 可以直接替换为需要的字符串 , 但如果使用 #{} 就不能实现排序查询了 , 因为传递的值为 String 会加单引号 , 导致 SQL 语句错误.
2.1.4 Like 查询
当我们需要实现模糊查询的功能时 , #{} 检测到传入值为 String 类型 , 会多加一对单引号 , 变为 ‘%‘username’%’ 导致 SQL 语句出错.
<select id="getListByName" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username like '%#{username}%'
</select>
解决方式:
MySQL 内置函数 concat 字符串拼接可以解决此问题.
<select id="getListByName" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username like concat('%','#{username}','%')
</select>
索引失效:
模糊查询大致有三种写法:
- 张%
- %张
- %张%
只有第一种会触发索引 , 其余两种都会导致索引失效.
2.2 多表查询
增, 删, 改 这类只需返回受影响行数的操作 , 无需设置返回类型.
<update id="updatePassword">
update userinfo set password=#{newPassword}
where id=#{id} and password=#{password}
</update>
<delete id="delById">
delete from userinfo where id=#{id}
</delete>
<insert id="addUser">
insert into userinfo(username, password) values(#{username},#{password})
</insert>
然而 , 如果是查询操作 , 即使是查一个用户的名称也要设置返回类型 , 否则就会报错. 因为 MyBatis 无法将数据库中检索到的结果集映射为 java 对象.
2.2.1 返回类型 resultType
MyBatis 中 resultType 用于指定查询结果的类型 , 告诉 MyBatis 将数据库中检索到的结果集映射为 Java 对象.
<select id="getUser" resultType="com.example.User"> select * from user where id = #{id} </select>
上面的例子中,resultType指定了查询结果的类型为com.example.User,表示查询结果将会被转换成一个User对象。
Tips: resultType只适用于单结果查询,如果我们需要进行多结果查询,那么应该使用resultMap来指定查询结果的映射关系。
2.2.2 返回字典映射 resultMap
resultMap 使用场景
- 字段名和属性名不同 , 可以使用 resultMap 配置映射
- 一对一和一对多可以使用 resultMap 映射并查询数据
如果数据库中字段名 , 和 java 对象中的不一致 , 那么直接使用 resultType 就会报错. 因为 resultType 直接按照指定类型 , 在数据库和 Java 对象中进行映射 , 如果找不到匹配的就会映射失败.
假设数据库中密码为 password , java 对象中为 pwd.
xml 中设置 resultMap
每个 resultMap 默认都有两个属性 , id 表示该 resultMap 的唯一标识符 , 用于区分MyBatis 中不同 resultMap(可任意起) , type 表示实体类的类型 , property 为 java 对象中的属性 , column 为数据库中对应字段名.
<resultMap id="BaseMap" type="com.example.demo.entity.UserEntity">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="pwd" column="password"></result>
<result property="createtime" column="createtime"></result>
</resultMap>
为指定方法设置resultMap
<select id="getListByName" resultMap="BaseMap">
select * from userinfo where username like concat('%',#{username},'%')
</select>
暴力方法
也可以在 xml 中给 SQL 语句起个别名
<select id="getListByName" resultMap="BaseMap">
select id,username,password as pwd from userinfo where username like concat('%',#{username},'%')
</select>
2.2.3 多表联查
多表查询时 , 如果一个类中包含另一个对象 , resultType 是查不出包含对象的.
多表查询时 , 通常使用 left/right join 来连接表 , 这样可以把表分为主表和次表.
查询文章表的详情
假设我们要查询文章表的详情 , 但却只能查到作者 id , 为了查到作者姓名 , 必须和作者表联合查询.
首先创建用户表的实体类
@Data
public class ArticleInfo {
private int id;
private String title;
private String content;
private LocalDateTime createTime;
private int uid;
private int rcount;
private int state;
}
创建一个扩展类 vo(view object ) , 存放扩展信息
继承可以更加简单的将原实体类中的属性继承过来. 直接在原实体类中添加 username 属性 , 这样看似方便却不符合程序设计的单一原则 , 如果后续我们扩展成百上千个属性 , 就会污染原实体类
@Data
public class ArticleInfoVO extends ArticleInfo {
private String username;
@Override
public String toString() {
return "ArticleInfoVO{" +
"username='" + username + '\'' +
"} " + super.toString();
}
}
Tisp: 扩展类一定要重写 toString() 方法 , 否则 lombok 的 toString() 默认只打印当前类的属性.
创建一个 MyBatis 的 Interface 声明方法:
@Mapper
public interface ArticleMapper {
// 查询文章详情
ArticleInfoVO getDetail(@Param("id") Integer id);
}
MyBatis 中创建 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.example.demo.mapper.ArticleMapper">
<select id="getDetail" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select a.*,u.username from articleinfo a
left join userinfo u on u.id=a.uid
where a.id=#{id}
</select>
</mapper>
单元测试:
@SpringBootTest
class ArticleMapperTest {
@Autowired
ArticleMapper articleMapper;
@Test
void getDetail() {
ArticleInfoVO articleInfoVO = articleMapper.getDetail(1);
System.out.println(articleInfoVO);
}
}
查询一个用户的所有文章
首先分析清楚谁是主表谁是辅表 , 通过分析可知查询结果中大部分都是文章表的字段 , 所以文章表是主表.
创建 Mybatis 的Interface 声明
@Mapper
public interface ArticleMapper {
//查询用户的所有文章
List<ArticleInfoVO> getListByUid(@Param("uid") Integer uid);
}
Mybatis 的 xml 文件中实现该方法
<select id="getListByUid" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select a.*,u.username from articleinfo a
left join userinfo u on a.uid=u.id
where a.uid=#{uid}
</select>
进行单元测试
@Test
void getListByUid() {
Integer uid = 1;
List<ArticleInfoVO> list = articleMapper.getListByUid(uid);
// 使用并行的方式打印用户的信息
// list.stream().parallel().forEach(System.out::println);
for (ArticleInfoVO list1: list) {
System.out.println(list1);
}
}
三. 复杂查询-动态SQL
动态 SQL 通常用于需要根据用户输入或其他运行时条件来生成 SQL 语句的场景,比如搜索功能、动态排序、动态筛选等。动态SQL可以使用编程语言中的字符串拼接、条件判断等语法来实现.简而言之 , 动态 SQL 就是允许在 SQL 语句中做条件拼接.
3.1 if 标签
用户添加时 , 可能会出现如下问题:
添加字段分为两种: 必填字段和非必填字段 , 其中 id 就是必填字段 , 其余字段都是非必填字段 , 那么假设我们添加用户时 , 由于 photo 字段不做限制 , 可能会出现出乎意料的结果 , 为了防止出现这种情况我们可以使用标签来解决.
<insert id="addUser2">
insert into userinfo(username,password
<if test="photo!=null and photo!=''">
,photo
</if>
) values(#{username},#{password}
<if test="photo!=null and photo!=''">
,#{photo}
</if>
)
</insert>
其中标签中 test 的内容不是数据库中的字段 , 而是传入对象的属性 , 由 Mybatis 执行. 当输入的 photo 字段为 null 或者为空时 , 不拼接.
如何区别是数据库的字段还是对象的属性?
只需看是否被特殊字符修饰 , 类似于 #{photo} 这种被特殊字符修饰的一定是对象的属性 , 而 photo 则是数据库字段.
单元测试:
@Transactional
@Test
void addUser2() {
String username = "liliu";
String password = "123456";
String photo = "";
UserEntity user = new UserEntity();
user.setUsername(username);
user.setPassword(password);
user.setPhoto(photo);
int result = userMapper.addUser2(user);
System.out.println("添加: "+result);
}
很明显当我们输入photo 字段为空时 , sql 中并没有拼接.
3.2 trim 标签
如果所有属性都是非必填项 , 就考虑使用 标签结合 标签 , 对多个字段采取动态生成的方式.
标签中有如下属性:
- prefix: 表示整个语句块 , 以 perfix 的值作为前缀.
- suffix: 表示整个语句块 , 以 suffix 的值作为后缀.
- prefixOverrides: 表示整个语句块要去除的前缀.
- suffixOverrides: 表示整个语句块要去除的后缀.
上述添加用户的问题 , 如果所有字段都是非必填字段 , 那么按 标签来写的话 , 无论怎么设置逗号的位置 , 必然会出现多逗号或少逗号的情况.
<insert id="addUser3">
insert into userinfo(
<if test="username!=null and username!=''">
username,
</if>
<if test="password!=null and password!=''">
password,
</if>
<if test="photo!=null and photo!=''">
photo
</if>
) values(
<if test="username!=null and username!=''">
#{username},
</if>
<if test="password!=null and password!=''">
#{password},
</if>
<if test="photo!=null and photo!=''">
#{photo}
</if>
)
</insert>
因此我们可以改为 配合 的写法
以下代码中 trim 的写法 , 可以去除整个语句块的最后一个逗号 , 并且在 中添加括号.
- 基于 prefix 配置 , 开始部分加上 (
- 基于 suffix 配置 , 结束部分加上 )
- 基于 suffixOverrides 配置会去掉最后一个逗号
<insert id="addUser3">
insert into userinfo
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username!=null and username!=''">
username,
</if>
<if test="password!=null and password!=''">
password,
</if>
<if test="photo!=null and photo!=''">
photo,
</if>
</trim>
values
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username!=null and username!=''">
#{username},
</if>
<if test="password!=null and password!=''">
#{password},
</if>
<if test="photo!=null and photo!=''">
#{photo},
</if>
</trim>
</insert>
执行单元测试:
@Transactional
@Test
void addUser3() {
String username = "liliu";
String password = "123456";
String photo = "";
UserEntity user = new UserEntity();
user.setUsername(username);
user.setPassword(password);
// user.setPhoto(photo);
int result = userMapper.addUser3(user);
System.out.println("添加: "+result);
}
如果不加 标签 , 只添加用户名和密码字段 , 那么密码后面一定会多一个逗号 , 导致 sql 语句出错. 如果使用则正常执行.
3.3 where 标签
where 标签用于生成 SQL 语句中的 where 子句,它的作用是根据指定的条件过滤数据。
通过 id 和 title 查询文章
<select id="getListByIdOrTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select * from articleinfo
where
<trim suffixOverrides="and">
<if test="id!=null and id > 0">
id=#{id} and
</if>
<if test="title!=null and title!=''">
title like concat('%',title,'%')
</if>
</trim>
</select>
上述代码 where 中的参数传递有 4 种情况 : 1. id 传, title 不传. 2. id 不传,title传. 3. id传 , title 传. 4. id不传 , title 不传.
通过测试可以发现 , 第四种情况 , where 条件为空会发生 sql 语句错误. 针对这种情况有两种解决方案:
解决方案一: 1=1 解决方法
<select id="getListByIdOrTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select * from articleinfo
where 1=1
<trim prefixOverrides="and">
<if test="id!=null and id > 0">
and id=#{id}
</if>
<if test="title!=null and title!=''">
and title like concat('%',title,'%')
</if>
</trim>
</select>
虽然 1=1 在代码的编译器会被优化不消耗性能 , 但许多公司的代码规范并不推荐这么做.
解决方案二: where 作为 trim 标签的前缀
<select id="getListByIdOrTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select * from articleinfo
<trim prefix="where" suffixOverrides="and">
<if test="id!=null and id > 0">
id=#{id} and
</if>
<if test="title!=null and title!=''">
title like concat('%',title,'%')
</if>
</trim>
</select>
当 trim 中生成了代码 , 那么才会添加 里的前缀和后缀 , 如果 trim 中没有代码 , 那么才会添加 中的前缀和后缀.
但是我们可以发现 , 使用 标签会产生很多的顾虑 , 如加前缀还是后缀 , and 或 逗号加在前面还是后面…
这时如果我们使用 标签就可以完美的解决该顾虑.
解决方案三: 标签
<select id="getListByIdOrTitle" resultType="com.example.demo.entity.vo.ArticleInfoVO">
select * from articleinfo
<where>
<if test="id!=null and id > 0">
id=#{id}
</if>
<if test="title!=null and title!=''">
and title like concat('%',title,'%')
</if>
</where>
</select>
标签除了 , 可以更简洁方便 , 还可以去除最前面的 and 关键字. 但注意 标签不会去除最后面的关键字.
3.4 set 标签
set 标签和 where 标签的作用一致 , 只不过 set 标签需要搭配 update 来使用.
<update id="updatename">
update user
<set>
<if test="username!=null">
username=#{username},
</if>
<if test="password!=null">
password=#{username},
</if>
<if test="sex!=null">
sex=#{sex},
</if>
<if test="birth!=null">
birth=#{birth},
</if>
</set>
where id = #{id}
</update>
标签可以去除 , 最后面的关键字 , 但必须注意的是如果 set 标签中没有参数会出现 sql 语句错误. 因此执行该方法之前必须在 controller 层先判断一下传入对象的参数都为是否为空 , 如果都为空则不执行.
3.5 foreach 标签
对集合遍历时可以使用该标签. 标签有如下常用属性:
- colletion: 存放传递过来集合的名称 , List , Map , Set
- item: 存放遍历时的每一个对象.
- open: foreach 的前缀是什么
- close: foreach 的后缀是什么
- separator: 每一层遍历的分隔符是什么
根据集合中的 id 批量删除文章
<delete id="deleteByIdS">
delete from articleinfo
where id in
<foreach collection="ids" item="item" open="(" close=")" separator=",">
#{item}
</foreach>
</delete>
标签执行结果
delete from Articleinfo
where id in
(id1, id2 , id3 , id4)
Tips: 执行代码之前必须在 controller 层判断一下传入参数是否都为空 , 如果为空就不在执行 , 否则就等于删库跑路了.
ername},
password=#{username},
sex=#{sex},
birth=#{birth},
where id = #{id}
<set> 标签可以去除 , 最后面的关键字 , 但必须注意的是如果 set 标签中没有参数会出现 sql 语句错误. 因此执行该方法之前必须在 controller 层先判断一下传入对象的参数都为是否为空 , 如果都为空则不执行.
---
### 7.5 foreach 标签
对集合遍历时可以使用该标签. <foreach> 标签有如下常用属性:
- colletion: 存放传递过来集合的名称 , List , Map , Set
- item: 存放遍历时的每一个对象.
- open: foreach 的前缀是什么
- close: foreach 的后缀是什么
- separator: 每一层遍历的分隔符是什么
根据集合中的 id 批量删除文章
```xml
<delete id="deleteByIdS">
delete from articleinfo
where id in
<foreach collection="ids" item="item" open="(" close=")" separator=",">
#{item}
</foreach>
</delete>
标签执行结果
delete from Articleinfo
where id in
(id1, id2 , id3 , id4)
Tips: 执行代码之前必须在 controller 层判断一下传入参数是否都为空 , 如果为空就不在执行 , 否则就等于删库跑路了.