iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

Java × AI:30天打造智慧健康助理系列 第 27 篇

用 Spring Boot 建立 AI 對話查詢與刪除 API

  • 分享至 

  • xImage
  •  

Day 27|用 Spring Boot 建立 AI 對話查詢與刪除 API

昨天我們成功把 AI 多輪對話紀錄保存到 H2 Database。

原本只要重新啟動 Spring Boot,所有聊天紀錄就會消失。

但現在透過 Spring JDBC,我們已經可以把使用者問題和 AI 回覆保存到資料庫,甚至在重新啟動程式後,繼續之前的對話。

不過我發現,現在的功能還有一個問題。

我們可以保存對話,卻沒有辦法方便地管理對話。

例如,假設使用者已經建立了三段聊天:

對話 A:BMI 是什麼?
對話 B:每天應該喝多少水?
對話 C:如何養成規律作息?

目前如果沒有記住 conversationId,就很難找到之前的對話。

而且即使想刪除某段聊天,也只能手動進入 H2 Console 操作資料庫。

這對使用者來說實在不太方便 XD

所以今天決定加入:

AI 對話管理功能!

今天的目標是:

使用 Spring Boot 和 Spring JDBC,建立對話列表查詢、完整聊天紀錄查詢,以及對話刪除 API,讓 AI 健康助理具備基本的聊天紀錄管理能力。


一、今天要完成什麼功能?

目前我們已經有:

POST /health/ai/conversation

可以建立新對話或接續既有對話。

今天準備新增三個 API。

HTTP Method API 功能
GET /health/ai/conversations 查詢所有對話
GET /health/ai/conversations/{id} 查詢指定對話的完整紀錄
DELETE /health/ai/conversations/{id} 刪除指定對話

例如,使用者想查看所有對話:

GET /health/ai/conversations

就可以取得對話列表。

如果想查看某段聊天:

GET /health/ai/conversations/550e8400-e29b-41d4-a716-446655440000

就可以取得完整的問題和回答。

如果想刪除:

DELETE /health/ai/conversations/550e8400-e29b-41d4-a716-446655440000

就可以刪除該段對話及其訊息。

今天我們會沿用 Day 26 的資料庫,不需要重新建立專案。


二、先回顧目前的資料庫設計

昨天我們建立了兩張資料表:

conversations

以及:

conversation_messages

它們之間是一對多關係。

conversations
│
├── Conversation A
│   ├── User Message
│   ├── Assistant Message
│   ├── User Message
│   └── Assistant Message
│
└── Conversation B
    ├── User Message
    └── Assistant Message

其中 conversations 負責保存對話資訊:

欄位 說明
id 對話識別碼
created_at 對話建立時間

而 conversation_messages 負責保存每一則訊息:

欄位 說明
id 訊息流水號
conversation_id 所屬對話
role user 或 assistant
content 訊息內容
created_at 訊息建立時間

今天不需要修改這兩張資料表。

我們只要透過 SQL,就可以取得需要的資料。


三、今天的系統架構

今天會延續之前的分層設計。

使用者
   ↓
AIController
   ↓
ConversationManagementService
   ↓
ConversationRepository
   ↓
H2 Database

其中每一層的工作如下:

層級 負責工作
Controller 接收 HTTP Request、回傳 Response
Service 處理查詢與刪除流程
Repository 執行 SQL
DTO 定義回傳資料格式
Database 保存對話與訊息

今天準備新增:

ConversationListResponse.java
ConversationDetailResponse.java
ConversationManagementService.java
ConversationNotFoundException.java

並修改:

ConversationMessage.java
ConversationRepository.java
AIController.java
GlobalExceptionHandler.java

這次不需要修改 AI 模型的呼叫方式。

因為今天主要處理的是資料庫中的聊天紀錄。


Part 1|建立對話列表查詢功能

四、先設計對話列表的回傳格式

假設使用者已經建立了三段對話。

我們希望呼叫:

GET /health/ai/conversations

可以回傳:

[
  {
    "conversationId": "550e8400-e29b-41d4-a716-446655440000",
    "createdAt": "2026-10-17T14:00:00",
    "messageCount": 4
  },
  {
    "conversationId": "9c1e4b20-2f7a-4c88-a2d3-123456789abc",
    "createdAt": "2026-10-17T14:05:00",
    "messageCount": 2
  }
]

這裡包含三個欄位:

conversationId
createdAt
messageCount

其中 messageCount 代表這段對話總共有幾則訊息。

例如:

User:什麼是 BMI?
Assistant:BMI 是身體質量指數。
User:那它怎麼計算?
Assistant:體重除以身高的平方。

總共就是:

4 則訊息

而不是四輪對話。

這點要特別注意。


五、建立 ConversationListResponse DTO

首先在:

dto

Package 裡面新增:

ConversationListResponse.java

完整程式如下:

package com.example.healthassistant.dto;

import java.time.LocalDateTime;

public class ConversationListResponse {

    private String conversationId;
    private LocalDateTime createdAt;
    private long messageCount;

    public ConversationListResponse(
            String conversationId,
            LocalDateTime createdAt,
            long messageCount) {

        this.conversationId = conversationId;
        this.createdAt = createdAt;
        this.messageCount = messageCount;
    }

    public String getConversationId() {
        return conversationId;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public long getMessageCount() {
        return messageCount;
    }
}

這裡出現了一個新的 Java 型別:

LocalDateTime

之前我們經常使用:

LocalDate

例如:

2026-10-17

但 LocalDateTime 可以同時保存日期與時間。

例如:

2026-10-17T14:00:00

這樣就可以顯示對話建立的時間。

不過 LocalDateTime 本身不包含時區資訊。

今天沿用 H2 的 TIMESTAMP 欄位,正式系統則需要更明確地管理時區。


六、修改 ConversationRepository

接下來打開:

ConversationRepository.java

新增以下 Import:

import com.example.healthassistant.dto.ConversationListResponse;

再加入一個 Method:

public List<ConversationListResponse> findAllConversations() {

    String sql = """
        SELECT
            c.id,
            c.created_at,
            COUNT(m.id) AS message_count
        FROM conversations c
        LEFT JOIN conversation_messages m
            ON c.id = m.conversation_id
        GROUP BY c.id, c.created_at
        ORDER BY c.created_at DESC, c.id DESC
        """;

    return jdbcTemplate.query(
            sql,
            (rs, rowNum) -> new ConversationListResponse(
                    rs.getString("id"),
                    rs.getTimestamp("created_at")
                            .toLocalDateTime(),
                    rs.getLong("message_count")
            )
    );
}

這段 SQL 比之前稍微複雜一點。

因為我們這次不只要查詢對話,也想知道每段對話有幾則訊息。

所以使用:

LEFT JOIN

以及:

COUNT()

接下來分別說明。


七、認識 LEFT JOIN

今天使用:

LEFT JOIN conversation_messages m
    ON c.id = m.conversation_id

這代表我們要把:

conversations

和:

conversation_messages

兩張資料表關聯起來。

例如:

conversations
-------------------
A
B
C

而訊息資料表是:

conversation_messages
-------------------
A → Message 1
A → Message 2
B → Message 3

其中:

Conversation C

目前沒有任何訊息。

如果使用 INNER JOIN,沒有訊息的對話可能就不會出現在結果中。

但今天使用:

LEFT JOIN

所以即使對話沒有訊息,也可以保留在查詢結果裡。

例如:

conversationId messageCount
A 2
B 1
C 0

這樣比較符合對話列表的需求。


八、認識 COUNT 與 GROUP BY

接著看到:

COUNT(m.id) AS message_count

這裡使用:

COUNT()

計算訊息數量。

為什麼使用:

COUNT(m.id)

而不是:

COUNT(*)

呢?

因為我們使用了 LEFT JOIN。

如果某段對話沒有訊息,LEFT JOIN 仍然會保留一筆對話資料。

使用 COUNT(*) 可能會把這筆資料算進去。

但:

COUNT(m.id)

只會計算實際存在的訊息 ID。

所以沒有訊息的對話會得到:

0

接著:

GROUP BY c.id, c.created_at

會按照每段對話進行分組。

最後:

ORDER BY c.created_at DESC, c.id DESC

讓較新的對話排在前面。

這樣我們就完成對話列表的 SQL 查詢。


Part 2|建立完整聊天紀錄查詢功能

九、設計對話詳細資料的回傳格式

接下來要完成第二個功能:

GET /health/ai/conversations/{id}

例如:

GET /health/ai/conversations/550e8400-e29b-41d4-a716-446655440000

我們希望回傳:

{
  "conversationId": "550e8400-e29b-41d4-a716-446655440000",
  "messages": [
    {
      "role": "user",
      "content": "什麼是 BMI?"
    },
    {
      "role": "assistant",
      "content": "BMI 是身體質量指數。"
    },
    {
      "role": "user",
      "content": "那它怎麼計算?"
    },
    {
      "role": "assistant",
      "content": "體重除以身高的平方。"
    }
  ]
}

這樣就可以取得一段對話的完整內容。

未來如果要製作網頁聊天介面,也可以直接使用這個 API 顯示歷史訊息。


十、建立 ConversationDetailResponse DTO

在:

dto

Package 裡面新增:

ConversationDetailResponse.java

完整程式如下:

package com.example.healthassistant.dto;

import java.util.List;

public class ConversationDetailResponse {

    private String conversationId;
    private List<ConversationMessage> messages;

    public ConversationDetailResponse(
            String conversationId,
            List<ConversationMessage> messages) {

        this.conversationId = conversationId;
        this.messages = messages;
    }

    public String getConversationId() {
        return conversationId;
    }

    public List<ConversationMessage> getMessages() {
        return messages;
    }
}

這裡我們直接沿用昨天建立的:

ConversationMessage

DTO。

所以不需要再另外建立一個訊息類別。

目前的結構會變成:

ConversationDetailResponse
│
├── conversationId
│
└── messages
    ├── ConversationMessage
    ├── ConversationMessage
    ├── ConversationMessage
    └── ConversationMessage

這樣的設計也方便未來擴充。


十一、修改 ConversationMessage DTO

昨天的 ConversationMessage 只有:

role
content

今天既然要建立完整聊天紀錄查詢功能,我想再加入:

id
createdAt

這樣未來如果要在網頁上顯示訊息時間,或是區分不同訊息,就會比較方便。

打開:

ConversationMessage.java

將內容更新成:

package com.example.healthassistant.dto;

import java.time.LocalDateTime;

public class ConversationMessage {

    private Long id;
    private String role;
    private String content;
    private LocalDateTime createdAt;

    public ConversationMessage(
            Long id,
            String role,
            String content,
            LocalDateTime createdAt) {

        this.id = id;
        this.role = role;
        this.content = content;
        this.createdAt = createdAt;
    }

    public Long getId() {
        return id;
    }

    public String getRole() {
        return role;
    }

    public String getContent() {
        return content;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }
}

這次新增了:

private Long id;

以及:

private LocalDateTime createdAt;

因為原本的建構子已經改變,所以 Repository 裡面建立 ConversationMessage 的程式,也需要一起更新。


十二、更新 ConversationRepository 的訊息查詢

打開:

ConversationRepository.java

找到昨天建立的:

findMessages()

將它改成:

public List<ConversationMessage> findMessages(
        String conversationId) {

    String sql = """
        SELECT id, role, content, created_at
        FROM conversation_messages
        WHERE conversation_id = ?
        ORDER BY id
        """;

    return jdbcTemplate.query(
            sql,
            (rs, rowNum) -> new ConversationMessage(
                    rs.getLong("id"),
                    rs.getString("role"),
                    rs.getString("content"),
                    rs.getTimestamp("created_at")
                            .toLocalDateTime()
            ),
            conversationId
    );
}

這次多查詢了:

id

以及:

created_at

這樣就能取得完整訊息資訊。

另外,昨天建立的:

findRecentMessages()

也要一起修改。

更新成:

public List<ConversationMessage> findRecentMessages(
        String conversationId,
        int limit) {

    String sql = """
        SELECT id, role, content, created_at
        FROM (
            SELECT id, role, content, created_at
            FROM conversation_messages
            WHERE conversation_id = ?
            ORDER BY id DESC
            LIMIT ?
        ) recent
        ORDER BY id
        """;

    return jdbcTemplate.query(
            sql,
            (rs, rowNum) -> new ConversationMessage(
                    rs.getLong("id"),
                    rs.getString("role"),
                    rs.getString("content"),
                    rs.getTimestamp("created_at")
                            .toLocalDateTime()
            ),
            conversationId,
            limit
    );
}

這樣 Day 26 的多輪對話功能也可以繼續使用。

因為 AIConversationService 原本使用的是:

message.getRole()

以及:

message.getContent()

這兩個 Getter 仍然保留,所以不需要修改 AI 對話邏輯。


Part 3|建立對話管理 Service

十三、建立 ConversationManagementService

接下來我們需要一個 Service,負責管理對話資料。

在:

service

Package 裡面新增:

ConversationManagementService.java

這個 Service 會負責:

查詢所有對話
查詢指定對話

以及:

刪除指定對話

完整程式如下:

package com.example.healthassistant.service;

import com.example.healthassistant.dto.ConversationDetailResponse;
import com.example.healthassistant.dto.ConversationListResponse;
import com.example.healthassistant.dto.ConversationMessage;

import com.example.healthassistant.exception.ConversationNotFoundException;
import com.example.healthassistant.repository.ConversationRepository;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class ConversationManagementService {

    private final ConversationRepository conversationRepository;

    public ConversationManagementService(
            ConversationRepository conversationRepository) {

        this.conversationRepository = conversationRepository;
    }

    public List<ConversationListResponse> getAllConversations() {

        return conversationRepository.findAllConversations();
    }

    public ConversationDetailResponse getConversation(
            String conversationId) {

        if (!conversationRepository.exists(conversationId)) {

            throw new ConversationNotFoundException(
                    "Conversation not found."
            );
        }

        List<ConversationMessage> messages =
                conversationRepository.findMessages(conversationId);

        return new ConversationDetailResponse(
                conversationId,
                messages
        );
    }

    @Transactional
    public void deleteConversation(String conversationId) {

        if (!conversationRepository.exists(conversationId)) {

            throw new ConversationNotFoundException(
                    "Conversation not found."
            );
        }

        conversationRepository.deleteMessages(conversationId);

        conversationRepository.deleteConversation(conversationId);
    }
}

這個 Service 的設計很簡單。

Controller 不需要直接操作資料庫。

而是透過:

ConversationManagementService

呼叫:

ConversationRepository

這樣可以讓不同層級的責任更加清楚。

不過目前 Repository 還沒有刪除功能。

所以接下來就要完成 DELETE 的 SQL。


Part 4|建立刪除對話功能

十四、為什麼刪除對話需要注意 Foreign Key?

還記得昨天我們建立的外鍵嗎?

FOREIGN KEY (conversation_id)
REFERENCES conversations(id)

這個外鍵會確保:

每一則訊息都屬於一段存在的對話。

但也代表:

不能隨便刪除仍然有訊息的對話。

例如:

Conversation A
│
├── Message 1
├── Message 2
├── Message 3
└── Message 4

如果我們直接執行:

DELETE FROM conversations
WHERE id = 'A';

資料庫可能會拒絕這個操作。

因為:

conversation_messages

仍然有資料參照這段對話。

所以今天採用:

先刪除訊息,再刪除對話。

流程如下:

DELETE conversation_messages
           ↓
DELETE conversations

這樣就不會違反外鍵限制。


十五、在 ConversationRepository 新增刪除功能

打開:

ConversationRepository.java

加入第一個 Method:

public void deleteMessages(String conversationId) {

    String sql = """
        DELETE FROM conversation_messages
        WHERE conversation_id = ?
        """;

    jdbcTemplate.update(
            sql,
            conversationId
    );
}

這個 Method 會刪除指定對話的所有訊息。

接著加入第二個 Method:

public void deleteConversation(String conversationId) {

    String sql = """
        DELETE FROM conversations
        WHERE id = ?
        """;

    jdbcTemplate.update(
            sql,
            conversationId
    );
}

這個 Method 則會刪除對話本身。

最後我們就可以在 Service 裡面依序呼叫:

conversationRepository.deleteMessages(conversationId);

conversationRepository.deleteConversation(conversationId);

這樣就完成基本的刪除流程。


十六、為什麼刪除功能要使用 @Transactional?

今天在:

deleteConversation()

加入:

@Transactional

原因是我們需要執行兩個 SQL 操作。

第一個:

DELETE FROM conversation_messages

第二個:

DELETE FROM conversations

我們希望這兩個操作要嘛全部成功,要嘛在發生錯誤時一起回復。

例如:

刪除訊息成功
       ↓
刪除對話失敗

如果沒有交易管理,就可能留下不一致的結果。

但使用:

@Transactional

後,在一般未捕捉的執行期例外情況下,Spring 可以將同一筆交易中的資料庫變更回滾。

這樣就能降低部分資料不一致的風險。

而且今天的:

ConversationManagementService

是由 Controller 呼叫的獨立 Service。

因此交易可以透過 Spring 的代理機制生效。

這和昨天提到的「同一個物件內部直接呼叫交易 Method」不同。


十七、建立 ConversationNotFoundException

今天的查詢與刪除功能,都可能遇到:

Conversation not found.

也就是找不到指定對話。

之前我們使用:

IllegalArgumentException

但這個例外的用途比較廣泛。

例如參數格式錯誤,也可能使用它。

所以今天決定建立一個更明確的例外類別。

在:

exception

Package 裡面新增:

ConversationNotFoundException.java

完整程式如下:

package com.example.healthassistant.exception;

public class ConversationNotFoundException
        extends RuntimeException {

    public ConversationNotFoundException(String message) {
        super(message);
    }
}

這樣我們就可以明確表示:

指定對話不存在。

接著在:

GlobalExceptionHandler.java

加入:

@ExceptionHandler(ConversationNotFoundException.class)
public ResponseEntity<Map<String, String>>
handleConversationNotFound(
        ConversationNotFoundException ex) {

    return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(
                Map.of("error", ex.getMessage())
            );
}

記得確認有以下 Import:

import com.example.healthassistant.exception.ConversationNotFoundException;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.Map;

如果原本已經有:

GlobalExceptionHandler

只要新增 Method 即可,不需要重新建立整個類別。

這樣找不到對話時,就會回傳:

404 Not Found

例如:

{
  "error": "Conversation not found."
}

另外,Day 26 的 AIConversationService 也可以將原本的:

throw new IllegalArgumentException(
        "Conversation not found."
);

改成:

throw new ConversationNotFoundException(
        "Conversation not found."
);

並加入對應 Import。

這樣整個專案就能使用一致的例外處理方式。


Part 5|建立 Controller API

十八、修改 AIController

接下來終於要建立今天的三個 API!

打開:

AIController.java

今天不需要刪除原本的 AI 功能。

我們只要新增:

ConversationManagementService

即可。

首先新增 Import:

import com.example.healthassistant.dto.ConversationListResponse;
import com.example.healthassistant.dto.ConversationDetailResponse;

import com.example.healthassistant.service.ConversationManagementService;

import java.util.List;

接著在 Controller 裡面新增欄位:

private final ConversationManagementService conversationManagementService;

並將它加入原本的建構子參數:

ConversationManagementService conversationManagementService

在建構子內指定:

this.conversationManagementService =
        conversationManagementService;

最後加入以下三個 API Method。

API 1:查詢所有對話

@GetMapping("/conversations")
public ResponseEntity<List<ConversationListResponse>>
getAllConversations() {

    List<ConversationListResponse> conversations =
            conversationManagementService.getAllConversations();

    return ResponseEntity.ok(conversations);
}

API 2:查詢指定對話

@GetMapping("/conversations/{id}")
public ResponseEntity<ConversationDetailResponse>
getConversation(@PathVariable String id) {

    ConversationDetailResponse conversation =
            conversationManagementService.getConversation(id);

    return ResponseEntity.ok(conversation);
}

API 3:刪除指定對話

@DeleteMapping("/conversations/{id}")
public ResponseEntity<Void>
deleteConversation(@PathVariable String id) {

    conversationManagementService.deleteConversation(id);

    return ResponseEntity.noContent().build();
}

這樣就完成今天的 Controller!


十九、認識 @PathVariable

今天的 API 使用:

@PathVariable

例如:

@GetMapping("/conversations/{id}")

其中:

{id}

代表 URL 中的一個變數。

假設使用者呼叫:

GET /health/ai/conversations/123

Spring Boot 就會把:

123

帶入:

@PathVariable String id

所以:

id = "123";

這樣我們就可以使用 URL 指定要查詢的對話。

之前我們比較常使用:

@RequestParam

例如:

GET /health/water/date?date=2026-10-17

兩者的差異如下:

註解 用途 範例
@RequestParam 取得查詢參數 ?date=2026-10-17
@PathVariable 取得 URL 路徑變數 /conversations/123

今天因為要操作指定的對話資源,所以使用 @PathVariable。


二十、認識 DELETE 與 204 No Content

今天第一次在專案中使用:

@DeleteMapping

例如:

@DeleteMapping("/conversations/{id}")

它對應 HTTP 的:

DELETE

主要用來刪除指定資源。

另外,今天成功刪除後會回傳:

ResponseEntity.noContent().build();

代表:

204 No Content

意思是:

請求已成功處理,但沒有需要回傳的內容。

例如:

DELETE /health/ai/conversations/123

如果成功:

204 No Content

Response Body 為空。

這也是 REST API 常見的刪除回應方式。


Part 6|使用 Postman 測試

二十一、測試查詢所有對話

接下來重新啟動 Spring Boot。

先確認資料庫中已經有一些對話。

如果還沒有,可以先使用之前的:

POST /health/ai/conversation

建立幾段測試對話。

接著打開 Postman。

HTTP Method:

GET

URL:

http://localhost:8080/health/ai/conversations

按下 Send。

如果查詢成功,預期回傳:

[
  {
    "conversationId": "550e8400-e29b-41d4-a716-446655440000",
    "createdAt": "2026-10-17T14:00:00",
    "messageCount": 4
  },
  {
    "conversationId": "9c1e4b20-2f7a-4c88-a2d3-123456789abc",
    "createdAt": "2026-10-17T14:05:00",
    "messageCount": 2
  }
]

實際 ID、時間及訊息數量會依照資料庫內容而不同。

如果目前沒有任何對話,則會回傳:

[]

這是正常的。

因為:

沒有資料

不代表:

API 發生錯誤

二十二、測試查詢完整聊天紀錄

接著測試第二個 API。

HTTP Method:

GET

URL:

http://localhost:8080/health/ai/conversations/550e8400-e29b-41d4-a716-446655440000

請將 URL 最後面的 UUID 替換成實際存在的 conversationId。

如果查詢成功,預期回傳類似:

{
  "conversationId": "550e8400-e29b-41d4-a716-446655440000",
  "messages": [
    {
      "id": 1,
      "role": "user",
      "content": "什麼是 BMI?",
      "createdAt": "2026-10-17T14:00:01"
    },
    {
      "id": 2,
      "role": "assistant",
      "content": "BMI 是身體質量指數。",
      "createdAt": "2026-10-17T14:00:03"
    },
    {
      "id": 3,
      "role": "user",
      "content": "那它怎麼計算?",
      "createdAt": "2026-10-17T14:01:00"
    },
    {
      "id": 4,
      "role": "assistant",
      "content": "BMI 的計算方式是體重除以身高的平方。",
      "createdAt": "2026-10-17T14:01:02"
    }
  ]
}

這樣就可以取得完整的聊天內容。

未來如果要建立前端介面,就可以將這些訊息依照 role 分別顯示。

例如:

User → 靠右顯示
Assistant → 靠左顯示

就會比較接近一般聊天應用程式。


二十三、測試不存在的對話

接下來故意輸入一個不存在的 ID。

例如:

GET /health/ai/conversations/not-exist-id

因為資料庫中找不到這段對話,所以 Service 會拋出:

ConversationNotFoundException

接著由:

GlobalExceptionHandler

統一處理。

預期 HTTP Status:

404 Not Found

Response Body:

{
  "error": "Conversation not found."
}

這樣使用者就可以知道:

指定的對話不存在。

而不是只看到不明原因的伺服器錯誤。


二十四、測試刪除對話

接下來是今天最重要的刪除功能!

先使用:

GET /health/ai/conversations

確認資料庫中有哪幾段對話。

選擇一段測試用對話。

接著在 Postman 設定:

HTTP Method:

DELETE

URL:

http://localhost:8080/health/ai/conversations/550e8400-e29b-41d4-a716-446655440000

請使用實際存在的測試對話 ID。

按下 Send。

如果刪除成功,預期回傳:

204 No Content

而且不會有 Response Body。

接著再呼叫:

GET /health/ai/conversations

就會發現剛剛刪除的對話已經不在列表中。

如果再次查詢被刪除的對話:

GET /health/ai/conversations/550e8400-e29b-41d4-a716-446655440000

預期會得到:

404 Not Found

這樣就完成刪除功能的測試!


二十五、使用 H2 Console 確認資料真的刪除了

雖然 API 已經回傳:

204 No Content

但我們還可以進一步使用 H2 Console 確認資料。

打開:

http://localhost:8080/h2-console

登入後執行:

SELECT *
FROM conversations;

確認指定對話已經不存在。

接著執行:

SELECT *
FROM conversation_messages;

確認屬於該對話的訊息也已經刪除。

也可以使用更精確的查詢:

SELECT *
FROM conversation_messages
WHERE conversation_id = '你的實際對話ID';

如果刪除成功,應該查不到任何資料。

這樣就能確認:

我們不只刪除了對話本身,也刪除了對應的聊天訊息。


Part 7|整理今天的功能與注意事項

二十六、為什麼今天沒有使用 ON DELETE CASCADE?

今天刪除對話時,我們使用:

先刪除訊息
再刪除對話

但其實資料庫還有另一種做法:

ON DELETE CASCADE

例如:

FOREIGN KEY (conversation_id)
REFERENCES conversations(id)
ON DELETE CASCADE

這代表:

當某段對話被刪除時,資料庫會自動刪除屬於該對話的訊息。

例如:

DELETE Conversation A
        ↓
自動刪除 Message 1
        ↓
自動刪除 Message 2
        ↓
自動刪除 Message 3

這樣就不需要在 Java 裡面分別執行兩個 DELETE。

不過今天沒有直接修改原本的 Foreign Key。

原因是:

我們的 H2 Database 已經建立了資料表。

即使修改:

schema.sql

裡面的 CREATE TABLE IF NOT EXISTS,也不會自動更新既有外鍵設定。

因此今天先使用明確的兩步驟刪除方式。

這樣也比較容易理解外鍵和交易的關係。


二十七、今天的 API 有哪些限制?

雖然今天已經完成對話管理功能,但目前仍然是本機教學版本。

1. 尚未加入使用者驗證

目前:

GET /health/ai/conversations

會查詢資料庫中的所有對話。

如果未來有多個使用者,這樣就可能造成隱私問題。

正式系統應該只允許使用者查詢自己的對話。

例如:

User A → 只能查看 Conversation A
User B → 只能查看 Conversation B

所以未來需要加入登入驗證與資料存取權限。

2. conversationId 不是安全驗證

即使 UUID 不容易被猜到,也不能把它當成登入憑證。

知道某個對話 ID,不應該直接代表有權限查看或刪除該對話。

3. 尚未加入分頁查詢

目前:

GET /health/ai/conversations

會一次回傳所有對話。

如果未來有幾千筆資料,回傳內容就可能非常龐大。

所以正式系統可以加入:

page

以及:

size

例如:

GET /health/ai/conversations?page=0&size=10

每次只取得部分資料。

4. 尚未加入刪除確認機制

目前只要呼叫 DELETE API,就會直接刪除資料。

如果未來有前端介面,應該先讓使用者確認。

例如:

確定要刪除這段對話嗎?

避免誤刪。

5. 尚未處理多實例並行問題

目前是單機教學架構。

如果未來部署多個 Spring Boot 實例,仍然需要考慮同時新增訊息、刪除對話,以及資料庫交易之間的並行問題。


二十八、目前的專案結構

今天新增了:

ConversationListResponse.java
ConversationDetailResponse.java
ConversationManagementService.java
ConversationNotFoundException.java

並修改:

ConversationMessage.java
ConversationRepository.java
AIController.java
GlobalExceptionHandler.java

目前專案結構如下:

health-assistant
│
├── src/main/java/com/example/healthassistant
│   │
│   ├── HealthAssistantApplication.java
│   │
│   ├── controller
│   │   ├── HealthController.java
│   │   ├── WaterController.java
│   │   ├── SleepController.java
│   │   ├── HealthSummaryController.java
│   │   └── AIController.java
│   │
│   ├── service
│   │   ├── BmiService.java
│   │   ├── WaterService.java
│   │   ├── SleepService.java
│   │   ├── HealthSummaryService.java
│   │   ├── AIService.java
│   │   ├── AIHealthSummaryService.java
│   │   ├── AIChatService.java
│   │   ├── AIConversationService.java
│   │   ├── ConversationPersistenceService.java
│   │   └── ConversationManagementService.java
│   │
│   ├── repository
│   │   ├── WaterRepository.java
│   │   ├── SleepRepository.java
│   │   └── ConversationRepository.java
│   │
│   ├── dto
│   │   ├── HealthRequest.java
│   │   ├── WaterRequest.java
│   │   ├── WaterHistoryResponse.java
│   │   ├── WaterStatisticsResponse.java
│   │   ├── SleepRequest.java
│   │   ├── SleepResponse.java
│   │   ├── SleepHistoryResponse.java
│   │   ├── SleepStatisticsResponse.java
│   │   ├── WaterSummaryResponse.java
│   │   ├── SleepSummaryResponse.java
│   │   ├── HealthSummaryResponse.java
│   │   ├── AIHealthSummaryResponse.java
│   │   ├── AIChatRequest.java
│   │   ├── AIChatResponse.java
│   │   ├── ConversationRequest.java
│   │   ├── ConversationResponse.java
│   │   ├── ConversationMessage.java
│   │   ├── ConversationListResponse.java
│   │   └── ConversationDetailResponse.java
│   │
│   └── exception
│       ├── GlobalExceptionHandler.java
│       └── ConversationNotFoundException.java
│
├── src/main/resources
│   ├── application.properties
│   └── schema.sql
│
└── pom.xml

目前智慧健康助理的 AI 對話功能已經包含:

建立對話
    ↓
多輪聊天
    ↓
保存資料庫
    ↓
查詢對話列表
    ↓
查看完整聊天紀錄
    ↓
刪除對話

和 Day 24 剛建立單次問答時相比,功能真的完整了不少!


Day 27 小結

今天成功讓智慧健康助理加入 AI 對話管理功能!

原本我們只能透過 conversationId 接續聊天。

現在已經可以查詢所有對話、查看完整歷史訊息,以及刪除不需要的對話。

今天完成的功能包括:

  1. 建立 ConversationListResponse DTO
  2. 建立 ConversationDetailResponse DTO
  3. 擴充 ConversationMessage DTO
  4. 使用 LEFT JOIN 查詢對話列表
  5. 使用 COUNT() 計算訊息數量
  6. 使用 GROUP BY 彙整對話資訊
  7. 建立 ConversationManagementService
  8. 建立 ConversationNotFoundException
  9. 使用 @PathVariable 取得對話 ID
  10. 建立 GET 對話列表 API
  11. 建立 GET 對話詳細資料 API
  12. 建立 DELETE 對話刪除 API
  13. 使用 @Transactional 保護刪除流程
  14. 使用 Postman 測試對話管理

現在我們的 AI 功能已經變成:

智慧健康助理
│
├── BMI 計算與分類
│
├── 飲水量紀錄
│   ├── 新增紀錄
│   ├── 歷史查詢
│   └── 七天統計
│
├── 睡眠時間紀錄
│   ├── 新增紀錄
│   ├── 指定日期查詢
│   └── 七天統計
│
├── 每日健康摘要
│
└── AI 功能
    ├── AI API 串接測試
    ├── AI 每日健康摘要
    ├── AI 單次健康問答
    ├── AI 多輪對話
    ├── AI 對話資料庫保存
    └── AI 對話管理
        ├── 查詢所有對話
        ├── 查詢完整紀錄
        └── 刪除指定對話

今天最大的收穫是:

建立聊天機器人不只是讓 AI 可以回答問題,還需要讓使用者能夠查詢、管理與刪除自己的對話資料。

而且透過今天的練習,我們又進一步熟悉了 Spring Boot、Spring JDBC、REST API,以及資料庫關聯設計。

距離 30 天挑戰結束,真的只剩最後三天了!


明日預告

Day 28|不用每次打開 Postman!用 HTML、CSS 和 JavaScript 打造健康助理聊天介面

到目前為止,我們所有的功能幾乎都是透過 Postman 測試。

雖然 API 已經越來越完整,但如果想讓一般使用者操作,總不能要求每個人都先學會 Postman 吧 XD

所以下一篇準備挑戰:

建立智慧健康助理的簡易網頁介面!

預計會學習:

  • 使用 Spring Boot 提供靜態網頁
  • 建立 HTML 聊天畫面
  • 使用 CSS 設計基本版面
  • 使用 JavaScript 的 fetch() 呼叫 API
  • 將使用者問題送到 Spring Boot
  • 在網頁上顯示 AI 回答
  • 保存 conversationId 以支援連續聊天

讓我們的智慧健康助理終於不只是一組 API,而是有一個可以實際操作的聊天介面!


上一篇
用 H2 Database 保存 AI 多輪對話
下一篇
用 HTML、CSS 和 JavaScript 打造智慧健康助理聊天介面
系列文
Java × AI:30天打造智慧健康助理 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言