0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[第三回] ~ DB設計とNeonの準備 ~ Docker Compose×Next.js×Go×Neonポートフォリオを作っていく

0
Last updated at Posted at 2026-02-13

記事の概要

このシリーズは、私がポートフォリオを制作する過程を記事にしたものです。

また、GitHubにソースコードを公開しています。

第一回はこちら

第二回
https://qiita.com/snak81/items/740d5748e780a989c90c

今回の作業内容

  • DBの設計を行い、その設計をもとにしてNeonでPostgreSQLを構築する

作業の流れ

  1. 機能の書き出し
  2. DB設計
  3. Neonに実装
  4. Neonのデータを取れるかAPIでテスト

1. 機能の書き出し

必要なデータを考えるために実装したい機能を書き出していきます。

そもそもどんなアプリだっけ?という方は第一回をご覧ください。

1. ユーザー管理機能

自分の「飼い主プロフィール」を作成・管理する機能です。

  • プロフィール登録・編集: アイコン画像、自己紹介文の設定。
  • フォロー機能: 気になる飼い主さんをフォローし、その人の「レシピ」を追いかけやすくする。
  • マイページ: 自分の過去の投稿記事や、いいねした記事を一覧で確認。

2. レシピ投稿・管理機能

わが家の飼い方のコツを「記事」として形にする機能です。

  • 記事作成・編集: タイトル、本文、画像の投稿。
  • ペット属性の設定: 記事に対して「種類(犬・猫など)」と「大きさ(小型・中型・大型)」をタグ付け。
  • 自由タグ(ハッシュタグ): 「#手作りごはん」「#腰痛対策」など、独自のカテゴリを自由に作成・付与。

3. 検索・発見機能

こだわりの「検索」に関連する、情報を探すための機能です。

  • 属性絞り込み検索: 「犬 × 大型」などの条件を組み合わせてレシピを検索。
  • キーワード検索: 記事の本文やタイトルから、気になるワード(例:「餌」「内装」)を探す。
  • タグ検索: ユーザーが作った独自のタグから同じ悩みの記事を探す。

4. コミュニケーション・共有機能

DMはあえて作らず、記事を軸に緩くつながる機能です。

  • いいね機能: 役に立ったレシピに「いいね」でリアクション。
  • コメント機能: 記事に対して質問や感想を投稿。
  • 外部シェア機能: LINEやSNSへ記事のURLを共有(専用のシェアボタン)。

2. DB設計

users

カラム名 型・制約 説明 備考
id uuid (PK) ユーザーID UUIDによる一意識別
username varchar(50) 表示名 サービス上の表示名
email varchar(255) メールアドレス ログイン・連絡用(Unique)
x_account varchar(50) X(Twitter) ID @以降のアカウント名
instagram_account varchar(50) Instagram ID アカウント名
profile_text varchar(200) プロフィール文 飼い主の自己紹介
owned_pets varchar(100) 飼育動物 飼っている動物の種類など
avatar_url text 画像URL アイコンの保存先パス
created_at timestamp 作成日時 DEFAULT NOW()

articles

カラム名 型・制約 説明 備考
id uuid (PK) 記事ID
user_id uuid (FK) 投稿者ID users.id を参照
title varchar(255) タイトル 記事の題名
content text 本文 飼育ノウハウの詳細
pet_type varchar(50) ペットの種類 INDEX設定対象
pet_size varchar(20) ペットの大きさ INDEX設定対象
created_at timestamp 投稿日時 新着順表示に使用

tags

カラム名 型・制約 説明 備考
id serial (PK) タグID 自動連番
name varchar(50) タグ名 ユニーク制約(重複不可)

article_tags

カラム名 型・制約 説明 備考
article_id uuid (FK) 記事ID articles.id を参照
tag_id integer (FK) タグID tags.id を参照

likes

カラム名 型・制約 説明 備考
user_id uuid (FK) ユーザーID いいねしたユーザー
article_id uuid (FK) 記事ID 対象の記事

follows

カラム名 型・制約 説明 備考
follower_id uuid (FK) フォロワーID フォローを実行した人
following_id uuid (FK) フォロー先ID フォローされた人

comments

カラム名 型・制約 説明 備考
id uuid (PK) コメントID 一意識別子
article_id uuid (FK) 記事ID 紐付く記事
user_id uuid (FK) ユーザーID 投稿したユーザー
content text コメント本文

3. Neonに実装

事前準備

以下のサービスを利用していくのでアカウント登録は済ませておいてください。

プロジェクトの作成

ログイン後のダッシュボードで画面右上のNew projectからプロジェクトを作成します。
image.png

image.png

プロジェクトが作成できたら、SQL EditorからDB設計を基にしたSQLを実行していきます。

image.png

PostgreSQL
-- UUID生成用の拡張機能を追加(インストールされていない場合)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- 1. users テーブル
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    username VARCHAR(50) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    x_account VARCHAR(50),
    instagram_account VARCHAR(50),
    profile_text VARCHAR(200),
    owned_pets VARCHAR(100),
    avatar_url TEXT,
    created_at TIMESTAMP DEFAULT NOW()
);

-- 2. articles テーブル
CREATE TABLE articles (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    title VARCHAR(255) NOT NULL,
    content TEXT NOT NULL,
    pet_type VARCHAR(50) NOT NULL,
    pet_size VARCHAR(20) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

-- 検索用インデックス
CREATE INDEX idx_articles_pet_type ON articles(pet_type);
CREATE INDEX idx_articles_pet_size ON articles(pet_size);

-- 3. tags テーブル
CREATE TABLE tags (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) UNIQUE NOT NULL
);

-- 4. article_tags テーブル (中間テーブル)
CREATE TABLE article_tags (
    article_id UUID REFERENCES articles(id) ON DELETE CASCADE,
    tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE,
    PRIMARY KEY (article_id, tag_id)
);

-- 5. likes テーブル
CREATE TABLE likes (
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    article_id UUID REFERENCES articles(id) ON DELETE CASCADE,
    PRIMARY KEY (user_id, article_id)
);

-- 6. follows テーブル
CREATE TABLE follows (
    follower_id UUID REFERENCES users(id) ON DELETE CASCADE,
    following_id UUID REFERENCES users(id) ON DELETE CASCADE,
    PRIMARY KEY (follower_id, following_id),
    CHECK (follower_id <> following_id) -- 自分自身をフォローできないように制約
);

-- 7. comments テーブル
CREATE TABLE comments (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE,
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

実行するとテーブルが作成されます。Tablesに作成したテーブルが表示されていれば成功です。

image.png

4. Neonのデータを取れるかAPIでテスト

Go と Neonの接続

この章では、Go APIとNeonを接続し、APIからDBを利用できるようにしていきます。

今回は、Go言語で広く使われているORMであるGORMを使用していきます。

参考資料:

今回はこんな感じでファイルを分けていきます。

  • main.go: API全体の管理をするファイル。このファイルに他のファイルを集結させる
  • database.go: データベースとAPIの接続を行うファイル
  • models.go: 設計したDBのモデルをORM様に定義するファイル

また、今回はORMであるGORMを利用して開発していきます。

まずは、必要なパッケージをインストールしていきます。

powershell
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go get -u github.com/joho/godotenv

次にdatabase.goを記述していきます。

database.go
// Based on https://neon.com/guides/golang-gorm-postgres
package main

import (
	"fmt"
	"log"
	"os"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"

	"github.com/joho/godotenv"
)

var DB *gorm.DB

func InitDB() {
	//Load Environment
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	// Connection string for Neon Postgres
	dsn := os.Getenv("NEON_DB_URL")

	// Connect to the database
	DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info),
	})
	if err != nil {
		log.Fatalf("Failed to connect to database: %v", err)
	}

	// Get the underlying SQL DB object
	sqlDB, err := DB.DB()
	if err != nil {
		log.Fatalf("Failed to get DB object: %v", err)
	}

	// Verify connection
	if err := sqlDB.Ping(); err != nil {
		log.Fatalf("Failed to ping DB: %v", err)
	}

	fmt.Println("Successfully connected to Neon Postgres database!")
}

参考にしているNeonの公式ドキュメントを別ファイルに切り分けました。
関数名がInitDBになっていたり、最初に変数の宣言をしています。

関数の最初で環境変数を読み込んでいます。
また、dsn := os.Getenv("NEON_DB_URL")では、NEONで作成したDBに接続するためのURLを指定しています。
.envの内容は以下の通りです。

.env
NEON_DB_URL=postgresql://~~~~~~~~~~~~~~~us-east-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require

ここで指定するURLはNEONのダッシュボードから取得します。
image.png
Connection string をクリックするとURLが表示されるのでそのままコピーして使用します。

image.png

次にmodels.goを作成します。

models.go
package main

import (
	"time"

	"github.com/google/uuid"
)

type User struct {
	ID               uuid.UUID `gorm:"type:uuid;primaryKey;default:uuid_generate_v4()"`
	Username         string    `gorm:"size:50;not null"`
	Email            string    `gorm:"size:255;unique;not null"`
	XAccount         string    `gorm:"size:50"`
	InstagramAccount string    `gorm:"size:50"`
	ProfileText      string    `gorm:"size:200"`
	OwnedPets        string    `gorm:"size:100"`
	AvatarURL        string    `gorm:"type:text"`
	CreatedAt        time.Time
	Articles         []Article `gorm:"constraint:OnDelete:CASCADE;"`
	Likes            []Article `gorm:"many2many:likes;"`
	Comments         []Comment `gorm:"constraint:OnDelete:CASCADE;"`
}

type Article struct {
	ID        uuid.UUID `gorm:"type:uuid;primaryKey;default:uuid_generate_v4()"`
	UserID    uuid.UUID `gorm:"type:uuid;not null"`
	Title     string    `gorm:"size:255;not null"`
	Content   string    `gorm:"type:text;not null"`
	PetType   string    `gorm:"size:50;not null;index"`
	PetSize   string    `gorm:"size:20;not null;index"`
	CreatedAt time.Time
	Tags      []Tag `gorm:"many2many:article_tags;constraint:OnDelete:CASCADE;"`
}

type Tag struct {
	ID   uint   `gorm:"primaryKey"`
	Name string `gorm:"size:50;unique;not null"`
}

type Comment struct {
	ID        uuid.UUID `gorm:"type:uuid;primaryKey;default:uuid_generate_v4()"`
	ArticleID uuid.UUID `gorm:"type:uuid;not null"`
	UserID    uuid.UUID `gorm:"type:uuid;not null"`
	Content   string    `gorm:"type:text;not null"`
	CreatedAt time.Time
}

type Follow struct {
	FollowerID  uuid.UUID `gorm:"primaryKey"`
	FollowingID uuid.UUID `gorm:"primaryKey"`
	CreatedAt   time.Time
}

最後はmain.goです。

main.go
package main

import (
	"fmt"
	"log"

	"github.com/gin-gonic/gin"
)

func main() {
	InitDB()

	err := DB.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";").Error
	if err != nil {
		log.Fatalf("Failed to enable UUID extension: %v", err)
	}

	err = DB.AutoMigrate(
		&User{},
		&Article{},
		&Tag{},
		&Comment{},
		&Follow{},
	)
	if err != nil {
		log.Fatalf("Migration failed: %v", err)
	}
	fmt.Println("Migration Copleted")

	// Ginエンジンのインスタンスを作成
	r := gin.Default()

	// ルートURL ("/") に対するGETリクエストをハンドル
	r.GET("/", func(c *gin.Context) {
		// JSONレスポンスを返す
		c.JSON(200, gin.H{
			"message": "Hello World",
		})
	})

	// 8080ポートでサーバーを起動
	r.Run(":8080")
}

この時点では、まだDBテスト用のエンドポイントは作成していません。

新しいパッケージをコードから呼び出したときは依存関係の整理を行う必要があるため、以下のコマンドを実行します。

go mod tidy

4. Neonのデータを取れるかAPIでテスト

テスト用のapiを追加してみます。
今回は、ユーザー登録とユーザー情報の取得を行ってみます。
(セキュリティのことは一旦置いておいて、、)

main.go
package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	InitDB()

	err := DB.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";").Error
	if err != nil {
		log.Fatalf("Failed to enable UUID extension: %v", err)
	}

	err = DB.AutoMigrate(
		&User{},
		&Article{},
		&Tag{},
		&Comment{},
		&Follow{},
	)
	if err != nil {
		log.Fatalf("Migration failed: %v", err)
	}
	fmt.Println("Migration Copleted")

	// Ginエンジンのインスタンスを作成
	r := gin.Default()

	// ルートURL ("/") に対するGETリクエストをハンドル
	r.GET("/", func(c *gin.Context) {
		// JSONレスポンスを返す
		c.JSON(200, gin.H{
			"message": "Hello World",
		})
	})

	r.POST("/users", func(c *gin.Context) {
		var user User
		if err := c.ShouldBindJSON(&user); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		if err := DB.Create(&user).Error; err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		}
		c.JSON(http.StatusOK, user)
	})

	r.GET("/users", func(c *gin.Context) {
		var users []User
		// 関連する記事(Articles)も一緒に取得
		DB.Preload("Articles").Find(&users)
		c.JSON(http.StatusOK, users)
	})

	// 8080ポートでサーバーを起動
	r.Run(":8080")
}

データを追加したいので、以下のコマンドを叩きます。

powershell
# 1. URLを設定
$url = "http://サーバーURL/api/users"

# 2. データを変数 $body に入れる(名前を合わせました)
$body = @{
    Username         = "snak"
    Email            = "snak@example.com"
    XAccount         = "@snak81_"
    InstagramAccount = "snak_insta"
    ProfileText      = "NeonとGoを勉強中です!"
    OwnedPets        = "柴犬, マンチカン"
} | ConvertTo-Json -Compress

# 3. 実行(今度は通るはずです)
$response = Invoke-RestMethod -Uri $url -Method Post -Body $body -ContentType "application/json; charset=utf-8"

# 4. 結果を表示
$response | Format-List

responseはこんな感じで成功しています。

powershell
portfolio-app\api> $response | Format-List


ID               : 80f95fb4-8c30-42ca-bab5-5eac08f1e31b
Username         : snak
Email            : snak@example.com
XAccount         : @snak81_
InstagramAccount : snak_insta
ProfileText      : NeonGoを勉強中です!
OwnedPets        : 柴犬, マンチカン
AvatarURL        :
CreatedAt        : 2026-02-13T07:56:03.798358558Z
Articles         :
Likes            :
Comments         :

また、このURLをブラウザから叩くと(つまりGETすると)

http://サーバーURL/api/users

こんな感じのレスポンスが返ってきたら成功です。

[
  {
    "ID": "80f95fb4-8c30-42ca-bab5-5eac08f1e31b",
    "Username": "snak",
    "Email": "snak@example.com",
    "XAccount": "@snak81_",
    "InstagramAccount": "snak_insta",
    "ProfileText": "NeonとGoを勉強中です!",
    "OwnedPets": "柴犬, マンチカン",
    "AvatarURL": "",
    "CreatedAt": "2026-02-13T07:56:03.798358Z",
    "Articles": [],
    "Likes": null,
    "Comments": null
  }
]

これで、DB設計とNeonの準備、DBのテストは終了です。

次の記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?