昨天的 Day 15,我們終於成功讓 Java 程式透過 JdbcTemplate,將飲水紀錄儲存到 H2 Database。
現在就算重新啟動 Spring Boot,資料也不會像之前使用 List 時一樣直接消失。
不過我發現,目前的飲水紀錄功能還有一個限制。
我們只能查詢今天的飲水量,卻沒辦法查看昨天或前幾天的紀錄!
既然資料都已經存進資料庫了,如果只能查詢今天,好像有點可惜 XD
所以今天決定繼續升級飲水紀錄功能!
除了查詢當日飲水量,也要讓使用者可以指定日期,甚至一次查看最近七天的飲水紀錄。
今天的目標是:
使用 Spring Boot、LocalDate 和 SQL,建立飲水量歷史查詢 API。
目前我們已經有兩個 API:
| HTTP Method | API | 功能 |
|---|---|---|
| POST | /health/water |
新增飲水紀錄 |
| GET | /health/water |
查詢今日飲水總量 |
今天準備再增加兩個:
| HTTP Method | API | 功能 |
|---|---|---|
| GET | /health/water/date?date=2026-10-17 |
查詢指定日期的飲水量 |
| GET | /health/water/history |
查詢最近七天的飲水紀錄 |
例如使用者想知道某一天喝了多少水,可以發送:
GET /health/water/date?date=2026-10-17
系統就會回傳:
{
"date": "2026-10-17",
"totalWater": 1500,
"unit": "ml"
}
如果想查看最近七天的紀錄,就可以發送:
GET /health/water/history
回傳一組包含日期和飲水量的 JSON 陣列。
這樣就能開始觀察不同日期的飲水情況。
在 Day 13,我們已經簡單使用過:
LocalDate
它是 Java 提供的日期類別。
例如:
LocalDate today = LocalDate.now();
代表取得今天的日期。
如果想取得昨天:
LocalDate yesterday = LocalDate.now().minusDays(1);
如果想取得七天前:
LocalDate sevenDaysAgo = LocalDate.now().minusDays(7);
也可以直接指定日期:
LocalDate date = LocalDate.of(2026, 10, 17);
這樣就能建立:
2026-10-17
今天我們會利用 LocalDate,讓使用者查詢不同日期的飲水紀錄。
其實我們在 Day 6 就已經接觸過:
@RequestParam
當時是讓使用者透過網址傳送參數。
例如:
/hello?name=Hannie
今天則要利用相同的概念接收日期。
例如:
/health/water/date?date=2026-10-17
其中:
date=2026-10-17
就是 Query Parameter。
在 Spring Boot 裡面可以這樣接收:
@GetMapping("/date")
public String getWaterByDate(
@RequestParam String date) {
return date;
}
如果使用者輸入:
?date=2026-10-17
就可以取得:
2026-10-17
不過今天我們不想只把日期當成一般字串。
而是希望 Spring Boot 直接將它轉換成:
LocalDate
為了讓 Spring Boot 知道日期的格式,可以使用:
@DateTimeFormat
例如:
@RequestParam
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
LocalDate date
這樣就可以接收:
2026-10-17
這種 ISO 日期格式。
如果使用者輸入不符合格式的日期,例如:
2026/10/17
Spring Boot 就可能回傳 HTTP 400,表示參數格式不正確。
這樣就不需要自己使用字串切割來處理日期。
接下來正式開始修改程式。
首先打開:
WaterRepository.java
昨天我們已經建立:
getTotalWater(LocalDate date)
可以查詢指定日期的飲水總量。
所以今天第一個功能,其實已經有一部分完成了!
不過如果要查詢最近七天的紀錄,我們還需要新增一個 Method。
今天會使用:
query()
來取得多筆資料。
昨天我們使用:
queryForObject()
取得單一結果。
例如:
Integer total = jdbcTemplate.queryForObject(
sql,
Integer.class,
date
);
但今天要查詢最近七天的資料,結果可能不只一筆。
這時候就可以使用:
query()
例如:
jdbcTemplate.query(sql, rowMapper, startDate, endDate);
其中 rowMapper 負責將資料庫查詢結果轉換成 Java 物件。
例如資料庫回傳:
2026-10-15 | 1200
2026-10-16 | 1500
2026-10-17 | 1800
我們就可以把這些資料整理成 Java 的 List。
不過這次的 List 和 Day 13 不太一樣。
Day 13 是使用 List 儲存資料,今天則是使用 List 接收資料庫查詢結果。
真正的資料仍然保存在 H2 Database 裡面。
為了讓回傳資料更清楚,今天先建立一個新的 DTO。
檔案名稱:
WaterHistoryResponse.java
程式如下:
package com.example.healthassistant.dto;
import java.time.LocalDate;
public class WaterHistoryResponse {
private LocalDate date;
private int totalWater;
public WaterHistoryResponse(LocalDate date, int totalWater) {
this.date = date;
this.totalWater = totalWater;
}
public LocalDate getDate() {
return date;
}
public int getTotalWater() {
return totalWater;
}
}
這個 DTO 主要用來表示:
日期
飲水總量
例如:
{
"date": "2026-10-17",
"totalWater": 1500
}
之前我們使用 Map 回傳資料。
但如果回傳內容有固定結構,也可以建立專門的 DTO。
這樣比較容易知道每個欄位代表什麼。
接下來回到:
WaterRepository.java
加入:
public List<WaterHistoryResponse> getWaterHistory(
LocalDate startDate,
LocalDate endDate) {
String sql = """
SELECT record_date, SUM(amount) AS total_water
FROM water_records
WHERE record_date BETWEEN ? AND ?
GROUP BY record_date
ORDER BY record_date
""";
return jdbcTemplate.query(
sql,
(rs, rowNum) -> new WaterHistoryResponse(
rs.getDate("record_date").toLocalDate(),
rs.getInt("total_water")
),
startDate,
endDate
);
}
記得加入:
import java.util.List;
import com.example.healthassistant.dto.WaterHistoryResponse;
這段程式看起來比較複雜,但其實可以拆開理解。
WHERE record_date BETWEEN ? AND ?
代表查詢某個日期範圍內的資料。
例如:
2026-10-11 ~ 2026-10-17
GROUP BY record_date
代表依照日期分組。
因為同一天可能會有很多筆飲水紀錄。
例如:
10/17:300 ml
10/17:500 ml
10/17:250 ml
我們希望把同一天的資料合併。
SUM(amount)
代表計算同一天的飲水總量。
因此:
300 + 500 + 250
就會得到:
1050 ml
ORDER BY record_date
代表依照日期排序。
這樣回傳的紀錄就會按照日期由舊到新排列。
Repository 完成後,接下來要修改 Service。
在:
WaterService.java
加入:
public int getTotalWaterByDate(LocalDate date) {
return waterRepository.getTotalWater(date);
}
這個 Method 負責查詢指定日期的飲水總量。
接著加入:
public List<WaterHistoryResponse> getWaterHistory() {
LocalDate today = LocalDate.now();
LocalDate startDate = today.minusDays(6);
return waterRepository.getWaterHistory(startDate, today);
}
這裡有一個需要注意的地方。
為什麼是:
minusDays(6)
而不是:
minusDays(7)
因為我們希望查詢的是:
包含今天在內的最近七個日曆日。
例如今天是:
10/17
就要查詢:
10/11 ~ 10/17
總共七天。
如果使用 minusDays(7),再加上今天,就會變成八天。
整理之後,WaterService.java 可以改成:
package com.example.healthassistant.service;
import com.example.healthassistant.dto.WaterHistoryResponse;
import com.example.healthassistant.repository.WaterRepository;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
@Service
public class WaterService {
private final WaterRepository waterRepository;
public WaterService(WaterRepository waterRepository) {
this.waterRepository = waterRepository;
}
public int addWater(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException(
"Water amount must be greater than 0."
);
}
LocalDate today = LocalDate.now();
waterRepository.save(amount, today);
return waterRepository.getTotalWater(today);
}
public int getTotalWater() {
return waterRepository.getTotalWater(LocalDate.now());
}
public int getTotalWaterByDate(LocalDate date) {
return waterRepository.getTotalWater(date);
}
public List<WaterHistoryResponse> getWaterHistory() {
LocalDate today = LocalDate.now();
LocalDate startDate = today.minusDays(6);
return waterRepository.getWaterHistory(startDate, today);
}
}
現在 Service 已經可以完成四種工作:
功能開始越來越完整了!
接下來是今天最後一個主要步驟。
我們要在 Controller 加入兩個新的 GET API。
打開:
WaterController.java
加入:
@GetMapping("/date")
public ResponseEntity<Map<String, Object>> getWaterByDate(
@RequestParam
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
LocalDate date) {
int total = waterService.getTotalWaterByDate(date);
Map<String, Object> result = new HashMap<>();
result.put("date", date);
result.put("totalWater", total);
result.put("unit", "ml");
return ResponseEntity.ok(result);
}
這個 API 用來查詢指定日期。
接著加入:
@GetMapping("/history")
public ResponseEntity<List<WaterHistoryResponse>> getWaterHistory() {
List<WaterHistoryResponse> history =
waterService.getWaterHistory();
return ResponseEntity.ok(history);
}
記得在 Controller 補上:
import java.time.LocalDate;
import java.util.List;
import org.springframework.format.annotation.DateTimeFormat;
import com.example.healthassistant.dto.WaterHistoryResponse;
這樣就完成了!
接下來重新啟動 Spring Boot。
先測試指定日期查詢。
HTTP Method:
GET
URL:
http://localhost:8080/health/water/date?date=2026-10-17
如果資料庫裡面有該日期的飲水紀錄,例如:
300 ml
500 ml
700 ml
預期回傳:
{
"date": "2026-10-17",
"totalWater": 1500,
"unit": "ml"
}
如果查詢的日期沒有任何資料,則會回傳:
{
"date": "2026-10-17",
"totalWater": 0,
"unit": "ml"
}
這樣就可以讓使用者查詢特定日期的飲水量。
接著測試:
GET http://localhost:8080/health/water/history
假設今天是 2026 年 10 月 17 日,而且資料庫有以下紀錄:
| 日期 | 飲水總量 |
|---|---|
| 10/13 | 1200 ml |
| 10/14 | 1500 ml |
| 10/15 | 1800 ml |
| 10/16 | 1400 ml |
| 10/17 | 1600 ml |
預期回傳:
[
{
"date": "2026-10-13",
"totalWater": 1200
},
{
"date": "2026-10-14",
"totalWater": 1500
},
{
"date": "2026-10-15",
"totalWater": 1800
},
{
"date": "2026-10-16",
"totalWater": 1400
},
{
"date": "2026-10-17",
"totalWater": 1600
}
]
這裡有一個小細節。
雖然我們查詢的是最近七天,但目前 SQL 只會回傳資料庫裡有紀錄的日期。
如果某一天沒有飲水紀錄,就不會出現在結果中。
例如:
10/11 沒有紀錄
10/12 沒有紀錄
這兩天就不會出現在 JSON 裡面。
而且沒有紀錄不代表當天完全沒有喝水,只代表系統沒有保存到資料。
未來如果要製作完整的七天圖表,就可以再補上沒有紀錄的日期。
經過今天的更新,專案架構大致如下:
health-assistant
│
├── src/main/java/com/example/healthassistant
│ │
│ ├── HealthAssistantApplication.java
│ │
│ ├── controller
│ │ ├── HealthController.java
│ │ └── WaterController.java
│ │
│ ├── service
│ │ ├── BmiService.java
│ │ └── WaterService.java
│ │
│ ├── repository
│ │ └── WaterRepository.java
│ │
│ ├── dto
│ │ ├── HealthRequest.java
│ │ ├── WaterRequest.java
│ │ └── WaterHistoryResponse.java
│ │
│ └── exception
│ └── GlobalExceptionHandler.java
│
├── src/main/resources
│ ├── application.properties
│ └── schema.sql
│
└── pom.xml
今天主要增加了:
WaterHistoryResponse.java
並修改:
WaterRepository.java
WaterService.java
WaterController.java
整個系統依然維持:
Controller
↓
Service
↓
Repository
↓
Database
這樣即使功能持續增加,程式也比較容易整理。
今天使用了幾個新的 SQL 語法。
整理如下:
| SQL 語法 | 功能 |
|---|---|
SELECT |
查詢資料 |
WHERE |
設定查詢條件 |
BETWEEN |
查詢指定範圍 |
SUM() |
加總數值 |
GROUP BY |
將資料分組 |
ORDER BY |
排序查詢結果 |
其中我覺得最重要的是:
GROUP BY
因為我們的飲水紀錄是一次一次新增的。
例如:
10/17 09:00 → 300 ml
10/17 12:00 → 500 ml
10/17 15:00 → 250 ml
但如果想知道當天總共喝了多少水,就需要將同一天的紀錄加總。
所以可以使用:
SELECT record_date, SUM(amount)
FROM water_records
GROUP BY record_date;
這樣就能把多筆資料整理成每日統計結果。
今天的功能雖然還沒有真正使用 AI,但已經開始接近未來的健康趨勢分析了。
因為 AI 如果要整理一段時間的健康紀錄,首先就需要取得歷史資料。
例如:
[
{
"date": "2026-10-15",
"totalWater": 1800
},
{
"date": "2026-10-16",
"totalWater": 1400
},
{
"date": "2026-10-17",
"totalWater": 1600
}
]
未來可以先由 Java 計算:
平均飲水量
最高飲水量
最低飲水量
再讓 AI 協助將統計結果整理成容易閱讀的文字。
例如:
最近幾天的飲水紀錄有所變化,
其中 10/15 的紀錄量最高。
不過目前的資料只是使用者輸入的紀錄,不能直接代表實際攝取量,也不能單憑這些數字判斷是否符合個人健康需求。
所以未來 AI 的角色,會先以整理資料和提供一般資訊為主。
今天成功讓智慧健康助理加入歷史查詢功能!
原本只能查詢今天的飲水總量,現在已經可以:
LocalDate 處理日期@RequestParam 接收日期參數@DateTimeFormat 轉換日期格式GROUP BY 和 SUM() 統計資料JdbcTemplate.query() 取得多筆結果目前飲水紀錄功能已經具備:
新增飲水紀錄
↓
儲存至 H2
↓
查詢當日總量
↓
查詢指定日期
↓
查詢最近七天
今天最大的收穫是:
資料庫不只是拿來保存資料,更重要的是能夠查詢、整理和分析資料。
雖然現在還沒有做出圖表,但已經有了可以用來分析的歷史紀錄。
今天已經成功查詢最近七天的飲水紀錄。
但如果只是回傳一大串 JSON,使用者可能還是不容易看出整體情況。
所以下一篇準備加入統計功能!
預計會學習:
讓智慧健康助理不只可以查詢紀錄,也能開始整理出更有意義的資訊!