記事の概要
このシリーズは、私がポートフォリオを制作する過程を記事にしたものです。
また、GitHubにソースコードを公開しています。
第一回はこちら
第二回
https://qiita.com/snak81/items/740d5748e780a989c90c
今回の作業内容
- DBの設計を行い、その設計をもとにしてNeonでPostgreSQLを構築する
作業の流れ
- 機能の書き出し
- DB設計
- Neonに実装
- Neonのデータを取れるかAPIでテスト
1. 機能の書き出し
必要なデータを考えるために実装したい機能を書き出していきます。
そもそもどんなアプリだっけ?という方は第一回をご覧ください。
1. ユーザー管理機能
自分の「飼い主プロフィール」を作成・管理する機能です。
- プロフィール登録・編集: アイコン画像、自己紹介文の設定。
- フォロー機能: 気になる飼い主さんをフォローし、その人の「レシピ」を追いかけやすくする。
- マイページ: 自分の過去の投稿記事や、いいねした記事を一覧で確認。
2. レシピ投稿・管理機能
わが家の飼い方のコツを「記事」として形にする機能です。
- 記事作成・編集: タイトル、本文、画像の投稿。
- ペット属性の設定: 記事に対して「種類(犬・猫など)」と「大きさ(小型・中型・大型)」をタグ付け。
- 自由タグ(ハッシュタグ): 「#手作りごはん」「#腰痛対策」など、独自のカテゴリを自由に作成・付与。
3. 検索・発見機能
こだわりの「検索」に関連する、情報を探すための機能です。
- 属性絞り込み検索: 「犬 × 大型」などの条件を組み合わせてレシピを検索。
- キーワード検索: 記事の本文やタイトルから、気になるワード(例:「餌」「内装」)を探す。
- タグ検索: ユーザーが作った独自のタグから同じ悩みの記事を探す。
4. コミュニケーション・共有機能
DMはあえて作らず、記事を軸に緩くつながる機能です。
- いいね機能: 役に立ったレシピに「いいね」でリアクション。
- コメント機能: 記事に対して質問や感想を投稿。
- 外部シェア機能: LINEやSNSへ記事のURLを共有(専用のシェアボタン)。
2. DB設計
users
| カラム名 | 型・制約 | 説明 | 備考 |
|---|---|---|---|
| id | uuid (PK) | ユーザーID | UUIDによる一意識別 |
| username | varchar(50) | 表示名 | サービス上の表示名 |
| 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からプロジェクトを作成します。

プロジェクトが作成できたら、SQL EditorからDB設計を基にしたSQLを実行していきます。
-- 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に作成したテーブルが表示されていれば成功です。
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を利用して開発していきます。
まずは、必要なパッケージをインストールしていきます。
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go get -u github.com/joho/godotenv
次に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の内容は以下の通りです。
NEON_DB_URL=postgresql://~~~~~~~~~~~~~~~us-east-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require
ここで指定するURLはNEONのダッシュボードから取得します。

Connection string をクリックするとURLが表示されるのでそのままコピーして使用します。
次に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です。
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を追加してみます。
今回は、ユーザー登録とユーザー情報の取得を行ってみます。
(セキュリティのことは一旦置いておいて、、)
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")
}
データを追加したいので、以下のコマンドを叩きます。
# 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はこんな感じで成功しています。
portfolio-app\api> $response | Format-List
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.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のテストは終了です。
次の記事



