Mybatis多表查询与动态SQL的使用

news2025/1/20 6:03:06

目录

1. Mybatis多表查询

1.1 添加文章表实体类

1.2 文章Interface

1.3 文章.xml

1.4 lombok的toString()有关对象打印的说明

1.5 场景: 一个用户查询多篇文章

2. 复杂情况: 动态SQL的使用

2.1 为什么要使用动态SQL?

2.2 <if>标签

2.3 <trim>标签

2.4 <where>标签

2.5 <set>标签

2.6 <foreach>标签


1. Mybatis多表查询

多表修改几乎是不使用的, 使用到的业务场景较少, 也很容易出问题. 所以对于多表的操作我们更多的是关心它的查询.

使用多表联查用的最多的是left/right...join...

我们举一个场景, 在个人博客系统项目中有文章表, 那么这个文章表会有一个uid用户ID. 不过我们在进行显示文章详情页的时候通常显示的是文章的作者名.

这个时候就涉及到数据的多表联查了.

多表联查在Mybatis中有两种实现, 一种是使用<resultMap>, 然后再里面使用<association>, 在这个标签中设置相应的信息, 实现多表联查. 但是很麻烦, 企业中几乎不用, 这里省略.

那么我们使用的是上文中的数据库字段和程序字段不匹配时进行重命名. 多表联查的也是类似的, 使用left/right...join...进行查询, 查询完了之后给它进行重命名, 就可以实现多表联查.


1.1 添加文章表实体类

要实现文章的详情查询, 我们先建立文章表实体类, 根据数据库中文章表的字段编写代码.

package com.example.demo.entity;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class ArticleInfo {
    private int id;
    private String title;
    private String content;
    private LocalDateTime createtime;
    private LocalDateTime updatetime;
    private int uid;
    private int rcount;
    private int state;
}

建扩展类, 因为在进行数据返回的时候返回的是作者name, 而不是用户uid, 所以我们在entity中创建vo.ArticleInfoVO.java

package com.example.demo.entity.vo;

import com.example.demo.entity.ArticleInfo;
import lombok.Data;

@Data
public class ArticleInfoVO extends ArticleInfo {
    private String username;

    // 处理 lombok 的 toString() 不含父类打印
    @Override
    public String toString() {
        return "ArticleInfoVO{" +
                "username='" + username + '\'' +
                "} " + super.toString();
    }
}
注: 重写toString时, 选择重写Template的时候一定要选择String concat (+) and super.toString(), 这个的意思是生成它的时候会生成父类的toString.

1.2 文章Interface

package com.example.demo.mapper;

import com.example.demo.entity.vo.ArticleInfoVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface ArticleMapper {
    // 查询文章详情
    ArticleInfoVO getDetail(@Param("id") Integer id);
}

1.3 文章.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>
SQL语句说明:
1. 查询文章表里面的所有信息以及用户表的username, 那么主表就为文章表并起别名a,
有了主表之后然后要关联其他表, 即"left join userinfo(并别名u)"这样的查询包含了笛卡尔积的结果,
那么我们需要把笛卡尔积里面的无效信息排除掉, 即"u.id=a.uid".
也就是当"u.id=a.uid"时取"userinfo".
where文章表的id等于传过来的参数id.

经查询数据库的文章表中有一条记录:

那么我们进行单元测试, 看是否能拿到这条信息:

package com.example.demo.mapper;

import com.example.demo.entity.vo.ArticleInfoVO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class ArticleMapperTest {
    @Autowired
    private ArticleMapper articleMapper;

    @Test
    void getDetail() {
        ArticleInfoVO articleInfoVO = articleMapper.getDetail(1);
        System.out.println(articleInfoVO);
    }
}

可以看到, 拿到了ArticleInfoVO的username以及它的父类ArticleInfo的各项属性, 也就是说我们成功查询到了文章详情.


1.4 lombok的toString()有关对象打印的说明

在上述测试代码中, 如果我们在ArticleInfoVO没有重写toString, 那么由lombok的注解重写出的toString就只有ArticleInfoVO的username, 没有包含父类toString的重写, 该结论由运行后生成的字节码可以看出.

并且, 此时如果运行测试代码, 就无法看到整个对象的打印, 如下所示.

注: 可以看到, JDBC的结果是没有问题的, 有文章所有的信息, 同时包括用户的username.

那么当我们主动重写了toString后, 运行再次查看字节码可以看到, toString已经变成了我们主动重写的toString, 测试结果也会是我们的预期结果.


还可以从以下角度说明是lombok的原因:

在未主动重写toString时的测试方法getDetail()中再写入以下代码:

System.out.println("title: " + articleInfoVO.getTitle());

可以看到title是有值的, 所以可以知道打印ArticleInfoVO对象是使用了lombok的toString.


1.5 场景: 一个用户查询多篇文章

我们先在文章表中添加数据以介绍一个用户查询多篇文章:

insert into articleinfo(title,content,uid) values('mysql','mysql正文',1),('redis','redis正文',1);

给ArticleMapper添加Interface与.xml:

List<ArticleInfoVO> getListByUid(@Param("uid") Integer uid);
    <select id="getListByUid" 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.uid=#{uid}
    </select>

测试代码:

    @Test
    void getListByUid() {
        Integer uid = 1;
        List<ArticleInfoVO> list = articleMapper.getListByUid(uid);
        // 使用 parallel() 设置多线程执行打印 list 中的信息
        list.stream().parallel().forEach(System.out::println);  
    }

可以看到, 在多线程执行打印list中的信息下, 不仅实现了预期结果, 并且id的顺序也是乱序的(每次执行顺序都不同)

2. 复杂情况: 动态SQL的使用

2.1 为什么要使用动态SQL?

官方文档: Mybatis动态sql

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

动态 sql是Mybatis的强大特性之一,能够完成不同条件下不同的 sql 拼接. 简单来说, 动态SQL允许我们在xml中写逻辑判断. 逻辑判断即if, else, for循环等, 也就是有些情况我们是需要在xml中做判断的.

比如我们在注册用户的时候,可能会有这样一个问题,如下图所示:

在上面这个表单中分为两种类型的字段: 必填字段和非必填字段.

那么非必填对于用户来说可以选择填写也可以选择不填写, 不过这里是可能会出现问题的.

我们以 前文的用户表 为例, 看一下它里面的默认值.

 show create table userinfo;

通过以上指令可以显示创建userinfo的SQL语句.

可以看到, 其中哪些字段有设置default值. 比如'photo'为空, 'state'为1.

就'state'来说, 如果这个是个可选的字段, 这个时候用户没有选择它来输入, 此时它接收的是Integer值就为null了.

当它是null的时候它相当于给state插入了null值, 显然null值和不插入是不同的.

-- 按照之前的写法生成的SQL
insert into userinfo(username,password,state) values('wangwu','123456',null);
-- 预期生成的SQL
insert into userinfo(username,password) values('wangwu2','123456');

以上两句SQL是有非常大的区别的, 那么可以看到, 'wangwu'的状态为null了, 但是我们预期的结果是1, 所以上面第一条就是一个无效的SQL, 因为是一个int却插入了一个null. 所以当我们添加的时候一定是后一种方式, 因为前一种方式可能会有莫名其妙的问题出现. 这就是要学习动态SQL的原因.

2.2 <if>标签

<if>的作用就是判断传递过来的参数是否有值.

在UserMapper的Interface中添加, :

    int addUser2(UserEntity user);

这里的UserEntity中的photo则是有可能输入, 有可能不输入.

    <insert id="addUser2">
        insert into userinfo(username,
        <if test="img!=null and img!=''">
            photo,
        </if>
        password) values(#{username},
        <if test="img!=null and img!=''">
            #{img},
        </if>
        #{pwd})
    </insert>
    @Transactional
    @Test
    void addUser2() {
        String username = "liliu";
        String password = "123456";
        UserEntity user = new UserEntity();
        user.setUsername(username);
        user.setPwd(password);
//        user.setImg("cat.png");
        int result = userMapper.addUser2(user);
        System.out.println("添加: " + result);
    }

取消掉user.setImg()的注释之后:

所以我们可以看到, 同样的代码, 但是不同的输入值, 生成的SQL语句也不同, 即这里的SQL经过我们在xml中<if>标签的逻辑书写变得不同.

注意 test 中的img,是传入对象中的属性,不是数据库字段

2.3 <trim>标签

之前的插入用户功能,只是有一个 img 字段可能是选填项,如果所有字段都是非必填项,就考虑使用<trim>标签结合<if>标签,对多个字段都采取动态生成的方式。

<trim>标签中有如下属性:

  • prefix: 表示整个语句块,以prefix的值作为前缀
  • suffix: 表示整个语句块,以suffix的值作为后缀
  • prefixOverrides: 表示整个语句块要去除掉的前缀
  • suffixOverrides: 表示整个语句块要去除掉的后缀
    int addUser3(UserEntity user);
   <insert id="addUser3">
        insert into userinfo
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="username!=null and username!=''">
                username,
            </if>
            <if test="img!=null and img!=''">
                photo,
            </if>
            <if test="pwd!=null and pwd!=''">
                password,
            </if>
        </trim>
        values
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="username!=null and username!=''">
                #{username},
            </if>
            <if test="img!=null and img!=''">
                #{img},
            </if>
            <if test="pwd!=null and pwd!=''">
                #{pwd}
            </if>
        </trim>
    </insert>
    @Test
    void addUser3() {
        String username = "liliu";
        String password = "123456";
        UserEntity user = new UserEntity();
        user.setUsername(username);
        user.setPwd(password);
//        user.setImg("cat.png");
        int result = userMapper.addUser3(user);
        System.out.println("添加: " + result);
    }

在以上 sql 动态解析时,会将第一个 <trim> 部分做如下处理:

  • 基于 prefix 配置,开始部分加上 (
  • 基于 suffix 配置,结束部分加上 )
  • 多个 <if>组织的语句都以 , 结尾,在最后拼接好的字符串还会以 , 结尾,会基于 suffixOverrides 配置去掉最后一个 ,
  • 注意 <if test="username != null"> 中的 username 是传入对象的属性
前面两个标签都是使用insert的案例, 但是实际使用的时候还是需要根据使用场景灵活变通.

2.4 <where>标签

<where>标签就是进行条件查询的时候进行使用的.

比如有一种场景, 博客系统的搜索功能, 那么当我们网站的其中一个页面(比如个人主页)查看内容的时候, 这个时候也是有可能是会进行搜索的, 此时它会判断我们有没有输入内容(username, title...), 如果输入title, 那么它就会根据title进行查询, 但是这里title是非必传的, 那么参数个数就可能会有变化.

所以, 下面我们来探讨: 当Mybatis中多个都是非必传参数的时候, 有哪些解决方案?

解决方案1: 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>

不过这样去写并不是特别好, 因为如果此时id和title都不传(或者都传null), 生成的代码中依然有where条件, 这部分代码是造成冗余的, 虽然执行是没有问题的, 代码会被优化掉, 但这可能对于中大型公司来说是不允许, 所以我们尽量减少这类1=1代码的使用.

解决方案2: <trim>的方式

因为上面所阐述的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>
注意<if>标签中and的位置

当 trim 中生成了代码,那么才会添加<trim>里面的前缀和后缀,如果 trim 中没有生成代码,则前缀和后缀都会省略 (不生成)


进行单元测试来验证一下生成的SQL代码:

我们先传入两个null:

    @Test
    void getListByIdOrTitle() {
        List<ArticleInfoVO> list = articleMapper.getListByIdOrTitle(null, null);
        System.out.println(list.size());
    }

可以看到, 此时代码简化了.

然后我们传id为1, title为null进行测试:

可以看到, 当我们传id的时候, where自动加上了.


解决方案3: <where>标签方式

    <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>

<where> 自动帮我们去除最前面的 and 关键字,但需要注意的是,使用 <where>标签不会自动帮我们去除最后面的 and 关键字。

对不同种情况进行测试({null,null},{1,null},{null,"java"}):

以上<where>标签也可以使用 <trim prefix="where" prefixOverrides="and"> 替换。

2.5 <set>标签

根据传入的用户对象属性来更新用户数据,可以使用<set>标签来指定动态内容.

那么由于<set>的使用和where类似, 此处仅提供示例代码.

    int updateById(User user);
   <update id="updateById">
        update user
        <set>
            <if test="username != null">
                username=#{username},
            </if>
            <if test="password != null">
                password=#{password},
            </if>
            <if test="nickname != null">
                nickname=#{nickname},
            </if>
            <if test="sex != null">
                sex=#{sex},
            </if>
            <if test="birthday != null">
                birthday=#{birthday},
            </if>
            <if test="head != null">
                head=#{head},
            </if>
            <if test="createTime != null">
                create_time=#{createTime},
            </if>
        </set>
        where id=#{id}
    </update>
<set>标签也可以使用 <trim prefix="set" suffixOverrides=",">替换。

2.6 <foreach>标签

对集合进行遍历时可以使用该标签。<foreach>标签有如下属性:

  • collection: 绑定方法参数中的集合,如 List,Set,Map或数组对象
  • item: 遍历时的每一个对象
  • open: 语句块开头的字符串
  • close: 语句块结束的字符串
  • separator: 每次遍历之间间隔的字符串
    // 根据文章id集合批量删除文章
    int delByIdList(List<Integer> idList);
    <delete id="delByIdList">
        delete from articleinfo
        where id in
        <foreach collection="idList" item="aid" open="(" close=")" separator=",">
            #{aid}
        </foreach>
    </delete>
    @Transactional
    @Test
    void delByIdList() {
        List<Integer> idList = new ArrayList<>();
        idList.add(1);
        idList.add(2);
        idList.add(3);
        int result = articleMapper.delByIdList(idList);
        System.out.println("删除条数: " + result);
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/884126.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何在 iOS 上安装并使用 ONLYOFFICE 文档

借助 iOS 版文档应用&#xff0c;您可在移动端设备上访问存储于 ONLYOFFICE 账户中的文件&#xff0c;查看和编辑现有文本文档、电子表格和演示文稿&#xff0c;创建新文档并对其进行整理&#xff0c;以及连接第三方云存储服务。您可与其他门户网站用户协作编辑文档&#xff0c…

【第三阶段】kotlin语言的可空性

1.kotlin语言默认是不可空类型&#xff0c;不能随意给null fun main() {var name:String"kotlin"namenull }执行结果 报错&#xff1a; Null can not be a value of a non-null type String2.声明可空类型 &#xff1f; fun main() {var name:String ?namenull…

语聚AI公测发布,大语言模型时代下新的生产力工具

语聚AI 公测发布 距离语聚AI内测上线已经过去近1个月。 这期间&#xff0c;我们共邀请了近百位资深用户与行业专家加入语聚AI产品体验。通过大家的热情参与积极反馈&#xff0c;我们不断优化并完善了语聚AI的功能与使用体验。 经过研发团队不懈的努力&#xff0c;今天语聚AI终…

[谦实思纪 01]整理自2023雷军年度演讲——《成长》(上篇)武大回忆(梦想与成长)

文章目录 [谦实思纪]整理自2023雷军年度演讲 ——《成长》&#xff08;上篇&#xff09;武大回忆&#xff08;梦想与成长&#xff09;0. 写在前面1. 梦开始的地方1.1 要有梦想&#xff0c;要用目标量化梦想 2. 在两年内修完所有的学分。2.1 别老自己琢磨&#xff0c;找个懂的人…

【C++】STL案例1-评委打分

0.前言 1.系统自动生成的评委评分代码&#xff1a; #include <iostream> using namespace std; #include <deque> #include <vector> #include <algorithm> #include <string>//选手类 class Player { public:Player(string name, float score)…

Python:LVGL与触摸屏的调试记录

在移远模块EC-600M上驱动电容触摸屏&#xff0c;触摸屏控制IC为FT6206。 一、接口 TP屏的管脚如下&#xff0c;有6PIN。使用I2C接口通讯 所以我们用模块的I2C1通道&#xff0c;模块的IO口电压也是1.8v 二、I2C从地址 FT6x06芯片相对于主机来说是一个I2C设备 因此需要一个I2C…

3.1 命名空集using声明

博主介绍&#xff1a;爱打游戏的计算机专业学生 博主主页&#xff1a;夏驰和徐策 所属专栏&#xff1a;夏驰和徐策带你从零开始学C 前言&#xff1a; 第2章介绍的内置类型是由C语言直接定义的。这些类型&#xff0c;比如数字和字符&#xff0c;体现了大 多数计算机硬件本身具…

Python弹球小游戏

给在校的小妹妹做个游戏玩&#xff1a;. 弹珠游戏主要是靠坐标xy&#xff0c;接板长度&#xff0c;球的半径等决定&#xff1a; # -*- coding: utf-8 -*- # Author : Codeooo # Time : 2022/04/29import sys import time import random import pygame as pgprint("&q…

TCP三次握手,四次挥手,SYN泛洪攻击

目录 一.三次握手 二.SYN泛洪攻击概念 三.四次挥手 一.三次握手 当客户端调用connect连接服务器时,底层会发生“三次握手”&#xff0c;握手成功&#xff0c;建立连接,connect解阻塞&#xff0c;继续执行。 TCP报头&#xff1a; 三次握手过程&#xff1a; 客户端发出SYN请求…

P16 电路定理——巧妙-灵性-智慧

1、诺顿定理的证明 诺顿定理的证明&#xff0c; 回忆戴维南定理的证明是&#xff0c;在a,b两端加上一个电流源&#xff0c;再根据叠加定理&#xff0c;就解电压Uab。 对偶原理&#xff1a; 在a,b两端加上一个电压源u&#xff0c;再根据叠加定理求A中的独立源作用是给到a&#x…

下一代计算:嵌入AI的云/雾/边缘/量子计算

计算系统在过去几十年中推动了计算机科学的发展&#xff0c;现在已成为企业世界的核心&#xff0c;提供基于云计算、雾计算、边缘计算、无服务器计算和量子计算的服务。现代计算系统解决了现实世界中许多需要低延迟和低响应时间的问题。这有助于全球各地的青年才俊创办初创企业…

【学会动态规划】环形子数组的最大和(20)

目录 动态规划怎么学&#xff1f; 1. 题目解析 2. 算法原理 1. 状态表示 2. 状态转移方程 3. 初始化 4. 填表顺序 5. 返回值 3. 代码编写 写在最后&#xff1a; 动态规划怎么学&#xff1f; 学习一个算法没有捷径&#xff0c;更何况是学习动态规划&#xff0c; 跟我…

用友时空KSOA SQL注入漏洞[2023-HW]

用友时空KSOA SQL注入漏洞 一、 产品简介二、 漏洞分析三、 影响范围四、 复现环境五、 漏洞复现小龙POC检测脚本:批量SQL注入源码 六、 修复建议**临时缓解方案**升级修复方案 免责声明&#xff1a;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供…

vue3+vite+pinia

目录 一、项目准备 1.1、Vite搭建项目 1.2、vue_cli创建项目 二、组合式API(基于setup) 2.1、ref 2.2、reactive 2.3、toRefs 2.4、watch和watchEffect 2.5、computed 2.6、生命周期钩子函数 2.7、setup(子组件)的第一个参数-props 2.8、setup(子组件)的第二个参数…

springboot父服务下创建子服务,依赖管理

在微服务中&#xff0c;我们会有一个父服务&#xff0c;下面建多个子服务。 1、创建子服务 2、子服务依赖父服务 引入父工程 3、父工程管理子服务 4、查看依赖正常 结束&#xff01;&#xff01; hy:25 知识是对灵魂的澄清。 -- 柏拉图《理想国》

29 深度玻尔兹曼机

文章目录 29 深度玻尔兹曼机29.1 背景介绍29.2 DBM的叠加方式 29 深度玻尔兹曼机 29.1 背景介绍 过去在解决BM问题的时候&#xff0c;提出过多种模型&#xff1a;RBM、SBN、DBN 其中RBM是一种有限制条件的&#xff0c;简化的BM&#xff0c;限制了隐藏层和观测层内部都没有连…

GEE学习05

修改输出绝对路径 和python里面一样&#xff0c;修改输出底图的路劲&#xff1a; Map3.save(r"C:\test\topo.html")1、浏览一下GEE帮助文档 https://developers.google.cn/earth-engine/guides 这个链接是国内外都可以正常访问的。 GEE指导主要是针对的JS&#xf…

Verdi_Annotate Macro功能

Verdi_Annotate Macro功能 介绍 When debugging designs that contain define macros, it is often helpfull to view a macro definition in the context of a macro reference. 在调试包含定义宏的设计时&#xff0c;在宏引用上下文中查看宏定义通常会很有帮助。 方法 Sh…

PDM/PLM系统建设

仅供学习使用&#xff0c;会随时更新 工程机械跨生命周期数据管理系统 来源&#xff1a;清华大学 浅论企业PDM/PLM系统建设成功经验 来源&#xff1a;e-works 作者&#xff1a;陈凡 https://articles.e-works.net.cn/pdm/article149572.htm 随着“中国制造2025”强基工程战略的…

髋关节 弹响

评估测试 https://www.bilibili.com/video/BV1A44y1j71Y/?spm_id_from333.880.my_history.page.click&vd_source3535bfaa5db8443d107998d15e88dc44 根据此视频整理所得 托马斯测试 第一种情况 如果你难于将膝关节拉到胸前&#xff0c;并感觉前面有骨性的挤压 说明你股…