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),最新的技术与你分享

相关推荐
高频因子挖掘机1 小时前
回测收益为什么可能不真实?从时间戳、数据可用性到未来函数检查
后端·github·api
宿州派大星2 小时前
Python实战:PyQt5 + OpenCV + Dlib 打造AI桌面应用
人工智能·python·qt
风娜3 小时前
IT 疑难杂症诊疗室:从现象到根因的系统排查方法论
人工智能·github
碧海银沙音频科技研究院3 小时前
炬芯ATS3089X手表的HFP通话不自动路由到设备BT的问题解决
网络·人工智能·嵌入式硬件·算法
承渊政道3 小时前
【从零开始大模型开发与微调:基于PyTorch与ChatGLM】(开源大模型ChatGLM高级定制化应用实战)
人工智能·pytorch·python·llm·模型微调·glm
howdoyoudo2026063 小时前
AI审计手记 #28:从“暂停训练”到“切断联网”——AI安全控制手段的极限升级
大数据·人工智能·安全·ai·语言模型
colman wang3 小时前
人工智能课程(配套AIMA(Artificial Intelligence: A Modern Approach))
人工智能
LinXunFeng7 小时前
Draw Things 换装 LoRA 实战:用 Silo 秒装,同一提示词对比出图
人工智能·开源·aigc
xiaohaiAIgeo7 小时前
【2026年】通风柜自动门安全吗?联锁逻辑与防夹设计
人工智能·安全·科普知识