Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 3 —— 消息表设计 + 级联删除 + 事务管理

写在前面

作为一名技术人,我深知学习新框架时的迷茫与焦虑。尤其是当你已经熟悉了一套技术栈,想要转向另一个生态时,那种"无从下手"的感觉尤为强烈。

最近,我开始系统学习Spring Boot生态,并尝试将Spring AI集成到实际项目中。经过一段时间的摸索,我整理出了一份7天学习路线图,目标是让有编程基础的开发者能快速上手Spring Boot + Spring AI,独立完成一个完整的AI对话应用。

这份计划不是零基础教程,而是为有一定后端开发经验(不限于Java)的朋友量身定制的"快速转型指南"。如果你对Python、Node.js或其他后端语言已有了解,这份计划将帮助你在最短时间内建立Spring Boot的核心认知,并跟上AI应用开发的浪潮。

下面,我将这7天的学习中的第三天,消息表设计 + 级联删除 + 事务管理。

一、创建消息表

1.1 执行 SQL

sql 复制代码
USE chat_db;

CREATE TABLE IF NOT EXISTS message (
    id BIGINT NOT NULL AUTO_INCREMENT COMMENT '消息ID',
    session_id BIGINT NOT NULL COMMENT '会话ID(逻辑外键,关联session表)',
    role VARCHAR(20) NOT NULL COMMENT '角色:user(用户)/assistant(AI)',
    content TEXT NOT NULL COMMENT '消息内容',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (id),
    INDEX idx_session_id (session_id),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息表';

二、创建 Message 实体类

创建 entity/Message.java:

kotlin 复制代码
package com.example.chatapi.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;


@Data
@TableName("message")
public class Message {

    @TableId(type = IdType.AUTO)
    private Long id;

    private Long sessionId;

    private String role;

    private String content;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createdAt;
}

三、创建 MessageMapper

创建 mapper/MessageMapper.java:

less 复制代码
package com.example.chatapi.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.chatapi.entity.Message;
import org.apache.ibatis.annotations.*;
import java.util.List;

@Mapper
public interface MessageMapper extends BaseMapper<Message> {

    @Select("SELECT * FROM message WHERE session_id = #{sessionId} ORDER BY created_at ASC")
    List<Message> selectBySessionId(@Param("sessionId") Long sessionId);

    @Delete("DELETE FROM message WHERE session_id = #{sessionId}")
    int deleteBySessionId(@Param("sessionId") Long sessionId);
}

四、创建统一响应类

创建 dto/ApiResponse.java:

typescript 复制代码
package com.example.chatapi.dto;


import lombok.Data;

@Data
public class ApiResponse<T> {
    private Integer code;
    private String message;
    private T data;

    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(200);
        response.setMessage("success");
        response.setData(data);
        return response;
    }

    public static <T> ApiResponse<T> error(Integer code, String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(code);
        response.setMessage(message);
        return response;
    }
}

创建 dto/MessageSaveRequest.java:

kotlin 复制代码
package com.example.chatapi.dto;
import lombok.Data;

@Data
public class MessageSaveRequest {
    private Long sessionId;
    private String role;
    private String content;
}

五、创建 MessageService

创建 service/MessageService.java:

java 复制代码
package com.example.chatapi.service;

import com.example.chatapi.entity.Message;
import com.example.chatapi.mapper.MessageMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class MessageService {

    private final MessageMapper messageMapper;
    
    public List<Message> getMessagesBySession(Long sessionId) {
        log.info("查询会话 {} 的消息列表", sessionId);
        return messageMapper.selectBySessionId(sessionId);
    }

    public boolean saveMessage(Message message) {
        log.info("保存消息: sessionId={}, role={}", message.getSessionId(), message.getRole());
        int rows = messageMapper.insert(message);
        return rows > 0;
    }
}

六、修改 SessionService(添加级联删除)

更新 service/SessionService.java:

java 复制代码
package com.example.chatapi.service;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.chatapi.entity.Session;
import com.example.chatapi.mapper.MessageMapper;
import com.example.chatapi.mapper.SessionMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;


@Slf4j
@Service
@RequiredArgsConstructor
public class SessionService {

    private final SessionMapper sessionMapper;
    private final MessageMapper messageMapper;

    public List<Session> listSessions() {
        LambdaQueryWrapper<Session> wrapper = new LambdaQueryWrapper<>();
        wrapper.orderByDesc(Session::getCreatedAt);
        return sessionMapper.selectList(wrapper);
    }

    public Session createSession(String name) {
        Session session = new Session();
        session.setName(name);
        session.setCreatedAt(LocalDateTime.now());
        sessionMapper.insert(session);
        log.info("创建会话成功: id={}, name={}", session.getId(), name);
        return session;
    }

    public boolean updateSession(Long id, String name) {
        Session session = new Session();
        session.setId(id);
        session.setName(name);
        int rows = sessionMapper.updateById(session);
        return rows > 0;
    }

    @Transactional(rollbackFor = Exception.class)
    public boolean deleteSession(Long id) {
        log.info("开始删除会话 {} 及其所有消息", id);

        int deletedMessages = messageMapper.deleteBySessionId(id);
        log.info("删除了 {} 条消息", deletedMessages);

        int deletedSession = sessionMapper.deleteById(id);
        return deletedSession > 0;
    }

    public boolean exists(Long id) {
        return sessionMapper.selectCount(
            new LambdaQueryWrapper<Session>().eq(Session::getId, id)
        ) > 0;
    }
}
  • rollbackFor = Exception.class:当发生任何异常时都回滚事务
  • 如果不指定,默认只在 RuntimeException 和 Error 时回滚

异常情况(删除消息后出错)

markdown 复制代码
开始事务
    ↓
删除消息(成功)✓
    ↓
删除会话(失败)✗ 抛出异常
    ↓
回滚事务 → 删除的消息恢复,数据回到原状

七、创建 MessageController

创建 controller/MessageController.java:

kotlin 复制代码
package com.example.chatapi.controller;

import com.example.chatapi.dto.ApiResponse;
import com.example.chatapi.dto.MessageSaveRequest;
import com.example.chatapi.entity.Message;
import com.example.chatapi.service.MessageService;
import com.example.chatapi.service.SessionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;


@Slf4j
@RestController
@RequestMapping("/api/messages")
@RequiredArgsConstructor
public class MessageController {

    private final MessageService messageService;
    private final SessionService sessionService;

    @GetMapping("/{sessionId}")
    public ApiResponse<List<Message>> getMessages(@PathVariable Long sessionId) {
        log.info("接收请求:获取会话 {} 的消息列表", sessionId);

        if (!sessionService.exists(sessionId)) {
            return ApiResponse.error(404, "会话不存在");
        }

        List<Message> messages = messageService.getMessagesBySession(sessionId);
        return ApiResponse.success(messages);
    }

    @PostMapping
    public ApiResponse<Message> saveMessage(@RequestBody MessageSaveRequest request) {
        log.info("接收请求:保存消息 sessionId={}, role={}", request.getSessionId(), request.getRole());

        if (!sessionService.exists(request.getSessionId())) {
            return ApiResponse.error(404, "会话不存在");
        }

        if (!"user".equals(request.getRole()) && !"assistant".equals(request.getRole())) {
            return ApiResponse.error(400, "角色只能是 user 或 assistant");
        }

        Message message = new Message();
        message.setSessionId(request.getSessionId());
        message.setRole(request.getRole());
        message.setContent(request.getContent());

        boolean success = messageService.saveMessage(message);
        if (success) {
            return ApiResponse.success(message);
        } else {
            return ApiResponse.error(500, "保存失败");
        }
    }
}

八、启动项目并测试

8.1 启动项目

运行 ChatApiApplication.java,控制台看到以下日志即成功:

8.2 Postman 测试

bash 复制代码
POST http://localhost:8080/api/messages
Content-Type: application/json
{"sessionId": 1, "role": "user", "content": "什么是Spring Boot?"}

学习建议: 今天的重点是创建一个message表,和级联删除。不要只复制代码,动手敲一遍,再用 Postman 把所有接口测一遍。

欢迎关注我的公众号(onething365),最新的技术与你分享

相关推荐
m0_617493947 小时前
OpenCV cv2.circle() 坐标类型错误排查与修复指南
人工智能·opencv·计算机视觉
AI创界者7 小时前
最新 ComfyUI + LTX-2.5 本地一键整合包:文生视频/图生视频/数字人全流程指南
人工智能·aigc·音视频
不才不才不不才7 小时前
Spring 源码系列(20): @SpringBootApplication 三注解拆解
java·后端·spring
罗小爬EX7 小时前
AI Chat 多类型流式事件处理方案(以AgentScope Java 2为例)
java·人工智能·状态模式
程序员爱钓鱼7 小时前
Rust 生命周期常见错误详解:看懂编译器报错并正确修复
前端·后端·rust
aneasystone本尊7 小时前
学习 Headroom 的跨 agent 记忆与失败学习
人工智能
andongni2037 小时前
SpringBoot RESTful API 设计与实现
spring boot·后端·restful
a1122998217 小时前
GEO工具怎么选?电商、本地生活与B2B行业的选型差异
大数据·人工智能·生活
程序员爱钓鱼7 小时前
Go if 判断详解
前端·后端·go
大模型探索者7 小时前
重塑金融合规与效率:中关村科金AI+RTC智能双录系统解析
人工智能·金融·实时音视频