コンテンツにスキップ

Phase 3: データベースを読んでみよう

さて、ここまででとにかくレスポンスを返すシンプルなバックエンドのコードを作りました。

しかし、「1章: 知っておきたい! 知識編」で触れたように、Webアプリケーションを構成する登場人物はこれだけではありませんでした。 すでにユーザが登録したデータを読んだり、新たにデータを永続化したりしたいですよね。 そこで、データベースの登場です。

今回は、PostgreSQLというリレーショナル・データベースを、GORMというGo用のORMを介して操作していこうと思います。

データベースのスキーマ定義

backend/model/message.go を見てください。

ここにはSQLのmessagesテーブルの定義のもとになるモデルが書いてあります。

package model
import "time"
// Message は掲示板の1件の投稿を表すモデルです。
// この struct の定義をもとに、GORM が messages テーブルを自動で作ります
// (db/db.go の AutoMigrate を参照)。
//
// `json:"..."` タグは、JSONに変換したときのキー名を指定しています。
type Message struct {
ID uint `gorm:"primaryKey" json:"id"`
Message string `gorm:"size:255;not null" json:"message"`
UserName string `gorm:"size:255;not null" json:"userName"`
CreatedAt time.Time `gorm:"not null" json:"createdAt"`
}

何やら大変なことが書いてありますが、 実は、Phase 2でdocker compose upをしたときに、すでにこの定義からテーブルが自動で作られています。

backend/db/db.go を見ると、アプリの起動時に呼ばれる処理の中に

// model.Message の定義に合わせて messages テーブルを自動で作成・更新する
if err := d.AutoMigrate(&model.Message{}); err != nil {
return fmt.Errorf("failed to migrate database: %w", err)
}

とあります。 このAutoMigrateが実行されると、

CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY,
message VARCHAR(255) NOT NULL,
user_name VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL
);

のようなSQL文が発行され1

id (整数, 行追加時には自動でカウントアップされる)message (255文字以下の文字列, 空っぽは駄目)user_name (255文字以下の文字列, 空っぽは駄目)created_at (時刻, 空っぽは駄目)
1”こんにちは!\nいまなにしてる?""風吹けば名無し”2026-06-06T13:00+09:00

のようなテーブルがデータベースに作られます。 なお、この段階では何のデータ(レコード, 行)も格納されていません。 (上の表の1行目のレコードは、こうなる予定というイメージです。)

本当にテーブルができているのか、覗いてみましょう。 docker composeにはAdminerという「ブラウザからデータベースの中身を覗けるツール」も入れてあります。 http://localhost:8080 を開いて、

  • System: PostgreSQL
  • Server: db
  • Username: app
  • Password: app
  • Database: app

でログインしてみてください。 たしかにmessagesテーブルができている(そして中身は空っぽな)のが確認できるはずです。

messageのハンドラ内でデータベースを操作しよう

backend/api/message.go を見てください。

package api
// /messages エンドポイントのハンドラをこのファイルに実装していきます。
// ここに追記

ここに、以下のように追記してください。 (Goではimportはファイルごとに書くので、このファイルにも必要です。)

// ここに追記
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/twin-te/web-training-2026-template/backend/db"
"github.com/twin-te/web-training-2026-template/backend/model"
)
// GetMessages は `/messages` への `GET` リクエストのハンドラです。
func GetMessages(c echo.Context) error {
messages := []model.Message{}
if err := db.DB.Find(&messages).Error; err != nil {
return err
}
return c.JSON(http.StatusOK, messages)
}

GetMessagesは、Phase 2で/healthのために書いたのと同じ形のハンドラ関数です。 今度は使い回せるように名前を付けて定義しました。

なお、このGetMessagesを、次に main.go/messagesへのGETリクエストのハンドラとして登録してください。

/* ここに追記 */
e.GET("/health", func(c echo.Context) error {
return c.JSON(http.StatusOK, healthResponse{
Status: "ok",
Message: "Hello, World!",
})
})
// 追記する
e.GET("/messages", api.GetMessages)

main.goimportにもapiパッケージの追記が必要ですね。

import (
"net/http"
"os"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/twin-te/web-training-2026-template/backend/api"
"github.com/twin-te/web-training-2026-template/backend/db"
)

もう一度、今 backend/api/message.go に追加したコードを見てみましょう。

func GetMessages(c echo.Context) error {
messages := []model.Message{}
if err := db.DB.Find(&messages).Error; err != nil {
return err
}
return c.JSON(http.StatusOK, messages)
}

この db.DB.Find(&messages) を実行すると、

SELECT * FROM messages;

のようなSQL文が発行されます。 messagesテーブル(先ほど定義を見たテーブルですね)から何でもかんでも(*)を取り出すということです。

そうすると取得できたいくつもの行がmessages(model.Messageのスライス2)に詰め込まれるので、これまでと同様c.JSON()でレスポンスを返します3

動作を検証しよう

今回もまた、curlを使って検証をしていきましょう。 (docker composeを立ち上げっぱなしにしていれば、コードはairが自動でビルドし直してくれていますよ。)

以下のように/messagesを見に行くと…

Terminal window
$ curl http://localhost:3000/messages

何ということでしょう、[]という文字列が返ってきただけです。

Terminal window
$ curl http://localhost:3000/messages
[]

失敗でしょうか…

確認用クライアントからも確認してみましょう。

Terminal window
# frontend/ 以下で
$ bun i
$ bun run dev

まだメッセージはありません

と表示されています。

そういえば先程見たmessagesテーブルには何のレコードも格納されていませんでしたね。

[]はJSONの空の配列を表し、つまり、メッセージが0件取得されたということです。 正しく動いていたのですね!

そうなると、今度はメッセージをデータベースに格納していきたいですね。 さあ、次のフェーズに行きましょう。


Footnotes

  1. 正確には、GORMは既存のテーブルと見比べて足りないテーブルや列だけを作ってくれます。開発中はモデルをいじって再起動するだけでテーブルが追いついてくるので楽ちんです。本物のサービスの運用では、データベースの変更をもっと慎重に管理するためにマイグレーションファイルを使うことが多いです。

  2. スライスというのはGoの可変長の配列(のようなもの)です。

  3. 細かい話ですが、messages := []model.Message{}と「空のスライス」で初期化しているのがミソです。var messages []model.Messageと宣言だけした場合、中身が空だとJSONでは[]ではなくnullになってしまいます。