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 小时前
Hermes Agent 代码仓库打包工具使用指南(repomix-rs 高性能版)
后端
王某某人1 小时前
LangChain4j 入门:Java 程序员的第一个 AI 对话程序
人工智能·后端
海兰1 小时前
【实用程序】电商销售分析仪表盘 — 从零搭建一个AI参与的全栈数据洞察系统
人工智能·学习·算法
枫糖浆AI1 小时前
openclaw页面无法访问解决方法
人工智能
码农刚子1 小时前
从零开始:在 Windows 服务器上部署 Node.js 项目(小白实战教程)
后端·node.js
Cache技术分享1 小时前
435. Java 日期时间 API - Clock 灵活获取当前时间
前端·后端
浩子coding2 小时前
通过 Spring AI Alibaba 源码,看如何玩转 ReAct 智能体范式
人工智能·后端
卡梅德生物科技小能手2 小时前
卡梅德生物科普CD124(IL-4Rα):2型免疫炎症的核心调控靶点
人工智能·经验分享·深度学习
垂钓的小鱼12 小时前
TRIZ理论是什么?萃智引擎如何将它变为工程师的AI创新助手
人工智能·microsoft