iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

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

使用 Spring JDBC 將飲水紀錄存進資料庫

  • 分享至 

  • xImage
  •  

Day 15|使用 Spring JDBC 將飲水紀錄存進資料庫

昨天的 Day 14,我們終於開始接觸資料庫!

除了認識 H2 Database,也成功透過 H2 Console 建立飲水紀錄資料表,並使用 SQL 新增與查詢資料。

不過目前還有一個問題。

雖然資料庫已經建立好了,但我們的 Java 程式其實還沒有真正使用它!

在 Day 13,我們使用 Java 的 List 儲存飲水紀錄:

private final List<Integer> waterRecords = new ArrayList<>();

這樣雖然可以記錄飲水量,但只要重新啟動 Spring Boot,資料就會消失。

所以今天決定正式升級!

我們要讓使用者透過 API 新增飲水紀錄,再由 Java 自動將資料寫入 H2 Database。

今天的目標是:

學會使用 Spring JDBC 的 JdbcTemplate,讓智慧健康助理可以真正儲存與查詢飲水紀錄。


一、先回顧目前的系統

在 Day 13,我們完成了兩個 API。

HTTP Method API 功能
POST /health/water 新增飲水紀錄
GET /health/water 查詢當日累積飲水量

例如使用者傳送:

{
  "amount": 300
}

系統就會把資料放進 List。

接著再傳送:

{
  "amount": 500
}

就可以計算:

300 + 500 = 800 ml

但這些資料只存在記憶體裡。

今天我們希望將流程改成:

使用者傳送 JSON
        ↓
WaterController
        ↓
WaterService
        ↓
JdbcTemplate
        ↓
H2 Database
        ↓
儲存飲水紀錄

查詢時則變成:

使用者發送 GET
        ↓
WaterController
        ↓
WaterService
        ↓
JdbcTemplate
        ↓
H2 Database
        ↓
查詢當日飲水總量
        ↓
回傳 JSON

這樣就不需要再依靠 List 保存紀錄了。


二、JDBC 是什麼?

今天會接觸到一個新的名詞:

JDBC(Java Database Connectivity)

簡單來說,JDBC 是 Java 用來連接和操作關聯式資料庫的 API。

透過 JDBC,Java 程式可以執行 SQL。

例如:

INSERT INTO water_records (amount, record_date)
VALUES (300, CURRENT_DATE);

或是:

SELECT * FROM water_records;

如果使用傳統 JDBC,我們通常需要自己處理資料庫連線、SQL 執行及資源關閉等工作。

但 Spring Boot 提供了更方便的方式。

那就是今天要使用的:

JdbcTemplate

三、認識 JdbcTemplate

JdbcTemplate 是 Spring JDBC 提供的工具類別。

它可以幫助我們簡化資料庫操作。

例如原本在 H2 Console 輸入:

INSERT INTO water_records (amount, record_date)
VALUES (300, CURRENT_DATE);

現在可以改成在 Java 裡面寫:

jdbcTemplate.update(
    "INSERT INTO water_records (amount, record_date) VALUES (?, CURRENT_DATE)",
    300
);

這樣就能透過 Java 執行 SQL。

其中:

update()

通常用來執行:

  • INSERT
  • UPDATE
  • DELETE

如果要查詢資料,就可以使用:

query()

或是:

queryForObject()

例如:

Integer total = jdbcTemplate.queryForObject(
    "SELECT COALESCE(SUM(amount), 0) FROM water_records",
    Integer.class
);

這樣就可以取得飲水總量。

對目前的專案來說,JdbcTemplate 已經足夠完成基本的資料庫操作。


四、確認 Maven 套件

昨天已經在 pom.xml 加入了:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

以及:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

所以今天不需要再安裝新的套件。

只要確認 Maven 已經成功載入即可。

另外,也要確認 application.properties 還保留昨天的設定:

spring.application.name=health-assistant

spring.datasource.url=jdbc:h2:file:./data/healthdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

這裡最重要的是:

spring.datasource.url=jdbc:h2:file:./data/healthdb

因為我們使用的是 H2 File Mode。

這樣資料才可以儲存在檔案中,而不是只存在記憶體裡。

提醒一下:這些設定適合本機開發練習,正式部署時不應開放 H2 Console,也不應直接使用空密碼。


五、建立資料表

昨天我們已經在 H2 Console 手動建立資料表。

不過我想讓專案更方便使用,所以今天改成透過 SQL 初始化檔案建立資料表。

首先在:

src/main/resources

新增:

schema.sql

內容如下:

CREATE TABLE IF NOT EXISTS water_records (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    amount INT NOT NULL CHECK (amount > 0),
    record_date DATE NOT NULL
);

這裡建立的資料表包含三個欄位:

欄位 型別 用途
id BIGINT 每筆紀錄的編號
amount INT 飲水量,單位 ml
record_date DATE 紀錄日期

其中:

PRIMARY KEY

代表主鍵。

而:

CHECK (amount > 0)

可以讓資料庫額外檢查飲水量必須大於 0。

雖然之前已經使用 @Positive 驗證資料,但資料庫也保留基本限制,會更完整。

接著在 application.properties 加入:

spring.sql.init.mode=always

這樣 Spring Boot 啟動時,就會執行 schema.sql。

因為我們使用:

CREATE TABLE IF NOT EXISTS

所以資料表已經存在時,不會因為重複建立而直接失敗。

不過如果未來需要修改資料表結構,就不能只依靠這個方法,還需要考慮資料庫版本管理工具。

目前先用這種方式練習即可。


六、建立 WaterRepository

在 Day 10,我們學過 Controller 和 Service 分層。

今天要再多認識一個新的分層:

Repository

Repository 主要負責與資料庫溝通。

所以現在的系統架構會變成:

Controller
    ↓
Service
    ↓
Repository
    ↓
Database

每一層負責的工作不同。

分層 主要工作
Controller 接收 HTTP Request、回傳 Response
Service 處理業務邏輯
Repository 執行資料庫操作
Database 儲存資料

這樣就不用把 SQL 全部寫在 Controller 或 Service 裡面。

首先建立新的 Package:

repository

接著建立:

WaterRepository.java

七、使用 JdbcTemplate 新增資料

先來完成第一個功能:新增飲水紀錄。

WaterRepository.java:

package com.example.healthassistant.repository;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;

@Repository
public class WaterRepository {

    private final JdbcTemplate jdbcTemplate;

    public WaterRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void save(int amount, LocalDate date) {

        String sql = """
            INSERT INTO water_records (amount, record_date)
            VALUES (?, ?)
            """;

        jdbcTemplate.update(sql, amount, date);
    }
}

這裡出現了新的 Annotation:

@Repository

它用來標記負責資料存取的類別。

和之前使用的:

@Service

有點類似,都是讓 Spring 管理元件。

但用途不同。

@Service 主要表示業務邏輯元件。

@Repository 則表示資料存取元件。

接著看到:

private final JdbcTemplate jdbcTemplate;

這裡使用了之前學過的建構子注入。

讓 Spring 提供 JdbcTemplate 物件。

最重要的是:

jdbcTemplate.update(sql, amount, date);

它會將飲水量和日期寫入資料庫。


八、SQL 裡面的問號是什麼?

剛剛的 SQL:

INSERT INTO water_records (amount, record_date)
VALUES (?, ?)

裡面有兩個:

?

它們代表 SQL 參數的位置。

例如:

jdbcTemplate.update(sql, 300, LocalDate.now());

第一個問號會對應:

300

第二個問號會對應:

今天的日期

這種方式稱為參數化查詢。

它不需要我們自己把使用者輸入的資料直接拼接到 SQL 字串裡。

例如不建議這樣寫:

String sql = "INSERT INTO water_records (amount) VALUES (" + amount + ")";

雖然某些簡單情況可以運作,但當資料來自使用者輸入時,直接拼接 SQL 字串可能造成安全問題。

所以使用:

?

搭配參數,是比較適合的做法。


九、使用 JdbcTemplate 查詢飲水總量

接下來新增第二個功能。

我們希望查詢指定日期的累積飲水量。

在 WaterRepository 裡面加入:

public int getTotalWater(LocalDate date) {

    String sql = """
        SELECT COALESCE(SUM(amount), 0)
        FROM water_records
        WHERE record_date = ?
        """;

    Integer total = jdbcTemplate.queryForObject(
        sql,
        Integer.class,
        date
    );

    return total != null ? total : 0;
}

這裡使用:

queryForObject()

取得查詢結果。

因為我們只需要一個數字:

當日飲水總量

所以不需要把所有紀錄都查出來。

另外,SQL 裡面使用了:

SUM(amount)

代表將飲水量加總。

而:

COALESCE(SUM(amount), 0)

代表如果查詢結果沒有資料,就回傳 0。

例如今天沒有任何飲水紀錄,結果就會是:

0

而不是:

NULL

這樣比較方便 Java 程式處理。


十、完整的 WaterRepository

整理之後,今天的 WaterRepository.java 如下:

package com.example.healthassistant.repository;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;

@Repository
public class WaterRepository {

    private final JdbcTemplate jdbcTemplate;

    public WaterRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void save(int amount, LocalDate date) {

        String sql = """
            INSERT INTO water_records (amount, record_date)
            VALUES (?, ?)
            """;

        jdbcTemplate.update(sql, amount, date);
    }

    public int getTotalWater(LocalDate date) {

        String sql = """
            SELECT COALESCE(SUM(amount), 0)
            FROM water_records
            WHERE record_date = ?
            """;

        Integer total = jdbcTemplate.queryForObject(
            sql,
            Integer.class,
            date
        );

        return total != null ? total : 0;
    }
}

現在 Repository 已經可以完成:

  1. 新增飲水紀錄
  2. 查詢指定日期的累積飲水量

接下來就要讓 Service 使用 Repository。


十一、修改 WaterService

在 Day 13,我們的 WaterService 使用:

List<Integer>

儲存資料。

但今天已經有資料庫了,所以不需要再使用 List。

我們要把原本的程式改成:

package com.example.healthassistant.service;

import com.example.healthassistant.repository.WaterRepository;

import org.springframework.stereotype.Service;

import java.time.LocalDate;

@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());
    }
}

這次最大的改變,就是原本的:

waterRecords.add(amount);

變成:

waterRepository.save(amount, today);

也就是:

不再把資料加入 List,而是直接寫入資料庫。

另外,之前使用 LocalDate 判斷跨日後清空 List。

今天也不需要這樣做了。

因為每一筆紀錄都有:

record_date

所以查詢時只要指定今天的日期,就可以取得今天的飲水量。

昨天的資料也不需要刪除。

這樣未來就能保留歷史紀錄!


十二、修改 WaterController

接下來修改 Controller。

原本的 API 路徑不需要改變。

我們仍然使用:

POST /health/water

以及:

GET /health/water

完整程式如下:

package com.example.healthassistant.controller;

import com.example.healthassistant.dto.WaterRequest;
import com.example.healthassistant.service.WaterService;

import jakarta.validation.Valid;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/health/water")
public class WaterController {

    private final WaterService waterService;

    public WaterController(WaterService waterService) {
        this.waterService = waterService;
    }

    @PostMapping
    public ResponseEntity<Map<String, Object>> addWater(
            @Valid @RequestBody WaterRequest request) {

        int total = waterService.addWater(request.getAmount());

        Map<String, Object> result = new HashMap<>();

        result.put("message", "Water record saved successfully.");
        result.put("amount", request.getAmount());
        result.put("totalWater", total);
        result.put("unit", "ml");

        return ResponseEntity.ok(result);
    }

    @GetMapping
    public ResponseEntity<Map<String, Object>> getWater() {

        Map<String, Object> result = new HashMap<>();

        result.put("totalWater", waterService.getTotalWater());
        result.put("unit", "ml");

        return ResponseEntity.ok(result);
    }
}

這次 Controller 其實沒有太大的變化。

因為我們之前已經將不同工作分層。

Controller 不需要知道資料到底是放在 List,還是存進 H2 Database。

它只需要呼叫:

waterService.addWater()

以及:

waterService.getTotalWater()

這也是分層架構的好處!


十三、使用 Postman 測試新增資料

接下來重新啟動 Spring Boot。

先測試新增飲水紀錄。

HTTP Method:

POST

URL:

http://localhost:8080/health/water

Body:

{
  "amount": 300
}

如果資料庫原本沒有今天的紀錄,預期回傳:

{
  "message": "Water record saved successfully.",
  "amount": 300,
  "totalWater": 300,
  "unit": "ml"
}

接著再新增一筆:

{
  "amount": 500
}

預期回傳:

{
  "message": "Water record saved successfully.",
  "amount": 500,
  "totalWater": 800,
  "unit": "ml"
}

這次和 Day 13 最大的不同是:

飲水紀錄已經不是存在 List 裡面,而是寫入 H2 Database。

如果之前在 H2 Console 手動新增過今天的資料,累積總量也會包含那些紀錄,因此實際數字可能不同。


十四、確認資料真的存進 H2

光看到 API 回傳成功還不夠。

我們還要確認資料真的進入資料庫。

打開:

http://localhost:8080/h2-console

使用之前設定的 JDBC URL 連線:

jdbc:h2:file:./data/healthdb

接著執行:

SELECT * FROM water_records;

預期可以看到剛剛透過 API 新增的資料。

例如:

ID AMOUNT RECORD_DATE
1 300 2026-10-17
2 500 2026-10-17

實際 ID 和日期會依照資料庫現有內容及執行日期而不同。

這樣就代表:

Postman
   ↓
Spring Boot
   ↓
WaterController
   ↓
WaterService
   ↓
WaterRepository
   ↓
JdbcTemplate
   ↓
H2 Database

整個流程已經成功串接起來!


十五、測試重新啟動後資料是否保留

今天最重要的測試來了。

先確認今天已經新增了飲水紀錄。

接著:

  1. 停止 Spring Boot
  2. 重新啟動 Spring Boot
  3. 再次使用 Postman 發送 GET
  4. 查看累積飲水量

HTTP Method:

GET

URL:

http://localhost:8080/health/water

如果資料庫裡有今天的 800 ml 紀錄,預期回傳:

{
  "totalWater": 800,
  "unit": "ml"
}

這次重新啟動後,資料應該仍然存在!

因為我們已經將資料寫入 H2 的檔案模式資料庫。

不過如果跨日測試,GET API 只會查詢當天的紀錄,因此總量可能變成 0。

這不代表舊資料消失,而是因為我們使用日期條件查詢。


十六、目前的專案結構

經過今天的更新,專案結構又多了一個 Repository。

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
│       │       │
│       │       └── exception
│       │           └── GlobalExceptionHandler.java
│       │
│       └── resources
│           ├── application.properties
│           └── schema.sql
│
├── data
│   └── healthdb.mv.db
│
└── pom.xml

現在的系統已經具備比較完整的分層:

Controller
    ↓
Service
    ↓
Repository
    ↓
Database

雖然程式檔案變多了,但每個 Class 的工作也更明確。


十七、今天的設計還有哪些限制?

雖然今天已經成功將飲水紀錄存進資料庫,但還有幾個地方需要改進。

1. 目前沒有使用者帳號

所有使用者的飲水紀錄都會存在同一張資料表。

而 GET API 也會將當天所有紀錄加總。

所以目前仍然只適合單一使用者的本機練習。

2. 日期依照伺服器時區

目前使用:

LocalDate.now()

取得日期。

但如果未來使用者分布在不同國家,就需要處理時區問題。

3. 還不能查詢歷史紀錄

雖然資料庫已經保存不同日期的資料,但目前 GET API 只會查詢今天。

未來可以增加:

GET /health/water/history

讓使用者查看過去的飲水紀錄。

4. 還沒有刪除或修改功能

如果使用者不小心輸入錯誤的飲水量,目前還不能修改。

之後也可以考慮加入:

PUT
DELETE

讓健康紀錄管理更完整。


十八、資料庫和 AI 的距離又更近了!

今天雖然沒有直接使用 AI,但我們已經完成一個很重要的基礎。

原本智慧健康助理只能處理當下輸入的資料。

現在則可以把紀錄保存下來。

未來就可以讓系統查詢:

最近七天的飲水量

或是:

本週平均飲水量

再將必要的統計結果整理成文字摘要。

例如:

{
  "date": "2026-10-17",
  "totalWater": 1500,
  "unit": "ml"
}

之後如果加入睡眠和運動紀錄,就能讓智慧健康助理處理更多種類的資料。

不過在真正串接 AI 之前,還需要考慮使用者隱私、資料安全,以及哪些資料適合傳送給外部服務。

所以目前先把 Java 和資料庫的基礎建立好。


Day 15 小結

今天終於成功讓 Java 程式直接操作資料庫!

我們不再使用 List 儲存飲水紀錄,而是透過 Spring JDBC 的 JdbcTemplate,將資料寫入 H2 Database。

今天學到的重點包括:

  1. JDBC 的基本概念
  2. Spring JDBC 與 JdbcTemplate
  3. 使用 update() 執行 INSERT
  4. 使用 queryForObject() 查詢資料
  5. 使用 ? 進行參數化查詢
  6. 建立 Repository 分層
  7. 將 WaterService 改成資料庫版本
  8. 透過 Postman 測試資料是否成功保存

現在的飲水紀錄流程已經變成:

使用者輸入飲水量
        ↓
WaterRequest DTO
        ↓
@Valid 驗證
        ↓
WaterController
        ↓
WaterService
        ↓
WaterRepository
        ↓
JdbcTemplate
        ↓
H2 Database
        ↓
儲存飲水紀錄

今天最大的收穫是:

從使用 List 暫存資料,正式進入使用資料庫保存健康紀錄的階段。

目前已經完成 30 天挑戰的一半!

雖然還沒真正串接 AI,但 Java、Spring Boot、REST API、DTO、資料驗證和資料庫,都已經逐漸串起來了。


明日預告

Day 16|健康紀錄不只看今天!建立飲水量歷史查詢 API

今天已經成功將飲水紀錄存進 H2 Database。

但目前我們只能透過 API 查詢當日的飲水總量。

如果想知道:

昨天喝了多少水?

或是:

最近七天的飲水量是多少?

目前還做不到。

所以下一篇準備加入歷史查詢功能!

預計會學習:

  • 使用 SQL 查詢指定日期的資料
  • 認識 @RequestParam 的日期參數
  • 使用 LocalDate 處理日期
  • 建立飲水量歷史查詢 API
  • 為之後的健康趨勢分析做準備

讓智慧健康助理不只可以保存資料,也能開始查看過去的健康紀錄!


上一篇
第一次認識資料庫與 H2
下一篇
建立飲水量歷史查詢 API
系列文
Java × AI:30天打造智慧健康助理 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言