焚决糟糕篇

复制代码
//这个代码是MyBatis-Plus添加时为啥返回的时布尔值
@PostMapping("/add")
public R insert(@RequestBody GiftInfo giftInfo) {
    boolean save = giftInfoService.save(giftInfo);
    return R.ok(save);
}
问题 答案
为什么 save() 返回 boolean MyBatis-Plus 的设计约定,表示"是否成功影响了数据库行"
能不能不用 boolean 框架如此,你只能接受;但你可以选择是否检查它
要不要根据 boolean 返回错误? 建议检查,避免"静默失败";更推荐配合全局异常处理
最佳实践? 如果业务要求严格,就判断 if (!save) throw new BizException(...);否则信任异常机制

springboot+springMVC思维导图

springboot

SpringMvc

复制代码
/*mybatisPlus的注解:表示当前的类与数据库的表
*把表明是gift_info的表,和当前实体类绑定
*默认情况下是可以不写的,只有当类名与表名不一致的时候手动添加*/
@TableName("gift_info")
public class GiftInfo extends Model<GiftInfo> {
    /*gift_info是列名会自动匹配到属性giftId
    * 把列名gift_Id和当前属性的属性绑定
    * type = IdType.AUTO表示主键自增
    * @TableId标明谁是主键里面方法有(雪花算法(默认),自动递增,UUID,手动)
    * 如果没有属性上写@TableId那末就会有一些方法在调用的情况下报错因为找不到主键
    * 如果属性名是Id,列表主键也是Id,那末就可以像@TableName一样不用写@TableId了*/
    @TableId(value = "gift_id")

@TableId的注意事项

默认行为:未标注@TableId,默认使用雪花算法

主键标识:自动把名字时id的属性当作主键

代码生成建议:每次生成实体类之后,都需要手动检验主键字段和策略

@TableField

复制代码
@TableField("gift_name")
private String gName;//他是属性名与数据库列名不一致时加

非数据库字段标记

复制代码
如果实体类中有一个属性是数据库中没有的字段在生成sql也会把这个没有的字段添加到sql中 导致
    sql语句无法执行
    所以需要手动排除表中不存在的属性
@TableField(exist = false)
private Integer version;

查询排除

复制代码
​
@TableField(select = false)
private Double giftRatio;
​
表示数据库中,有这个属性就添加,修改的时候也可以使用,只是在查询的时候,默认不查该属性

条件过滤策略

复制代码
//NOT_EMPTY当前属性作为参数的时候不能时空字符串
     @TableField(value = "gift_name", whereStrategy =FieldStrategy.NOT_EMPTY,condition = SqlCondition.LIKE)
    private String gName;
​
//condition = SqlCondition.LIKE)配置之后查询的条件变成了like模糊查询

更新表达式

复制代码
@TableField(updateStrategy = FieldStrategy.NOT_EMPTY,update = "%s+555")
private Double giftPrice;

物理删除就是直接去数据库删除

逻辑删除就是标记数据库状态,查询的时候自动过滤标记数据

复制代码
/*逻辑删除的前提必须数据库标记的字段如果这个表没有删除标记,记得新建
    *  如果一个实体类添加了逻辑删除之后,后续默认都是is_del = 0的查询条件
    *  这个是框架自动添加的*/
//是否删除 0否(正常数据) 1是(已经删除的数据)
    @TableLogic(value = "0",delval = "1")
    private Integer isDel;

条件构造器在where里出现的内容都归构造器管

QueryWrapper和UpdateWrapper区别和用法

*QueryWrapper 用来查(SELECT),UpdateWrapper 用来改(UPDATE)------一个只管"筛",一个既能"设"又能"筛"。**

什么是乐观锁什么又是悲观锁它们的概念如下

乐观锁:它是假设不会被并发修改,仅仅是在更新的时候校验版本号

悲观锁:它是默认数据会被修改,然后通过锁机制,保证操作的原子性

实现方式

哪个表是想要加乐观锁功能,就需要给这个表加一个字段

复制代码
@Version
private Integer version;
复制代码
@Test
void f21(){
    //修改之前,实体类,需要从数据库中查询出来
    GiftInfo giftInfo = giftInfoService.getById(22);
    giftInfo.setGName("小娇妻");
​
    new Thread(()->{
        giftInfoService.updateById(new GiftInfo(22,"画画的北北",3));
    }).start();
    new Thread(()->{
        giftInfoService.updateById(new GiftInfo(22,"憨憨的妹妹",3));
    }).start();
    giftInfoService.updateById(giftInfo);
}

注意事项

必须注册拦截插件

更新的时候偶尔会抛出异常

OptimisticLockException

多线程并发更新的时候,后置的更新操作会因为版本号不匹配而失败

Mybatis-Plus分页插件使用限制

警告

与springBoot中使用的PageHelper插件存在冲突,不可以同时使用

默认不加载分页插件,寻要手动注册,收入引入依赖

九、常用方法

1、Dao接口核心方法

类型 方法名 说明 查询 selectById(主键) 根据主键ID查询单个实体 selectBatchids(主键集合) 批量ID查询 selectList(Wrapper w) 根据条件查询列表 添加 insert(T t) 新增单条 修改 updateById(T t) 根据Id更新 删除 deleteById(主键) 根据Id删除

2、Service层方法

查询方法

getById(主键); //根据id查询单条 getOne(Wrapper); //查询单条记录(多结果报错) list(Wrapper); //查询列表 如果wrapper不传或是null,查询全部 count(Wrapper); //查询总共多少条数据 增删改

save(T t); //新增数据 saveBatch(Collection<T>) //批量添加,注意,一些业务中,不能使用循环操作数据库,要改用批量添加 removeById(主键); //根据id删除 updateById(T); //根据主键更新,修改的值是实体类中其他属性的非空值 update(UpdateWrapper); //条件更新 updateBatchById(Collection<T> ) //批量更新,根据实体类中的主键作为where条件

MyBatis

在一些特殊情况下,需要使用多表联查,但是你不想使用MyBatisPuls提供的insql子查询的话这样你还可以使用MyBatis自带的注解进行多表联查

现在开发模式,是不推荐使用xml文件的,那么就尽可能在项目中,减少出现xml文件

MyBatis 注解

重点 1. Select查询

复制代码
public interface AdminUserDao extends BaseMapper<AdminUser> {
    /*
* 相当于 xml文件中的select标签
*      <select id = "selectByUname" resultMap = "AdminUser">
           select * from admin_user where username = #{username}
*      </select>*/
    @Select("select * from admin_user where username = #{username}")
    AdminUser selectByUname (String username);

2.多表联查 重点

复制代码
/*根据角色的名称,查询该角色下有哪些用户
* sql语句换行的时候,记得注意首尾呼应的空格,如果没有空格,会sql错误*/
@Select("select user.* from admin_user user,admin_role role" +
         "where user.role_id and" +
         "role.name = #{name}")
List<AdminUser> selectListByPoleName(String name);

3.条件查询QueryWrapper作为查询条件

不建议使用,建议使用QueryWrapper作为查询条件

复制代码
/**
 * 根据昵称查询用户列表的动态SQL查询方法
 * 
 * 为什么使用<script>而不是普通的<select>注解:
 * 1. <script>标签允许在注解中使用MyBatis的动态SQL特性,如<where>、<if>等标签
 * 2. 普通的@Select注解只能编写静态SQL,无法实现条件判断和动态拼接
 * 3. 使用<script>可以在注解形式中实现类似XML配置文件的动态SQL功能
 * 
 * SQL执行逻辑:
 * - 查询admin_user表的所有字段
 * - <where>标签自动处理WHERE子句,避免多余的"and"关键字
 * - <if>标签判断参数条件:当name不为null且不为空字符串时,才添加昵称查询条件
 * - 最终实现根据昵称模糊或精确查询用户的功能
 */
//根据昵称查询用户信息,如果不传入昵称,则查询全部信息
//尽量把业务判断放在service层完成,减少sql语句中的逻辑
@Select("<script>" +
        "select * from admin_user " +
        "<where>" +
        "<if test='name != null and name != \"\"'>" +
        "and nickname = #{name} " +
        "</if>" +
        "</where>" +
        "</script>")
List<AdminUser> selectListByNickname(String name);
复制代码
@SpringBootTest
public class Mpdemo02ApplicationTests {
    @Resource
    AdminUserDao adminUserDao;
    @Test
void f1(){
        adminUserDao.selectListByNickname("");
    }
​
}

4.使用QueryWrapper作为查询条件 重点重点

如果是单表查询,依然不建议自己写sql

复制代码
/**
     * 使用MyBatis Plus的QueryWrapper进行条件查询
     * 
     * ${ew.customSqlSegment} 的含义:
     * - 这是MyBatis Plus提供的动态SQL机制
     * - ew是QueryWrapper对象的引用,customSqlSegment是其属性
     * - 该属性会自动解析QueryWrapper中设置的查询条件,并转换为对应的SQL片段
     * - 例如:queryWrapper.eq("age", 18) 会被转换为 "AND age = ?"
     * 
     * 为什么使用${}而不是#{}:
     * 1. ${}是直接字符串替换,适用于动态表名、列名或SQL片段
     * 2. #{}是参数占位符,会进行预编译处理,适用于具体的参数值
     * 3. customSqlSegment本身就是一个完整的SQL片段(包含AND/OR等关键字),不能作为普通参数处理
     * 4. 使用#{}会导致SQL语法错误,因为会把整个SQL片段当作一个字符串参数
     * 
     * @Param("ew")的作用:
     * Mapper 是封装数据访问逻辑、负责数据库与 Java 对象映射的接口(MyBatis 中常用)
     * - 指定参数名称,使MyBatis能够正确识别和绑定QueryWrapper对象
     * - 在SQL中通过"ew"引用这个参数对象
     * - 实现了QueryWrapper与Mapper接口的映射关联
     */
    @Select("select * from admin_user ${ew.customSqlSegment}")
    List<AdminUser> selectListByQueryWrapper(@Param("ew") QueryWrapper<AdminUser> queryWrapper);
}
复制代码
@Resource
AdminUserDao adminUserDao;
​
​
@Override
​
/**
 * 根据条件查询管理员用户列表
 * @param adminUser 查询条件对象,包含昵称等筛选条件
 * @return 符合条件的管理员用户列表,按UID降序排列
 */
public List<AdminUser> queryListTest(AdminUser adminUser) {
    // 创建查询构造器,用于构建复杂的查询条件
    QueryWrapper<AdminUser> queryWrapper = new QueryWrapper<>();
    
    // 判断传入的adminUser对象不为null且昵称不为空字符串
    if (adminUser != null && adminUser.getNickname() != null && !adminUser.getNickname().isEmpty()) {
        // 添加等值查询条件:nickname等于传入的昵称值
        queryWrapper.eq("nickname", adminUser.getNickname());
    }
    
    // 添加排序条件:按照uid字段进行降序排列
    queryWrapper.orderByDesc("uid");
    
    // 调用DAO层方法执行查询,传入构造好的查询条件
    return adminUserDao.selectListByQueryWrapper(queryWrapper);
}

创建并配置跨域过滤器

复制代码
@Configuration
public class CorsConfig {
    @Bean
    /**
     * 创建并配置跨域过滤器
     * 
     * @return 配置好的CorsFilter实例
     */
    public CorsFilter corsFilter() {
        // 创建基于URL的CORS配置源
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        
        // 注册CORS配置,"/**"表示对所有路径生效,createCorsConfig()返回具体的CORS配置规则
        source.registerCorsConfiguration("/**", createCorsConfig());
        
        // 创建并返回CORS过滤器,使用上面配置的source作为参数
        return new CorsFilter(source);
    }
    private CorsConfiguration createCorsConfig() {
        //详细的配置跨域的规则
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        //配置允许哪些路径可以访问
        corsConfiguration.addAllowedOrigin("http://127.0.0.1:5173");
        corsConfiguration.addAllowedOrigin("http://localhost:5173");
        corsConfiguration.addAllowedOrigin("http://192.168.0.151:5173");
        //配置允许请求头信息
        corsConfiguration.addAllowedHeader("*");
        //允许任何请求方式
        corsConfiguration.addAllowedMethod("*");
        //允许cookie,这个不允许的话,会导致session失败
        corsConfiguration.setAllowCredentials(true);
        //超时的时间
        corsConfiguration.setMaxAge(3600L);
        return corsConfiguration;
    }
}
复制代码
@Resource
AdminUserService adminUserService;
@Test
void f2(){
    AdminUser adminUser = new AdminUser();
    //adminUser.setNickname("管理员小明");
    adminUserService.queryListTest(adminUser);
}

5.一对一查询

复制代码
/**
     * @Results 与 @Select 的联系:
     * - @Select 定义了要执行的 SQL 查询语句,获取基本的用户数据
     * - @Results 定义了如何将查询结果映射到 Java 对象,包括复杂的一对一关联关系
     * - 两者配合使用,先执行 SQL 查询,再根据结果映射规则构造完整的 AdminUser 对象
     * 
     * 方法作用总结:
     * - 根据用户ID查询用户基本信息
     * - 同时通过一对一关联查询,获取该用户所属的角色信息
     * - 返回包含完整用户信息和角色信息的 AdminUser 对象
     */
//@Results:相当于一个 <resultMap> 容器,里面包含多个 @Result
    //@Result:定义单个字段的映射规则
    //一对一查询,在查询用户信息的同时,把角色信息一起查询出来
    //@Results的id属性,是随意的,可以重复用,但是不能再本类中重名
    @Results(
           id = "hhh",value = {
                   //查询的结果,给自己的property属性
            //配置,把列role_id的属性,传给@One中配置的查询,返回一个AdminRole对象
            //赋值给role属性
            //@One(select = "可以直接使用mybatisPlus提供的方法,也可以自己写一个")
            @Result(property = "role",column = "role_id",
            one = @One(select = "com.javasm.mpdemo02.adminuser.dao" +
                    ".AdminRoleDao.selectAdminRoleByRid")),
            //可以选择MybatisPlus提供的方法,但是如果使用了,就必须在实体类中
            //标明主键是谁否则报错
            // 这个注解的作用是将数据库查询结果中的 role_id 字段映射到 AdminUser 对象的 roleId 属性上
            // 简单来说,就是让程序知道数据库里的 role_id 这一列的数据应该放到对象的 roleId 这个变量里
            @Result(property = "roleId",column = "role_id")
           }
    )
    @Select("select * from admin_user where uid = #{uid}")
    AdminUser selectUserByUidWirhRole(Integer uid);
}
复制代码
 @Select("select * from admin_role where rid = #{rid}")
    
    AdminRole selectAdminRoleByRid(Integer id);
复制代码
@Test
void f3(){
    AdminUser adminUser = adminUserDao.selectUserByUidWirhRole(1);
    System.out.println(adminUser);

}
复制代码
//这个属性,在数据库中是没有的,防止MybatisPlus报错所以添加这个注解
//在MybatisPlus注解中,是不会去识别这个注解的
@TableField(exist = false)
private AdminRole role;

在用mybatisPlus中执行Sql的时候这个Preparing: select * from admin_user where uid = ?记住打印台上这个有一个就证明执行了一次大佬说了这个如何分辨哪个是重点!!!

复制代码
@Select("select menu.* from admin_menu menu,rel_admin_role_menu rel " +
        "WHERE menu.mid = rel.mid and rel.rid in ${rids}")
List<AdminMenu> selectMenuListByRidSet(Integer rid);

//rel.rid IN ${rids} 的意思是:rel 表中的 rid 字段的值必须包含在 rids 这个由 Java 传入并直接拼接到 SQL 中的值列表里。

🌐 项目结构与调用流程图(AdminMenu 模块)

text

编辑

复制代码
┌──────────────────────┐
│   AdminMenuController │  ← 前端请求入口
└─────────┬────────────┘
          │
          ▼
┌──────────────────────┐
│ AdminMenuServiceImpl │  ← Service 实现类,处理业务逻辑
└─────────┬────────────┘
          │
          ▼
┌──────────────────────┐
│     AdminMenuDao     │  ← 数据访问层,执行 SQL 查询
└─────────┬────────────┘
          │
          ▼
┌──────────────────────┐
│    AdminMenu.java    │  ← 实体类,对应数据库表 admin_menu
└──────────────────────┘

🔗 各层说明与对应关系

层级 类/接口 作用
Controller AdminMenuController 接收前端 HTTP 请求(如 GET /menu/list),调用 Service 方法。
Service AdminMenuService(接口) 定义业务方法,如 List<AdminMenu> getMenuByRids(List<Integer> rids);
Service Impl AdminMenuServiceImpl 实现业务逻辑,调用 Dao 查询数据,可能包含权限判断、缓存等。
DAO AdminMenuDao 定义 Mapper 接口,使用 MyBatis 注解或 XML 映射 SQL,如 @Select("... IN ${rids}")
Entity AdminMenu Java 对象,映射数据库表 admin_menu,字段对应列(如 mid, name, url 等)

🔄 调用流程示例(以查询菜单为例)

text

编辑

复制代码
[前端请求] → Controller → Service → DAO → 数据库 → 返回结果 → 响应前端

具体步骤:

  1. 前端请求:GET /api/menu/list?rids=1,2

  2. AdminMenuController 接收参数 rids(List 或 String)

  3. 调用 adminMenuService.getMenuByRids(rids)

复制代码
AdminMenuServiceImpl

中:

  • rids 转为字符串 "1,2"(或集合)

  • 调用 adminMenuDao.selectByRids(rids)(注意:⚠️ 不推荐用 ${rids}

复制代码
AdminMenuDao

执行 SQL:

sql

编辑

复制代码
SELECT * FROM admin_menu menu
INNER JOIN rel_admin_role_menu rel ON menu.mid = rel.mid
WHERE rel.rid IN ${rids}  -- ⚠️ 这里是危险写法!
  1. 结果返回为 List<AdminMenu>,最终响应给前端。

⚠️ 关键提醒(安全问题)

  • ❌ 使用 ${rids}不安全的,可能导致 SQL 注入。

  • ✅ 正确做法是使用

    复制代码
    <foreach>
    复制代码
    #{}

    xml

    编辑

    复制代码
    <select id="selectByRids" resultType="AdminMenu">
        SELECT menu.* 
        FROM admin_menu menu
        INNER JOIN rel_admin_role_menu rel ON menu.mid = rel.mid
        WHERE rel.rid IN
        <foreach collection="rids" item="rid" open="(" separator="," close=")">
            #{rid}
        </foreach>
    </select>

✅ 总结一句话流程图

text

编辑

复制代码
Controller → Service → DAO → SQL → Database → Entity → Response

各层职责清晰,实体类映射数据库,DAO 处理 SQL,Service 封装逻辑,Controller 提供接口。

xml

编辑

复制代码
WHERE rel.rid IN
<foreach collection="rids" item="rid" open="(" separator="," close=")">
    #{rid}
</foreach>

1. 目的

安全地构建 SQL 中的 IN 条件,用于查询 rel.rid 匹配多个值的记录。


2. 各属性含义

  • collection="rids":指定传入的参数名(通常是 ListSet 或数组),如 Java 调用时传 Map<String, Object>"rids": [1,2,3],或方法参数为 @Param("rids") List<Integer> rids

  • item="rid":遍历集合时,每个元素的临时变量名。

  • open="(":拼接结果开头加 (

  • separator=",":元素之间用 , 分隔。

  • close=")":拼接结果结尾加 )

  • #{rid}:对每个元素使用预编译占位符,防止 SQL 注入。


3. 执行效果示例

若 Java 传入 rids = [101, 102, 103],则最终生成 SQL:

sql

编辑

复制代码
WHERE rel.rid IN (?,?,?)

MyBatis 自动将 101, 102, 103 绑定到 ?,等价于:

sql

编辑

复制代码
WHERE rel.rid IN (101,102,103)

4. 关键优势

  • 类型安全:支持 Integer、String 等类型自动处理。

  • 防注入#{} 使用 PreparedStatement,杜绝 SQL 注入风险。

  • 动态适配 :自动处理空集合(若 rids 为空,需额外判断,否则会生成 IN () 导致 SQL 语法错误)。


5. 注意事项

  • ridsnull 或空集合,应在外层加 <if test="rids != null and !rids.isEmpty()"> 判断,避免生成无效 SQL。

  • 不能用于 ${} 拼接,必须用 #{} 才能预编译。


总结:安全、动态、高效地将 Java 集合转为 SQL 的 IN 条件。

@Param 的使用时机(精准总结):

✅ 必须使用 @Param 的情况:

  1. Mapper 接口方法有多个参数 (非单个 POJO 或 Map) → MyBatis 无法自动映射参数名,需用 @Param("xxx") 显式指定名称,供 XML 或注解 SQL 中引用。

    java

    编辑

    复制代码
    List<User> findBy(@Param("name") String name, @Param("age") Integer age);

    对应 XML:

    xml

    编辑

    复制代码
    WHERE name = #{name} AND age = #{age}
  2. 参数是基本类型或包装类,且需在 XML 中通过有意义的名字访问 (即使只有一个参数,若想用自定义名字而非默认 param1,也可加)

    java

    编辑

    复制代码
    User getById(@Param("userId") Long id);

    XML 中可用 #{userId},否则只能用 #{id}(Java 8+ 编译保留参数名时)或 #{param1}


❌ 不需要使用 @Param 的情况:

  1. 单个参数

    • 是实体类(POJO)、Map、List 等,直接在 SQL 中用属性名或默认规则访问。

    • Java 8+ 且编译时保留参数名(-parameters),单个基本类型参数可直接用变量名(但不推荐依赖此行为)。

  2. 使用 Map 传参 : 参数本身就是 Map,SQL 中直接用 key 名(如 #{username})。


⚠️ 核心原则:

只要 SQL 中要引用的参数名 ≠ 方法参数变量名(或无法确定),就必须用 @Param 显式绑定。

简记:多参必加,单参看需求,安全清晰优先。

复制代码
    @Resource
    AdminRoleService adminRoleService;
    @Override
    public AdminUser queryByname(String username) {
        //根据用户名查询用户信息
        LambdaQueryWrapper<AdminUser> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AdminUser::getUsername,username);
        AdminUser adminUser = getOne(queryWrapper);
        if (adminUser!=null){
            /*这行代码的作用是:
​
根据当前管理员用户(adminUser)的角色ID,查询并获取对应的角色详情对象 AdminRole。
​
逐部分解释:
​
adminUser.getRoleId():获取当前登录用户的 roleId(通常为 Long 类型)。
.intValue():将其转换为 int(说明 queryRoleByRid 方法接收 int 类型参数)。
adminRoleService.queryRoleByRid(...):调用角色服务层方法,通过角色ID查询角色信息。
返回结果赋值给 AdminRole adminRole,后续可用于权限判断、返回角色数据等。
本质:“拿用户的角色ID,查出完整的角色信息”。*/
            //说明已经查到了用户信息
            //根据用户角色ID查询角色信息
            AdminRole adminRole = adminRoleService.queryRoleByRid(adminUser.getRoleId().intValue());
            adminUser.setRole(adminRole);
        }
​
        return null;
    }
相关推荐
6***v4171 小时前
spring boot 项目打印sql日志和结果,使用logback或配置文件
spring boot·sql·logback
3***g2052 小时前
如何使用Spring Boot框架整合Redis:超详细案例教程
spring boot·redis·后端
狂奔小菜鸡2 小时前
Day18 | 深入理解Object类
java·后端·java ee
jiayong232 小时前
Maven NUL文件问题 - 解决方案实施报告
java·maven
未秃头的程序猿2 小时前
🔒 从单机到分布式:三大锁机制深度剖析与实战指南
java·后端
大猫子的技术日记2 小时前
[百题重刷]前缀和 + Hash 表:缓存思想, 消除重复计算
java·缓存·哈希算法
s***35302 小时前
Spring Boot3.x集成Flowable7.x(一)Spring Boot集成与设计、部署、发起、完成简单流程
java·spring boot·后端
3***16102 小时前
【监控】Spring Boot+Prometheus+Grafana实现可视化监控
spring boot·grafana·prometheus
s***4532 小时前
SpringBoot返回文件让前端下载的几种方式
前端·spring boot·后端