Next.js × Go のWebアプリをDocker上で作成してみませんか？ 〜DB編〜

# はじめに
👋 こんにちは！ 地方に在住し、フルリモートでWebエンジニアとして働いている @takakouと申します🏙️✨

[前回の記事](https://qiita.com/takakou/items/a01af0515f49e90bd05c)では、Next.js × Go のWebアプリをDocker上で環境構築する方法について解説しました。たくさんの方々から、多くのいいねやストックをいただき、本当に嬉しかったです！🚀💬

今回は、前回の記事で構築した環境をベースに、実際に**APIの開発**をしていく前に、**DBの設計、構築手順**をお伝えします。 このステップでは、私自身が詰まったポイントや工夫した部分を共有しながら、みなさんと一緒にアプリケーションの構築を進めていきますのでよろしくお願いいたします。🌟

前回同様、記事執筆はまだまだ未熟で見づらい記事になってしまっていると思いますが、皆さんからのご意見や質問、アイデアなど、どんな些細なことでも構いませんので、お気軽にコメントしていただけると嬉しいです！💡📝

それでは、Next.jsとGoを組み合わせたWebアプリケーションの開発を楽しんでいきましょう！🎉🐳

# 目次
<!-- タイトルとアンカー名を編集 -->
1. [前提条件 💡](#1-前提条件)
1. [対象者 👨](#2-対象者)
1. [動作環境 🖥️](#3-動作環境)
1. [アプリ概要 📑](#4-アプリ概要)
1. [技術選定 🛠️](#5-技術選定)
1. [実装手順 🚀](#6-実装手順)
1. [参考文献 📚](#7-参考文献)
1. [おわりに 🎉](#8-おわりに)
<!-- 各チャプター -->

# 1. 前提条件
今回の記事の前提条件は、[前回の記事](https://qiita.com/drafts/a01af0515f49e90bd05c/edit)で既に **Next.js×Goのモノレポ構成のDockerの環境**を構築できていることです。そちらの記事をご覧になっていただいた後に、こちらの記事を確認いただけると、記事の内容について深く理解できると思いますのでよろしくお願いいたします。

https://qiita.com/takakou/items/a01af0515f49e90bd05c

# 2. 対象者

* ある程度のWebアプリケーションに関する知識がある人
* Dcokerを使ったことがある人
* 必要な知識は自分自身である程度調べられる人

:::note warn
**注意点**
**コードの詳しい説明は基本的に行いません**。
気になることがある場合は、記事を調べたり、chatGPTに聞いてみたりして自分で解決していただくようにお願いいたします。

:::

# 3. 動作環境

* PC : **MacBook Air(M1,2020)**

* RAM : **8GB**

* OS : **macOS Monterey(ver12.1)**
:::note warn
**注意点**
**Docker** が使える前提で始めていきます。
**Docker**が入っていない場合は、導入するようにお願いいたします。
**Docker Compose V2**を使っています。

:::
# 4. アプリ概要
今回作成するWebアプリケーションの概要を簡単に決めておきたいと思います。

## アプリ名
- 本投稿サイト

## 機能 
- ユーザ認証機能(ログイン、新規登録)
- 本投稿機能(CRUD)

## 設計図
::: note warn
**API仕様書**や**詳細設計**などは次回以降の記事で作成していくので今回は省きます。
:::

### テーブル定義書

- **users**

|論理名  |物理名  |型  |null  |default  |PK  |FK |
|---|---|---|---|---|---|---|
|ID|id|number|× | |○||
|メールアドレス  |email  |string  |×||||
|パスワード  |password  |string  |×||||
|名前 |name  |string  |×||||
|作成日時  |created_at  |datetime  |×|now|||
|更新日時  |updated_at  |datetime  |×|now|||

- **books**

|論理名  |物理名  |型  |null  |default  |PK  |FK |
|---|---|---|---|---|---|---|
|ID|id|number|×| |○||
|ユーザID |user_id  |number  |×|||○|
|タイトル  |title  |string  |×||||
|本文  |body  |string  |×||||
|作成日時  |created_at  |datetime  |×|now|||
|更新日時  |updated_at  |datetime  |×|now|||


# 5. 技術選定
## フロントエンド
* Language: **TypeScript**

* Library: **React**

* FW: **Next.js**

## バックエンド
* Language: **Go**

* FW: **gin**

* ORM: **ent**

* DB: **PostgreSQL**

:::note info
**ポイント**
ORMとは何ぞや？という方向けにORMに関する外部の記事を貼っておきます。
:::
https://utouto97.hatenablog.com/entry/2021/07/21/224759

# 6. 実装手順
:::note warn
注意点
**コンテナの終了**については明記しないので適宜必要なタイミングでdownをしてください。
使わないコンテナは終了しておかないと、PCのディスク容量を圧迫してしまいます。
```
~ /Next_Go_App $ docker compose down
```
:::
##  a. スキーマの作成
今回の記事では**ent**(ORM)を利用してスキーマの作成を行っていきます。
**ent**を用いた実装に関しては公式リファレンスを参考にしています。
**ent**について、日本語での詳細な情報を知りたい場合はメルカリの記事を見ると参考になると思います。

https://entgo.io/ja/docs/getting-started/

https://engineering.mercari.com/blog/entry/20211205-44a97f0e1c/#:~:text=ent%E3%81%AFGo%E3%81%AE%E3%81%9F%E3%82%81,%E8%A1%8C%E3%81%86%E3%81%93%E3%81%A8%E3%81%8C%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82


- コンテナを起動し**shell**へ入る
```
~ /Next_Go_App $ docker compose run backend sh
```

- ユーザのスキーマを作成
```
# go run -mod=mod entgo.io/ent/cmd/ent new User
```

- 本のスキーマを作成
```
# go run -mod=mod entgo.io/ent/cmd/ent new Book
```

- 作成された**ent/schema/user.go**、**ent/schema/book.go**に変更を加える
```diff_go:ent/schema/user.go
package schema

- import "entgo.io/ent"
+ import (
+	 "entgo.io/ent"
+	 "entgo.io/ent/schema/edge"
+	 "entgo.io/ent/schema/field"
+	 "time"
+ )

// User holds the schema definition for the User entity.
type User struct {
	ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
-   return nil
+	return []ent.Field{
+		field.String("email").
+			Unique().
+			NotEmpty(),
+		field.String("password").
+			Sensitive().
+			NotEmpty(),
+		field.String("name").
+			NotEmpty(),
+		field.Time("created_at").
+			Default(time.Now),
+		field.Time("updated_at").
+			Default(time.Now).
+			UpdateDefault(time.Now),
+	}
}

// Edges of the User.
func (User) Edges() []ent.Edge {
-	return nil
+	return []ent.Edge{
+		edge.To("books", Book.Type),
+	}
}
```

```diff_go:ent/schema/book.go
package schema

- import "entgo.io/ent"
+ import (
+	"entgo.io/ent"
+	"entgo.io/ent/schema/edge"
+	"entgo.io/ent/schema/field"
+	"time"
+ )

// Book holds the schema definition for the Book entity.
type Book struct {
	ent.Schema
}

// Fields of the Book.
func (Book) Fields() []ent.Field {
-   return nil
+	return []ent.Field{
+		field.Int("user_id").
+           Positive(),
+		field.String("title").
+			NotEmpty(),
+		field.String("body").
+			NotEmpty(),
+		field.Time("created_at").
+			Default(time.Now),
+		field.Time("updated_at").
+			Default(time.Now).
+			UpdateDefault(time.Now),
+	}
}

// Edges of the Book.
func (Book) Edges() []ent.Edge {
-   return nil
+	return []ent.Edge{
+		edge.From("user", User.Type).
+				Ref("books").
+				Field("user_id").
+				Unique().
+				Required(),
+	}
}

```
fieldの設定、edgeの設定については下記の公式リファレンスを参照してみると良いと思います。

https://entgo.io/ja/docs/schema-fields

https://entgo.io/ja/docs/schema-edges

- 再度コンテナの**shell**に入る
```sh
~ /Next_Go_App/backend $ docker compose run backend sh
```
- `go generate`コマンドを実行
```
# go generate ./ent
```
- backendのフォルダ構成
コマンド実行後、下記のフォルダ構成になっていればスキーマ作成の**準備**は完了しています。
::: note warn
**注意点**
ファイルが新規で作成されない場合はエラーで止まっている可能性が高いのでshellのログを確認してみてください。
:::
```
backend
├── Dockerfile
├── ent
│   ├── book
│   │   ├── book.go
│   │   └── where.go
│   ├── book.go
│   ├── book_create.go
│   ├── book_delete.go
│   ├── book_query.go
│   ├── book_update.go
│   ├── client.go
│   ├── ent.go
│   ├── enttest
│   │   └── enttest.go
│   ├── generate.go
│   ├── hook
│   │   └── hook.go
│   ├── migrate
│   │   ├── migrate.go
│   │   └── schema.go
│   ├── mutation.go
│   ├── predicate
│   │   └── predicate.go
│   ├── runtime
│   │   └── runtime.go
│   ├── runtime.go
│   ├── schema
│   │   ├── book.go
│   │   └── user.go
│   ├── tx.go
│   ├── user
│   │   ├── user.go
│   │   └── where.go
│   ├── user.go
│   ├── user_create.go
│   ├── user_delete.go
│   ├── user_query.go
│   └── user_update.go
├── go.mod
├── go.sum
└── main.go
```

## b. 必要なパッケージを追加
```diff_go
package main

import (
	"github.com/gin-gonic/gin"
+	"context"
+   "log"
+	"Next_Go_App/ent"
+	_ "github.com/lib/pq"
)
func main() {
	//Ginフレームワークのデフォルトの設定を使用してルータを作成
	router := gin.Default()
	
	// ルートハンドラの定義
	router.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "Hello, World!",
		})
	})

	// サーバー起動
	router.Run(":8080")
}
```
- **shell**に入って追記されたパッケージを**install**する
```
~ /Next_Go_App/backend $ docker compose run backend sh

# go mod tidy
```
インストールが無事に完了したら問題ないです。

## c. PostgreSQLとの接続設定
```diff_go
package main

import (
	"github.com/gin-gonic/gin"
	"context"
    "log"
	"github.com/j19015/Next_Go_Bookers2/ent"
	_ "github.com/lib/pq"
)
func main() {
	//Ginフレームワークのデフォルトの設定を使用してルータを作成
	router := gin.Default()

+	//PostgreSQLに接続
+   client, err := ent.Open("postgres", "host=db port=5432 user=postgres dbname=db password=password sslmode=disable")
+
+	if err != nil {
+		log.Fatalf("failed opening connection to postgres: %v", err)
+	}
+	// Run the auto migration tool.
+	if err := client.Schema.Create(context.Background()); err != nil {
+		log.Fatalf("failed creating schema resources: %v", err)
+	}
	
	// ルートハンドラの定義
	router.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "Hello, World!",
		})
	})

	// サーバー起動
	router.Run(":8080")
}
```
backendのコンテナが起動した際に、dbのコンテナへ通信し、スキーマの作成を行っています。
hostやportの設定はcompose.ymlの記述で変わりますので気を付けてください。
Auto migrationについては「**ent**」の公式リファレンスなどを参照することをお勧めします。

https://entgo.io/ja/docs/migrate/

## d.テーブルの作成確認
- コンテナを起動
```
~ /Next_Go_App $ docker compose up backend -d
```

:::note warn
**注意点**
ほかの記事等の再現や、個人開発で、**Docker Compose**を使用してデータベースを含むコンテナを永続化させた場合、コンテナ終了後にその永続化されたデータボリュームが残っていると、次回コンテナを起動する際に競合やエラーが発生する可能性があります。その際は下記コマンドにて永続化されている**データボリュームをリセット**してから再度コンテナの起動を試してください。
```
~ /Next_Go_App $ docker compose down -v
```
:::

- 起動している**db**のbashに入る
```
~ /Next_Go_App $ docker compose exec db bash
```
- **db**に接続
```
dbコンテナID:/# psql -U postgres -d db
```
- テーブル確認コマンド
```
db= # \dt

         List of relations
 Schema | Name  | Type  |  Owner   
--------+-------+-------+----------
 public | books | table | postgres
 public | users | table | postgres
(2 rows)
```
- **User**テーブルの確認
```
db=# \d users

   Column   |           Type           | Collation | Nullable |             Default              
------------+--------------------------+-----------+----------+----------------------------------
 id         | bigint                   |           | not null | generated by default as identity
 email      | character varying        |           | not null | 
 password   | character varying        |           | not null | 
 name       | character varying        |           | not null | 
 created_at | timestamp with time zone |           | not null | 
 updated_at | timestamp with time zone |           | not null | 
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)
    "users_email_key" UNIQUE, btree (email)
Referenced by:
    TABLE "books" CONSTRAINT "books_users_books" FOREIGN KEY (user_id) REFERENCES users(id)
```
- **Book**テーブルの確認
```
db=# \d books

   Column   |           Type           | Collation | Nullable |             Default              
------------+--------------------------+-----------+----------+----------------------------------
 id         | bigint                   |           | not null | generated by default as identity
 title      | character varying        |           | not null | 
 body       | character varying        |           | not null | 
 created_at | timestamp with time zone |           | not null | 
 updated_at | timestamp with time zone |           | not null | 
 user_id    | bigint                   |           | not null | 
Indexes:
    "books_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "books_users_books" FOREIGN KEY (user_id) REFERENCES users(id)
```

それぞれ正常にスキーマが作成されていることが確認できました。
default値やindex、外部キーの説明はこの記事では、省かせていただきますので気になる方は調べてみてください。

# 7. 参考文献
https://qiita.com/zakuroishikuro/items/f33929eab9d55c5bd073

https://qiita.com/ktr1211/items/c16cb4f99f91b998af46

# 8. おわりに
今回の記事では、DBの中で自分が作成したいスキーマが正常に構築できていることを確認しました💾

正直自分が一人でやるときは、リファレンスが少なくかなり苦労しました💦 

次回の記事では、APIの作成方法について今回同様ハンズオン形式で説明していきたいと思っています 📝🔗

もし興味を持っていただけましたら、ぜひ続きも読んでいただけるとうれしいです！ 📖🌟（次回の公開は来月中を予定しています）

(最後に、気づいた方もいらっしゃると思うのですが、絵文字を入れるのは苦手なので、文章に沿った絵文字を挿入するのに、ChatGPTを利用させていただきましたが、いかがでしたでしょうか。不快に思われた方がいたら申し訳ございません 🙇‍♂️)


(10/10追記)
続編となる記事を公開いたしました！
バックエンドの開発へと進まれる方はぜひ見ていただけますと嬉しいです！

https://qiita.com/takakou/items/b00f5065428ac1cb10ec