MyBatis 万字进阶

一. 增, 删, 改 操作

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 (回滚).

image-20230511105329228

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);
}

image-20230511110241852

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);
}

image-20230511111048298

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());
}

image-20230511112049299


二. 查询操作

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);
    }
}

执行结果如下 , 与数据库中一致

image-20230510164055956

image-20230510164142430

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);
}

运行结果正确

image-20230511095105890

但是如果我们在登录时输入: " ’ 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 注入.

image-20230511095437011

为什么会出现这样的错误? 这是因为把程序输入参数当做 SQL 指令去执行了.

SQL 中自动隐式类型转换 , 因此 1=‘1’ 一定是正确的.

image-20230511095948000

如何防止 SQL 注入?

由于 #{} 可以预执行 , 那么在它看来花括号中的参数就是一个值 , 相等就能查到 , 不等就查不到 , 不会存在拼接到 SQL 语句中的问题.

<select id="login" resultType="com.example.demo.entity.UserEntity">
    select * from userinfo where username=#{username} and password=#{password}
</select>

image-20230511100537106

#{} 不仅可以防止 SQL 注入 , 还可以根据参数类型自动加单引号 , 那么 ${} 岂不是一无是处 , 结果并非如此. 很多字符串拼接的场景非它不可 , 因此为了数据安全 , 不到万不得已不要使用 , 必须使用的话 , 一定要保证输入参数是可枚举的 , 在其替换变量之前就做检查.

2.1.3 ${} 的优点

我们日常在浏览 , 淘宝 , 京东这样的电商平台时 , 有时需要按各种属性排序 , 实现这样的功能时 , 可选参数有很多 , 因此无法写死 , 需要根据后续用户的选项来拼接.

image-20230510165926172

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 字符串拼接可以解决此问题.

image-20230510191540498

<select id="getListByName" resultType="com.example.demo.entity.UserEntity">
    select * from userinfo where username like concat('%','#{username}','%')
</select>

索引失效:

模糊查询大致有三种写法:

  • 张%
  • %张
  • %张%

只有第一种会触发索引 , 其余两种都会导致索引失效.

image-20230604195339150


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 , 为了查到作者姓名 , 必须和作者表联合查询.

image-20230512114105742

image-20230512114246486

首先创建用户表的实体类

@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);
    }
}

image-20230512120616171

查询一个用户的所有文章

首先分析清楚谁是主表谁是辅表 , 通过分析可知查询结果中大部分都是文章表的字段 , 所以文章表是主表.

创建 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);
        }
    }

image-20230604203121230


三. 复杂查询-动态SQL

动态 SQL 通常用于需要根据用户输入或其他运行时条件来生成 SQL 语句的场景,比如搜索功能、动态排序、动态筛选等。动态SQL可以使用编程语言中的字符串拼接、条件判断等语法来实现.简而言之 , 动态 SQL 就是允许在 SQL 语句中做条件拼接.

3.1 if 标签

用户添加时 , 可能会出现如下问题:

image-20230604210948905

添加字段分为两种: 必填字段和非必填字段 , 其中 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);
    }

image-20230604211816809

很明显当我们输入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 语句出错. 如果使用则正常执行.

image-20230605114414484


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 层判断一下传入参数是否都为空 , 如果为空就不在执行 , 否则就等于删库跑路了.

猜你喜欢

转载自blog.csdn.net/liu_xuixui/article/details/131073111