SpringBoot之旅4: MyBatis 操作数据库(进阶) 动态SQL+MyBatis-Plus实战,从入门到熟练,再也不踩绑定异常、SQL拼接坑

目录

[1. 动态SQL](#1. 动态SQL)

[1.1 if标签](#1.1 if标签)

[1.2 trim标签](#1.2 trim标签)

[1.2.1 if标签的安全隐患](#1.2.1 if标签的安全隐患)

[1.2.2 解决方案--使用trim标签](#1.2.2 解决方案--使用trim标签)

[1.3 where标签](#1.3 where标签)

[1.3.1 不使用where标签的情况](#1.3.1 不使用where标签的情况)

[1.3.2 使用where标签](#1.3.2 使用where标签)

[1.4 set标签](#1.4 set标签)

[1.5 foreach标签](#1.5 foreach标签)

[1.6 include标签](#1.6 include标签)

[2. MyBatis-Plus使用](#2. MyBatis-Plus使用)

[2.1 MyBatis-Plus介绍](#2.1 MyBatis-Plus介绍)

[2.2 准备工作](#2.2 准备工作)

[2.1.1 数据准备](#2.1.1 数据准备)

[2.1.2 项目准备](#2.1.2 项目准备)

[2.3 编码](#2.3 编码)

[2.4 CRUD单元测试](#2.4 CRUD单元测试)

[3. MyBatis-Plus复杂操作](#3. MyBatis-Plus复杂操作)

[3.1 常见注解](#3.1 常见注解)

[3.1.1 @TableName](#3.1.1 @TableName)

[3.1.2 @TableField](#3.1.2 @TableField)

[3.1.3 @TableId](#3.1.3 @TableId)

[3.2 打印日志](#3.2 打印日志)

[3.3 条件构造器](#3.3 条件构造器)

[3.3.1 QueryWrapper](#3.3.1 QueryWrapper)

[3.3.2 UpdateWrapper](#3.3.2 UpdateWrapper)

[3.3.3 LambdaQueryWrapper](#3.3.3 LambdaQueryWrapper)

[3.3.4 LambdaUpdateWrapper](#3.3.4 LambdaUpdateWrapper)

[3.4 自定义SQL](#3.4 自定义SQL)


1. 动态SQL

动态 SQL 是Mybatis的强大特性之一,能够完成不同条件下不同的 sql 拼接 可以参考官方文档:

https://mybatis.net.cn/dynamic-sql.html

1.1 if标签

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

这个需求使用普通的sql语句无法完成

这个时候就需要使用动态标签来判断了,比如添加的时候出版社 publish 为非必填字段,具体实现如下

Mapper.xml实现:

UserInfoXMLMapper.java

java 复制代码
void insertUserInfoByCondition(UserInfo userInfo);

UserInfoXMLMapperTest.java

java 复制代码
@Test
    void insertUserInfoByCondition() {
        UserInfo userInfo = new UserInfo();
//        userInfo.setAge(8);
        userInfo.setUsername("xht");
        userInfo.setPassword("aaa");
        userInfo.setDeleteFlag(1);
        userInfoMapperXML.insertUserInfoByCondition(userInfo);
    }

UserInfoXMLMapper.xml

XML 复制代码
<insert id="insertUserInfoByCondition">
        insert into user_info(
        id,
        username,
        password,
        <if test="age!=null">
            age,
        </if>
        <if test="gender!=null">
            gender,
        </if>
        <if test="phone!=null">
            phone,
        </if>
        delete_flag
        )
        values(
        #{id},
        #{username},
        #{password},
        <if test="age!=null">
            #{age},
        </if>
        <if test="gender!=null">
            #{gender},
        </if>
        <if test="phone!=null">
            #{phone},
        </if>
        #{deleteFlag}
        )
    </insert>

使用if标签的字段可以不传入属性,看一下服务器的日志,发现如果在test文件中没有传入age,gender,phone的话,他们就不会出现在insert语句当中

1.2 trim标签

1.2.1 if标签的安全隐患

之前的插入用户功能,只是有一个 gender 字段可能是选填项,如果有多个字段,只是用if标签则会有很大的安全隐患。

比如,我们对上面xml中的语句稍作改变

UserInfoXMLMapper.xml

XML 复制代码
<insert id="insertUserInfoByCondition">
        insert into user_info(
        id,
        username,
        password,
        <if test="age!=null">
            age,
        </if>
        <if test="gender!=null">
            gender,
        </if>
        <if test="phone!=null">
            phone,
        </if>
        )
        values(
        #{id},
        #{username},
        #{password},
        <if test="age!=null">
            #{age},
        </if>
        <if test="gender!=null">
            #{gender},
        </if>
        <if test="phone!=null">
            #{phone},
        </if>
        )
    </insert>

UserInfoXMLMapperTest.java

java 复制代码
    @Test
    void insertUserInfoByCondition() {
        UserInfo userInfo = new UserInfo();
        userInfo.setUsername("xht");
        userInfo.setPassword("aaa");
        userInfo.setAge(12);
//        userInfo.setGender(2);
//        userInfo.setPhone("123345");
        userInfoMapperXML.insertUserInfoByCondition(userInfo);
    }

这样,如果我们有些属性输入为空,那么,很有可能最后拼出来的sql语句是有问题的,我们查看一下这里的错误日志

1.2.2 解决方案--使用trim标签

一般考虑使用标签结合标签 ,对多个字段都采取动态生成的方式

标签中有如下属性:

• prefix:表示整个语句块,以prefix的值作为前缀

• suffix:表示整个语句块,以suffix的值作为后缀

• prefixOverrides:表示整个语句块要去除掉的前缀

• suffixOverrides:表示整个语句块要去除掉的后缀

调整 UserInfoXMLMapper.xml 的插入语句为:

XML 复制代码
<insert id="insertUserInfoByCondition">
        insert into user_info
        <trim prefix = "(" suffix=")" suffixOverrides=",">
            id,
            username,
            password,
            <if test="age!=null">
                age,
            </if>
            <if test="gender!=null">
                gender,
            </if>
            <if test="phone!=null">
                phone
            </if>
        </trim>
        values
        <trim prefix = "(" suffix=")" suffixOverrides=",">
            #{id},
            #{username},
            #{password},
            <if test="age!=null">
                #{age},
            </if>
            <if test="gender!=null">
                #{gender},
            </if>
            <if test="phone!=null">
                #{phone}
            </if>
        </trim>
    </insert>

UserInfoXMLMapperTest.java不变:

java 复制代码
    @Test
    void insertUserInfoByCondition() {
        UserInfo userInfo = new UserInfo();
        userInfo.setUsername("xht");
        userInfo.setPassword("aaa");
        userInfo.setAge(12);
//        userInfo.setGender(2);
//        userInfo.setPhone("123345");
        userInfoMapperXML.insertUserInfoByCondition(userInfo);
    }

查看服务器日志,发现执行成功

查看数据库,发现数据库更新数据正确

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

• 基于 prefix 配置,开始部分加上 (

• 基于 suffix 配置,结束部分加上 )

多个 组织的语句都以 , 结尾,在最后拼接好的字符串还会以 , 结尾,会基于 suffixOverrides 配置去掉最后一个 ,

• 注意<if test="username!=null">中的 username 是传入对象的属性

1.3 where标签

1.3.1 不使用where标签的情况

UserInfoXMLMapper.java

java 复制代码
List<UserInfo> selectByCondition(UserInfo userInfo);

UserInfoXMLMapper.xml

XML 复制代码
<select id="selectByCondition" resultType="com.mxy.mybatisreviewdemo.model.UserInfo">
        SELECT * FROM user_info
        WHERE
            age = #{age}
          AND gender = #{gender}
          AND delete_flag = #{deleteFlag}
    </select>

UserInfoXMLMapperTest.java不变:

java 复制代码
@Test
    void selectByCondition() {
        UserInfo userInfo = new UserInfo();
        userInfo.setDeleteFlag(0);
        userInfo.setAge(9);
        userInfo.setGender(2);
        userInfoMapperXML.selectByCondition(userInfo);
    }

但这种固定的sql语句无法做到随时增减查询条件,在京东购物的网站里面的选择标签就需要这个动态sql语句

1.3.2 使用where标签

看下面这个场景,系统会根据我们的筛选条件, 动态组装where 条件

这种如何实现呢? 接下来我们看代码实现:

需求: 传入的用户对象,根据属性做where条件查询,用户对象中属性不为 null 的,都为查询条件. 如 username 为 "a",则查询条件为 where username="a"

UserInfoXMLMapper.javaUserInfoXMLMapper.java

java 复制代码
List<UserInfo> selectByCondition(UserInfo userInfo);

UserInfoXMLMapper.xml

XML 复制代码
<select id="selectByCondition" resultType="com.mxy.mybatisreviewdemo.model.UserInfo">
        SELECT * FROM user_info
        <where>
            <if test="age!=null">
                age = #{age}
            </if>
            <if test="gender!=null">
                AND gender = #{gender}
            </if>
            <if test="deleteFlag!=null">
                AND delete_flag = #{deleteFlag}
            </if>
        </where>
    </select>

UserInfoXMLMapperTest.java:

java 复制代码
@Test
    void selectByCondition() {
        UserInfo userInfo = new UserInfo();
//        userInfo.setAge(9);
        userInfo.setGender(2);
        userInfo.setDeleteFlag(0);
        userInfoMapperXML.selectByCondition(userInfo);
    }

<where>只会在子元素有内容的情况下才插入where子句,而且会自动去除子句的开头的AND或 OR

以上标签也可以使用<trim prefix="where" prefixOverrides="and"> 替换, 但是此种情况下, 当子元素都没有内容时, where关键字也会保留

1.4 set标签

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

接口定义: 根据传入的用户 id 属性,修改其他不为 null 的属性

UserInfoXMLMapper.java

java 复制代码
Integer updateUserByCondition(UserInfo userInfo);

UserInfoXMLMapper.xml

XML 复制代码
<update id="updateUserByCondition">
        update user_info
        <set>
            <if test="username!=null">
                username = #{username},
            </if>
            <if test="age!=null">
                age = #{age},
            </if>
            <if test="deleteFlag!=null">
                delete_flag = #{deleteFlag},
            </if>
        </set>
        where id = #{id}
    </update>

UserInfoXMLMapperTest.java

java 复制代码
@Test
    void updateUserByCondition() {
        UserInfo userInfo = new UserInfo();
        userInfo.setAge(9);
        userInfo.setUsername("lisi");
        userInfo.setAge(23);
        userInfo.setId(7);
        userInfoMapperXML.updateUserByCondition(userInfo);
    }

我们这里没有传deleteFlag,因此,可以看出set标签也可以去除多余的","

<set>:动态的在SQL语句中插入set关键字,并会删掉额外的逗号. (用于update语句中) 以上标签也可以使用<trim prefix="set" suffixOverrides=",">替换

1.5 foreach标签

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

• collection:绑定方法参数中的集合,如 List,Set,Map或数组对象

• item:遍历时的每一个对象

• open:语句块开头的字符串

• close:语句块结束的字符串

• separator:每次遍历之间间隔的字符串

需求: 根据多个userid, 删除用户数据

接口方法 UserInfoXMLMapper.java

java 复制代码
void deleteByIds(List<Integer> ids);

UserInfoXMLMapper.xml

XML 复制代码
<delete id="deleteByIds">
        delete from user_info
        where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </delete>

UserInfoXMLMapperTest.java

java 复制代码
@Test
    void deleteByIds() {
        List<Integer> list = new ArrayList<>();
        list.add(6);
        list.add(7);
        userInfoMapperXML.deleteByIds(list);
    }

查看服务器日志:

查看数据库,看到id为6,7的数据都已经被删除了

1.6 include标签

问题分析:

• 在xml映射文件中配置的SQL,有时可能会存在很多重复的片段,此时就会存在很多冗余的代码

接口方法 UserInfoXMLMapper.java

java 复制代码
    List<UserInfo> selectAll1();

    List<UserInfo> selectAll2();

UserInfoXMLMapper.xml

XML 复制代码
<select id="selectAll1" resultType="com.mxy.mybatisreviewdemo.model.UserInfo">
        select
        <include refid="allColumn"></include>
        from user_info;
    </select>

    <select id="selectAll2" resultType="com.mxy.mybatisreviewdemo.model.UserInfo">
        select
        <include refid="allColumn"></include>
        from user_info where id = 3;
    </select>

我们可以对重复的代码片段进行抽取,将其通过 标签封装到一个SQL片段,然后再通过<include>标签进行引用

<sql>定义可重用的SQL片段

<include>:通过属性refid,指定包含的SQL片段

UserInfoXMLMapperTest.java

java 复制代码
@Test
    void selectAll1() {
        System.out.println(userInfoMapperXML.selectAll1());
    }

    @Test
    void selectAll2() {
        System.out.println(userInfoMapperXML.selectAll2());
    }

2. MyBatis-Plus使用

2.1 MyBatis-Plus介绍

MyBatis-Plus(简称 MP) 是一个 MyBatis 的增强工具, 在 MyBatis 的基础上只做增强不做改变, 为简化开 发. 提高效率而生

特性:

• 润物无声: 只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑.

• 效率至上: 只需简单配置,即可快速进行单表 CRUD 操作,从而节省大量时间.

• 丰富功能: 代码生成、自动分页、逻辑删除、自动填充、拦截器等功能一应俱全.

• 广泛认可: 连续 5 年获得开源中国年度最佳开源项目殊荣,Github 累计 16K Star.

2.2 准备工作

2.1.1 数据准备

创建用户表, 并创建对应的实体类User

sql 复制代码
DROP DATABASE IF EXISTS mybatis_test;
CREATE DATABASE mybatis_test DEFAULT CHARACTER SET utf8mb4;
-- 使用数据数据
USE mybatis_test;
-- 创建表[用户表]
DROP TABLE IF EXISTS user_info;
CREATE TABLE `user_info` (
 `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.user_info( username, `password`, age, gender, phone )
VALUES ( 'admin', 'admin', 18, 1, '18612340001' );
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'zhangsan', 'zhangsan', 18, 1, '18612340002' );
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'lisi', 'lisi', 18, 1, '18612340003' );
INSERT INTO mybatis_test.user_info( username, `password`, age, gender, phone )
VALUES ( 'wangwu', 'wangwu', 18, 1, '18612340004' );

2.1.2 项目准备

  1. 创建springboot工程 2. 添加MyBatis-Plus和MySQL依赖, 配置数据库连接信息

对于 Spring Boot3

需要在pom.xml中添加依赖

XML 复制代码
<dependency>
 <groupId>com.baomidou</groupId>
 <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
 <version>3.5.5</version>
</dependency>

配置数据库 application.yml文件, 配置内容如下:

复制代码
spring:
  application:
    name: mybatis-review-demo
# ???????
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 配置打印 MyBatis日志
    map-underscore-to-camel-case: true #配置驼峰自动转换

# 配置 mybatis xml 的文件路径,在 resources/mapper 创建所有表的 xml 文件
  mapper-locations: classpath:/static/*.xml

# 数据库连接配置
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

2.3 编码

创建实体类 UserInfo 实体类的属性名与表中的字段名一一对应

java 复制代码
@Data
public class UserInfo {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private Integer gender;
    private String phone;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;
}

编写Mapper接口类

MybatisPlus提供了一个基础的 BaseMapper 接口,已经实现了单表的CRUD, 我们自定义的 Mapper只需要继承这个BaseMapper, 就无需自己实现单表CRUD了

BaseMapper.java

java 复制代码
public interface BaseMapper<T> extends Mapper<T> {
    int insert(T entity);

    int deleteById(Serializable id);

    int deleteById(T entity);

    default int deleteByMap(Map<String, Object> columnMap) {
        return this.delete((Wrapper)Wrappers.query().allEq(columnMap));
    }

    int delete(@Param("ew") Wrapper<T> queryWrapper);

    int deleteBatchIds(@Param("coll") Collection<?> idList);

    int updateById(@Param("et") T entity);

    int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);

    default int update(@Param("ew") Wrapper<T> updateWrapper) {
        return this.update((Object)null, updateWrapper);
    }

    T selectById(Serializable id);

    List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);

    void selectBatchIds(@Param("coll") Collection<? extends Serializable> idList, ResultHandler<T> resultHandler);

    default List<T> selectByMap(Map<String, Object> columnMap) {
        return this.selectList((Wrapper)Wrappers.query().allEq(columnMap));
    }

    default void selectByMap(Map<String, Object> columnMap, ResultHandler<T> resultHandler) {
        this.selectList((Wrapper)Wrappers.query().allEq(columnMap), resultHandler);
    }

    default T selectOne(@Param("ew") Wrapper<T> queryWrapper) {
        return (T)this.selectOne(queryWrapper, true);
    }

    default T selectOne(@Param("ew") Wrapper<T> queryWrapper, boolean throwEx) {
        List<T> list = this.selectList(queryWrapper);
        int size = list.size();
        if (size == 1) {
            return (T)list.get(0);
        } else if (size > 1) {
            if (throwEx) {
                throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + size);
            } else {
                return (T)list.get(0);
            }
        } else {
            return null;
        }
    }

    default boolean exists(Wrapper<T> queryWrapper) {
        Long count = this.selectCount(queryWrapper);
        return null != count && count > 0L;
    }

    Long selectCount(@Param("ew") Wrapper<T> queryWrapper);

    List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);

    void selectList(@Param("ew") Wrapper<T> queryWrapper, ResultHandler<T> resultHandler);

    List<T> selectList(IPage<T> page, @Param("ew") Wrapper<T> queryWrapper);

    void selectList(IPage<T> page, @Param("ew") Wrapper<T> queryWrapper, ResultHandler<T> resultHandler);

    List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);

    void selectMaps(@Param("ew") Wrapper<T> queryWrapper, ResultHandler<Map<String, Object>> resultHandler);

    List<Map<String, Object>> selectMaps(IPage<? extends Map<String, Object>> page, @Param("ew") Wrapper<T> queryWrapper);

    void selectMaps(IPage<? extends Map<String, Object>> page, @Param("ew") Wrapper<T> queryWrapper, ResultHandler<Map<String, Object>> resultHandler);

    <E> List<E> selectObjs(@Param("ew") Wrapper<T> queryWrapper);

    <E> void selectObjs(@Param("ew") Wrapper<T> queryWrapper, ResultHandler<E> resultHandler);

    default <P extends IPage<T>> P selectPage(P page, @Param("ew") Wrapper<T> queryWrapper) {
        page.setRecords(this.selectList(page, queryWrapper));
        return page;
    }

    default <P extends IPage<Map<String, Object>>> P selectMapsPage(P page, @Param("ew") Wrapper<T> queryWrapper) {
        page.setRecords(this.selectMaps(page, queryWrapper));
        return page;
    }
}

使用UserInfoMapper2继承BaseMapper

java 复制代码
@Mapper
public interface UserInfoMapper2 extends BaseMapper<UserInfo> {

}

2.4 CRUD单元测试

在创建出来的SpringBoot工程中,在src下的test目录下,已经自动帮我们创建好了测试类 ,我们可以直接使用这个测试类来进行测试. 编写几个单元测试, 测试基本的CRUD功能

java 复制代码
@SpringBootTest
class UserInfoMapper2Test {
    @Autowired
    UserInfoMapper2 userInfoMapper2;

    @Test
    void testInsert() {
        UserInfo user = new UserInfo();
        user.setUsername("xht");
        user.setPassword("123456");
        user.setAge(11);
        user.setGender(0);
        user.setPhone("18610001234");
        userInfoMapper2.insert(user);
    }

    @Test
    void testSelectById() {
        UserInfo user = userInfoMapper2.selectById(1L);
        System.out.println("user: " + user);
    }

    @Test
    void testSelectByIds() {
        List<UserInfo> users = userInfoMapper2.selectBatchIds(List.of(1L, 2L, 3L, 4L));
        users.forEach(System.out::println);
    }
    @Test
    void testUpdateById() {
        UserInfo user = new UserInfo();
        user.setId(1);
        user.setPassword("4444444");
        userInfoMapper2.updateById(user);
    }
    @Test
    void testDelete() {
        userInfoMapper2.deleteById(1);
    }


}

3. MyBatis-Plus复杂操作

3.1 常见注解

在上面的程序中, MyBatis是如何知道, 我们要操作的是哪张表, 表里有哪些字段呢? 我们来看下咱们Mapper的代码

java 复制代码
@Mapper
public interface UserInfoMapper2 extends BaseMapper<UserInfo> {

}

UserInfoMapper2 在继承 BaseMapper 时, 指定了一个泛型, 这个UserInfo就是与数据库表相对应的实体类

MyBatis-Plus会根据这个实体类来推断表的信息. 默认情况下:

  1. 表名: 实体类的驼峰表示法转换成蛇形表示法(下划线分割), 作为表名. 比如UserInfo -> user_info

  2. 字段:根据实体类的属性名转换为蛇形表示法作为字段名. 比如deleteFlag -> delete_flag

  3. 主键: 默认为id

那如果实体类和数据库不是按照上述规则定义的呢? MyBatis-Plus也给我们提供了一下注解, 让我们标识表的信息.

3.1.1 @TableName

修改实体类名UserInfo为 Userinfo, 重新执行测试方法testSelectById 运行结果:

java 复制代码
@Data
public class Userinfo {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private Integer gender;
    private String phone;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;
}

修改相应方法,运行结果为:

从日志可以看到, 默认查找的表名为userinfo. 我们可以通过 @TableName 来标识实体类对应的表

java 复制代码
@Data
@TableName("user_info")
public class Userinfo {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private Integer gender;
    private String phone;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;
}

使用@TableName来标识实体类对应的数据库表名,则可以正常运行;

3.1.2 @TableField

修改属性名 deleteFlag 为 deleteflag, 重新执行测试方法testSelectById 运行结果:

java 复制代码
@Data
@TableName("user_info")
public class Userinfo {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private Integer gender;
    private String phone;
    private Integer deleteflag;
    private Date createTime;
    private Date updateTime;
}

从日志可以看到, 根据属性名转换后的字段名为: deleteflag.

我们可以通过 @TableField 来标识 对应的字段名

java 复制代码
@Data
@TableName("user_info")
public class Userinfo {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private Integer gender;
    private String phone;
    @TableField("delete_flag")
    private Integer deleteflag;
    private Date createTime;
    private Date updateTime;
}

再次运行程序, 程序运行结果正常.

3.1.3 @TableId

修改属性名 id 为 userId, 重新执行测试方法testSelectById

java 复制代码
@Data
@TableName("user_info")
public class Userinfo {
    private Integer userId;
    private String username;
    private String password;
    private Integer age;
    private Integer gender;
    private String phone;
    @TableField("delete_flag")
    private Integer deleteflag;
    private Date createTime;
    private Date updateTime;
}

运行结果:

我们可以通过 @TableId 来 指定对应的主键

java 复制代码
@Data
@TableName("user_info")
public class Userinfo {
    @TableId("id")
    private Integer userId;
    private String username;
    private String password;
    private Integer age;
    private Integer gender;
    private String phone;
    @TableField("delete_flag")
    private Integer deleteflag;
    private Date createTime;
    private Date updateTime;
}

运行结果:

3.2 打印日志

为了方便学习, 我们可以借助日志, 查看Mybatis-Plus执行的SQL语句, 参数和执行结果 Mybatis-Plus配置日志如下:

java 复制代码
mybatis-plus:
  configuration: # 配置打印 MyBatis日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

在配置文件当中添加了打印日志相关的配置之后,可以看到sql执行的相关信息

3.3 条件构造器

入门程序里的使用, 都是简单的CRUD, 在实际的应用场景中, 我们还需要使用更复杂的操作, MyBatisPlus 也给我们提供了相应的支持.

MyBatis-Plus 提供了一套强大的条件构造器(Wrapper), 用于构建复杂的数据库查询条件. Wrapper 类 允许开发者以链式调用的方式构造查询条件, 无需编写繁琐的 SQL 语句, 从而提高开发效率并减少 SQL 注入的风险.

以下是主要的 Wrapper 类及其功能:

• AbstractWrapper:这是一个抽象基类 , 提供了所有 Wrapper 类共有的方法和属性. 详细参考官网介绍: 条件构造器

• QueryWrapper:用于构造查询条件, 在AbstractWrapper的基础上拓展了一个select方法, 允许指 定查询字段.

• UpdateWrapper: 用于构造更新条件, 可以在更新数据时指定条件.

• LambdaQueryWrapper:基于 Lambda 表达式的查询条件构造器, 它通过 Lambda 表达式来引用 实体类的属性,从而避免了硬编码字段名.

• LambdaUpdateWrapper: 基于 Lambda 表达式的更新条件构造器, 它允许你使用 Lambda 表达 式来指定更新字段和条件,同样避免了硬编码字段名的问题

3.3.1 QueryWrapper

QueryWrapper并不只用于查询语句, 无论是修改, 删除, 查询, 都可以使用QueryWrapper来构建查询条 件.

查询

完成下述SQL查询

sql 复制代码
SELECT id,username,password,age FROM user_info WHERE age = 18 AND username like "%min%"

在navicat中的运行结果为:

测试代码

java 复制代码
@Test
    void testQueryWrapper(){
        QueryWrapper<UserInfo> userInfoQueryWrapper = new QueryWrapper<UserInfo>()
                .select("id","username","password","age")
                .eq("age",18)
                .like("username","min");
        List<UserInfo> userInfos = userInfoMapper2.selectList(userInfoQueryWrapper);
        userInfos.forEach(x-> System.out.println(x));
    }

查看服务器日志,看到,可以正常运行

注意

复制代码
QueryWrapper<UserInfo> userInfoQueryWrapper = new QueryWrapper<>();
  • QueryWrapper:MyBatis-Plus 提供的条件构造器
  • <UserInfo>:告诉 MyBatis-Plus(MP) 你要查的是哪张表(对应 user_info 表)

更新

完成下述SQL查询

java 复制代码
UPDATE user_info SET delete_flag=? WHERE age < 20

测试代码

java 复制代码
@Test
    void testUpdateByQueryWrapper(){
        QueryWrapper<UserInfo> userInfoQueryWrapper = new QueryWrapper<UserInfo>()
                .lt("age",20);
        UserInfo userInfo = new UserInfo();
        userInfo.setDeleteFlag(1);
        userInfoMapper2.update(userInfo, userInfoQueryWrapper);
    }

• lt : "less than" 的缩写,表示小于.

• le : "less than or equal to"的缩写, 表示小于等于

• ge : "greater than or equal to" 的缩写, 表示大于等于.

• gt : "greater than" 的缩写, 表示大于.

• eq : "equals" 的缩写, 表示等于.

• ne : "not equals" 的缩写, 表示不等于
注意:上述语句的执行条件更新

复制代码
userInfoMapper2.update(userInfo, userInfoQueryWrapper);

MP 的 update 方法两个参数

  1. 第一个参数:要更新的值(SET ...)

  2. 第二个参数:条件(WHERE ...)

删除

完成下述SQL查询

sql 复制代码
DELETE FROM user_info WHERE age = 18

测试代码

java 复制代码
@Test
void testDeleteByQueryWrapper(){
 QueryWrapper<UserInfo> userInfoQueryWrapper = new QueryWrapper<UserInfo>()
 .eq("age",11);
 userInfoMapper.delete(userInfoQueryWrapper);
}

如果在执行sql语句的过程中会出现很大的id的情况,那我们需要设置数据库id字段自增

java 复制代码
@Data
public class UserInfo {
    @TableId(type = IdType.AUTO) // 关键:type = IdType.AUTO 表示交给数据库自增
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private Integer gender;
    private String phone;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;
}

我们打算删除这一条记录

发现age=11的记录已经被删除了

3.3.2 UpdateWrapper

对于更新, 我们也可以直接使用 UpdateWrapper, 在不创建实体对象的情况下, 直接设置更新字段和条件.

基础更新: 完成下述SQL查询

sql 复制代码
UPDATE user_info SET delete_flag=0, age=5 WHERE id IN (1,2,3)

测试代码

java 复制代码
@Test
    void testUpdateByUpdateWrapper(){
        UpdateWrapper<UserInfo> updateWrapper = new UpdateWrapper<UserInfo>()
                .set("delete_flag",0)
                .set("age", 5)
                .in("id", List.of(1,2,3));
        userInfoMapper2.update(updateWrapper);
    }

运行结果为:

基于SQL更新: 完成下述SQL查询

java 复制代码
UPDATE user_info SET age = age+10 WHERE id IN (1,2,3)

测试代码

java 复制代码
@Test
void testUpdateBySQLUpdateWrapper(){
 UpdateWrapper<UserInfo> updateWrapper = new UpdateWrapper<UserInfo>()
 .setSql("age = age+10")
 .in("id", List.of(1,2,3));
 userInfoMapper2.update(updateWrapper);
}

3.3.3 LambdaQueryWrapper

QueryWrapper 和 UpdateWrapper存在一个问题, 就是需要写死字段名, 如果字段名发生变更, 可能会因为测试不到位酿成事故

MyBatis-Plus 给我们提供了一种基于Lambda表达式的条件构造器, 它通过 Lambda 表达式来引用实体 类的属性,从而避免了硬编码字段名, 也提高了代码的可读性和可维护性.

• LambdaQueryWrapper

• LambdaUpdateWrapper

分别对应上述的QueryWrapper和UpdateWrapper

java 复制代码
@Test
    void testLambdaQueryWrapper(){
        QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<UserInfo>();
        queryWrapper.lambda()
                .select(UserInfo::getUsername, UserInfo::getPassword,UserInfo::getAge)
                .eq(UserInfo::getId, 1);
        userInfoMapper2.selectList(queryWrapper).forEach(System.out::println);
    }

切换到 Lambda 模式(核心!)

复制代码
queryWrapper.lambda()

作用:从普通字符串方式 → 切换成 方法引用方式(编译安全)

好处:

  • 不会写错字段名
  • IDE 自动提示
  • 重构不会出错

注意:

  1. UserInfo::getUsername 等价于 →数据库字段 username
方法引用 对应的数据库字段
UserInfo::getId id
UserInfo::getUsername username
UserInfo::getAge age
UserInfo::getDeleteFlag delete_flag

MP 会自动把驼峰 → 下划线!

  1. 对于下面语句的解释
java 复制代码
userInfoMapper2.selectList(queryWrapper).forEach(System.out::println);

我们来分成两部分解释这个问题:

a. userInfoMapper2.selectList(queryWrapper)

去数据库查询,返回一组(多个)用户数据 → 得到一个 List 集合

b. .forEach(System.out::println)

遍历刚才查到的列表,把每一个用户对象 打印输出!

拆解:

  • forEach():循环遍历集合里的每一个元素
  • System.out::printlnJava8 方法引用
    • 等价于:每一个元素 -> 打印(元素)
    • 即类似于lamda表达式x->System.out.println(x)

3.3.4 LambdaUpdateWrapper

• LambdaUpdateWrapper用法和 LambdaQueryWrapper相似

java 复制代码
@Test
    void testLambdUpdateByUpdateWrapper(){
        UpdateWrapper<UserInfo> updateWrapper = new UpdateWrapper<UserInfo>();
        updateWrapper.lambda()
                .set(UserInfo::getDeleteFlag, 0)
                .set(UserInfo::getAge, 5)
                .in(UserInfo::getId, List.of(1,2,3));
        userInfoMapper2.update(updateWrapper);
    }

查看数据库,结果为:

3.4 自定义SQL

在实际的开发中, MyBatis-Plus提供的操作不能满足我们的实际需求, MyBatis-Plus 也提供了自定义 SQL的功能, 我们可以利用Wrapper构造查询条件, 再结合Mapper编写SQL

代码示例1

完成下述SQL查询

sql 复制代码
select id,username,password,age FROM user_info WHERE username = "admin"

UserInfoMapper2.java

java 复制代码
@Mapper
public interface UserInfoMapper2 extends BaseMapper<UserInfo> {
    @Select("select id,username,password,age FROM user_info ${ew.customSqlSegment}")
    List<UserInfo> queryUserByCustom(@Param(Constants.WRAPPER) Wrapper<UserInfo> wrapper);
}

UserInfoMapper2Test.java

java 复制代码
 @Test
    void queryUserByCustom() {
        QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<UserInfo>()
                .eq("username","admin");
        userInfoMapper2.queryUserByCustom(queryWrapper).forEach(System.out::println);
    }

注意事项:

• 参数命名:在自定义 SQL 时, 传递 Wrapper 对象作为参数时, 参数名必须为 ew ,或者使用注解 @Param(Constants.WRAPPER) 明确指定参数为 Wrapper 对象.

• 使用 {ew.customSqlSegment} :在 SQL 语句中,使用 **{ew.customSqlSegment}** 来引用 Wrapper 对象生成的 SQL 片段.

• 不支持基于 entity 的 where 语句:自定义 SQL 时,Wrapper对象不会基于实体类自动生成 where 子句,你需要手动编写完整的 SQL 语句.

代码示例2

MyBatis-Plus 在 MyBatis的基础上只做增强不做改变, 所以也支持XML的实现方式

上述功能也可以使用XML的方式完成

  1. 配置mapper路径
java 复制代码
mybatis-plus:
  configuration: # 配置打印 MyBatis日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
  mapper-locations: "classpath*:/static/mapper/**.xml" # Mapper.xml
  type-aliases-package: com.mxy.mybatisreviewdemo.model
  1. 定义方法
java 复制代码
List<UserInfo> queryUserByCustom2(@Param(Constants.WRAPPER) Wrapper<UserInfo> wrapper);
  1. 编写XML
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.mxy.mybatisreviewdemo.mapper.UserInfoMapper2">
    <select id="queryUserByCustom2" resultType="com.mxy.mybatisreviewdemo.model.UserInfo">
        select id,username,password,age
        FROM user_info ${ew.customSqlSegment}
    </select>
</mapper>
  1. 测试
java 复制代码
@Test
    void queryUserByCustom2() {
        QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<UserInfo>();
            queryWrapper.lambda().eq(UserInfo::getAge, 18);
        userInfoMapper2.queryUserByCustom2(queryWrapper).forEach(System.out::println);
//        LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<UserInfo>()
//                .eq(UserInfo::getAge, 18); // 显式写 <UserInfo>,让泛型推断正确
//
//        userInfoMapper2.selectList(queryWrapper)
//                .forEach(System.out::println);
    }

代码示例3

完成下述SQL查询

sql 复制代码
UPDATE user_info SET age = age+10 WHERE id IN (1,2,3)

Mapper:

java 复制代码
@Update("UPDATE user_info SET age = age+ #{addAge} ${ew.customSqlSegment}")
void updateUserByCustom(@Param("addAge") int addAge, @Param("ew")
Wrapper<UserInfo> wrapper);
  • #{addAge}:动态参数,安全传值

  • ${ew.customSqlSegment}:自动拼接 WHERE 条件

  • @Param("addAge")#{addAge}

  • @Param("ew")${ew.customSqlSegment}

测试代码

java 复制代码
@Test
void updateUserByCustom(){
 QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<UserInfo>()
 .in("id",List.of(1,2,3));
 userInfoMapper.updateUserByCustom(10, queryWrapper);
}
  • 10 → 传入 #{addAge}
  • wrapper → 自动生成 WHERE id IN (1,2,3)

查看服务器日志

注意:

1.下面的两种写法一样

java 复制代码
// 1. 手写 ew(最常见)
@Param("ew")

// 2. 用常量(最规范、最推荐)
@Param(Constants.WRAPPER)
  1. 总结一下:

通过注解写自定义 SQL,用 #{} 传普通参数,用 ${ew.customSqlSegment} 自动拼接条件,配合 @Param ("ew") 绑定 Wrapper,就能灵活、安全地实现动态更新。

相关推荐
大数据新鸟2 小时前
设计模式详解——模板方法模式
java·tomcat·模板方法模式
Boop_wu2 小时前
[Mybatis] MyBatis 快速入门教程(2)
mybatis
无籽西瓜a2 小时前
【西瓜带你学设计模式 | 第四期 - 抽象工厂模式】抽象工厂模式 —— 定义、核心结构、实战示例、优缺点与适用场景及模式区别
java·后端·设计模式·软件工程·抽象工厂模式
always_TT2 小时前
内存泄漏是什么?如何避免?
android·java·开发语言
智象科技2 小时前
告警自动化赋能运维:意义与价值解析
网络·数据库·人工智能·自动化·告警·一体化运维·ai运维
源远流长jerry2 小时前
在云环境中部署 NFV:OpenStack 讲解
数据库·openstack
白鸽梦游指南2 小时前
docker仓库的工作原理及搭建仓库
java·docker·eureka
_院长大人_2 小时前
Spring Boot 3.3 + Atomikos 分布式事务日志路径配置踩坑记录
spring boot·分布式·后端
java1234_小锋2 小时前
Java高频面试题:怎么实现Redis的高可用?
java·开发语言·redis