【Golang】Goa✖️golang-migration✖️gorm でCRUD APIを実装する。

# まえがき
[前記事](https://qiita.com/crml1206/items/ccdf62e713e3c9599fd5)でGoaプロジェクトの環境構築を行なった。本記事ではgoaプロジェクトで[golang-migration](https://github.com/golang-migrate/migrate)と[gorm](https://github.com/go-gorm/gorm)を利用してCRUD APIを実装してみる。

　① docker compose up でPostgreSQLコンテナ起動
　② `golang-migration`でDBスキーマ作成
　③ `gorm`でEntity作成・DB接続・CRUD処理実装
　④ 動作確認

ソースだけ見たい人向け。

https://github.com/daisuketakakuwa/inventory-system

goaのGitHubにexampleプロジェクトがいくつかあるので、プロジェクト構成やコーディング等参考になるのでリンクを貼っておく。

https://github.com/goadesign/examples

# プロジェクト構成
```
  /api
　　/model
　　　/database
　　　　/repository　
　　　　　product.go【DBのCRUD処理】
　　　　/entity
　　　　　product.go【Entity定義】
　　　　connection.go【DB接続処理】
　　/svc
     /design　
       product.go【デザイン定義】
       /request
         product.go【リクエストパラメータ】
       /response
         product.go【レスポンスパラメータ】
  /gen　　 ← goa gen で生成される。手でいじらない。
  /cmd　　 ← goa example で生成される。
    /inventory_system
      main.go【アプリ起動時に実行されるmain関数あり】
  inventory.go ← goa example で生成される。

　/db/migrations【golang-migration用ファイルを格納】
    000001_create_product_table.up.sql
    000001_create_product_table.down.sql
    000002_add_product.up.sql
    000002_add_product.down.sql

　docker-compose.yml
　.env【godotenvでアプリ起動時に読み込む/docker-compose up時に読み込む】
　.env_template ※ .envはGitHubにあげずこれをあげる。利用する時は「.env」にリネーム
```

# ① docker compose up でPostgreSQLコンテナ起動
・コンテナ起動用のYAMLと.envを任意のディレクトリに配置して`docker compose up -d`で起動。

docker-compose.yml
```yml
version: "3.8"
services:
  dbms:
    image: postgres:latest
    restart: always
    environment:
      TZ: ${OS_TIMEZONE}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d postgres -U postgres"]
      interval: 10s
      timeout: 10s
      retries: 5
    ports:
      - ${POSTGRES_PORT}:5432
```
.env ※リポジトリにあげる際は「.env.template」にリネームしてUPする。
```
OS_TIMEZONE=Asia/Tokyo
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_PORT=5435
```

# ② golang-migrationでDBスキーマ作成
golang-migrationの使い方、1ミリを知らないので公式のGetting Startedに沿っていく。

https://github.com/golang-migrate/migrate/blob/master/GETTING_STARTED.md

## migrate CLIインストール

https://github.com/golang-migrate/migrate/blob/master/database/postgres/TUTORIAL.md

```zsh
# Mac
brew install golang-migrate
# インストール確認
% migrate -version
v4.15.2
```

https://github.com/golang-migrate/migrate/tree/master/cmd/migrate

## Migrationファイル作成
migrate CLIで提供されているコマンドを実行することで、migration用のSQLファイルを生成できる。
### productテーブル
```
migrate create -ext sql -dir db/migrations -seq create_product_table
```
・上記コマンドを実行すると、upとdownの2ファイルが作成される。
>000001_create_product_table.up.sql
000001_create_product_table.down.sql

・中身は空なので、それぞれ定義する。

000001_create_product_table.up.sql
```sql
CREATE TABLE IF NOT EXISTS product(
   product_id serial PRIMARY KEY,
   product_name VARCHAR (50) UNIQUE NOT NULL,
   product_description VARCHAR(500),
   product_min_stock INTEGER
);
```
000001_create_product_table.down.sql
```sql
DROP TABLE IF EXISTS product;
```
### productデータ
```
migrate create -ext sql -dir db/migrations -seq add_product
```
・productテーブル同様にコマンドでSQLファイル作成。
>000002_add_product.up.sql
000002_add_product.down.sql

・中身は空なので、それぞれ定義する。

000002_add_product.up.sql
```sql
INSERT INTO product 
(product_name, product_description, product_min_stock)
VALUES
('製品A', '製品Aの説明', '10'),
('製品B', '製品Bの説明', '20'),
('製品C', '製品Cの説明', '30');
```
000002_add_product.down.sql
```sql
DELETE FROM product;
```

## Migration実行(up)
```zsh
# フォーマット
migrate -database ${POSTGRESQL_URL} -path db/migrations up
# 実行コマンド
migrate -database "postgres://postgres:postgres@localhost:5435/postgres?sslmode=disable" -path db/migrations up
```
実行結果
```
1/u create_product_table (42.524774ms)
2/u add_product (81.520087ms)
```
productテーブル作成＆データ追加されていることを確認👍
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/960bd7fe-9436-5092-cf53-bd20c1139676.png)

## Migration実行(down)
upで作成したテーブル/データをdownで取り消す。
```zsh
% migrate -database "postgres://postgres:postgres@localhost:5435/postgres?sslmode=disable" -path db/migrations down
```
実行結果
```zsh
Are you sure you want to apply all down migrations? [y/N]
y
Applying all down migrations
2/d add_product (39.820023ms)
1/d create_product_table (73.951284ms)
```
上記の通り「V2(データ削除)→V1(テーブル削除)」の順番でdownマイグレーションが実行される。
schema_migrationsテーブルは削除されず、レコードのみ削除される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/aa634d32-9fd8-c686-a8aa-06f0ca086b6d.png)

### 各DBの接続URLフォーマット

https://github.com/golang-migrate/migrate/tree/master/database

# ③ gormでEntity作成・DB接続・CRUD処理実装
GolangのORM人気度が下記リポジトリでまとまっている。現時点(2022/05/02)で2位の`gorm`を今回利用していく。

https://github.com/d-tsuji/awesome-go-orms

https://gorm.io/docs/index.html

## DB接続
・必要な依存をインストール
```
go get gorm.io/gorm
go get gorm.io/driver/postgres
```
・inventory-system/api/model/database/connection.go を作成する。
```go
package database

import (
	"log"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

var Db *gorm.DB
var err error

// アプリ起動時にこの関数を呼び出す。
func SetupDb() {
	var host = os.Getenv("DB_HOST")
	var user = os.Getenv("DB_USER")
	var password = os.Getenv("DB_PASSWORD")
	var name = os.Getenv("DB_NAME")
	var port = os.Getenv("DB_PORT")
	var timezone = os.Getenv("OS_TIMEZONE")

	dsn := "host=" + host + " user=" + user + " password=" + password + " dbname=" + name + " port=" + port + " sslmode=disable TimeZone=" + timezone
	Db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatal("FAILED TO CONNECT TO DB")
	} else {
		log.Println("CONNECTED TO DB")
	}
}
```

ルートディレクトリ/.env
```
DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=postgres
DB_PORT=5435
OS_TIMEZONE=Asia/Tokyo
```

※グローバル変数の注意点

https://qiita.com/UHNaKZ/items/637cb3e1c538d8e63ee2

### gorm.Open()で返されるDBオブジェクトについて

・DBオブジェクトの正体はGolang標準ライブラリの[database/sqlのDB struct](https://pkg.go.dev/database/sql#DB)である。

・1つのDBオブジェクトが**複数goroutinesからの利用・Connection Poolingに対応している**ため、gorm.Open()でDBオブジェクトを取得するのは**1アプリ内で1箇所だけでいい**。

>The returned DB is safe for concurrent use by multiple goroutines and maintains its own pool of idle connections. Thus, the Open function should be called just once. It is rarely necessary to close a DB.

https://pkg.go.dev/database/sql#Open

## Entity定義
・inventory-system/api/model/database/entity/product.go を作成する。
```go
package entity

type Product struct {
	ProductID          uint `gorm:"primaryKey"`
	ProductName        string
	ProductDescription string
	ProductMinStock    int32
}
```

## CRUD実装
### Request, Response作成
Designを定義する前に、Create, Update用のリクエストパラメータ、Find用のレスポンスパラメータ を先に作成する。

・inventory-system/api/svc/design/request/product.go を作成する。
```go
package request

import (
	. "goa.design/goa/v3/dsl"
)

var CreateProductPayload = Type("CreateProductPayload", func() {
	Field(1, "productName", String)
	Field(2, "productDescription", String)
	Field(3, "productMinStock", Int32)
	Required("productName")
})

var UpdateProductPayload = Type("UpdateProductPayload", func() {
	Field(1, "productId", Int)
	Field(2, "productName", String)
	Field(3, "productDescription", String)
	Field(4, "productMinStock", Int32)
	Required("productId")
})
```
・inventory-system/api/svc/design/response/product.go を作成する。
```go
package response

import (
	. "goa.design/goa/v3/dsl"
)

var FindProductResult = ResultType("FindProductResult", func() {
	Field(1, "productId", Int)
	Field(2, "productName", String)
	Field(3, "productDescription", String)
	Field(4, "productMinStock", Int32)
})
```

### design作成
・inventory-system/api/svc/design/product.go を作成する。
・productsに対するCRUDのデザインを定義する。
　登録　/products　　　　　　  POST
　更新　/products/{productId}　PUT
　取得　/products/{productId}　GET
　削除　/products/{productId}　DELETE
```go
package design

import (
	"inventory-system/api/svc/design/request"
	"inventory-system/api/svc/design/response"

	. "goa.design/goa/v3/dsl"
)

// APIサーバ定義
var _ = API("inventory-system", func() {
    // API の説明（タイトルと説明）
    Title("Inteventory System Service")
    Description("Service for inventory")

    // ホスト情報
    Server("inventory-system", func() {
        Host("localhost", func() {
            URI("http://localhost:8008") // HTTP REST API
            URI("grpc://localhost:8088") // gRPC
        })
    })
})

// サービス定義
var _ = Service("inventory", func() {
    Description("The inventory service")

    // Create
    Method("create", func() {
        Payload(request.CreateProductPayload)
        Result(String)
        HTTP(func() {
            POST("/products")
        })
        GRPC(func() {
            Response(CodeOK)
        })
    })
    // Update
    Method("update", func() {
        Payload(request.UpdateProductPayload)
        Result(String)
        HTTP(func() {
            PUT("/products/{productId}")
        })
        GRPC(func() {
            Response(CodeOK)
        })
    })
    // Find
    Method("find", func() {
        Payload(func(){
            Field(1, "productId", Int)
            Required("productId")
        })
        Result(response.FindProductResult)
        HTTP(func() {
            GET("/products/{productId}")
        })
        GRPC(func() {
            Response(CodeOK)
        })
    })
    // Delete
    Method("delete", func() {
        Payload(func(){
            Field(1, "productId", Int)
            Required("productId")
        })
        Result(String)
        HTTP(func() {
            DELETE("/products/{productId}")
        })
        GRPC(func() {
            Response(CodeOK)
        })
    })

})
```
### designよりコード作成
前工程で作成したdesignをもとに、goaで雛形ファイルを生成する。
```
// genフォルダ生成
goa gen inventory-system/api/svc/design

// cmdフォルダ + inventory.go生成
goa example inventory-system/api/svc/design
```
雛形ファイルであるinventory.goはこんな感じ。
```go
package inventorysystem

import (
	"context"
	"inventory-system/api/model/database/repository"
	inventory "inventory-system/gen/inventory"
	"log"
)

// inventory service example implementation.
// The example methods log the requests and return zero values.
type inventorysrvc struct {
	logger *log.Logger
}

// NewInventory returns the inventory service implementation.
func NewInventory(logger *log.Logger) inventory.Service {
	return &inventorysrvc{logger}
}

// Create implements create.
func (s *inventorysrvc) Create(ctx context.Context, p *inventory.CreateProductPayload) (res string, err error) {
	s.logger.Print("inventory.create")
	return
}

// Update implements update.
func (s *inventorysrvc) Update(ctx context.Context, p *inventory.UpdateProductPayload) (res string, err error) {
	s.logger.Print("inventory.update")
	return
}

// Find implements find.
func (s *inventorysrvc) Find(ctx context.Context, p *inventory.FindPayload) (res *inventory.Findproductresult, err error) {
	s.logger.Print("inventory.find")
	return
}

// Delete implements delete.
func (s *inventorysrvc) Delete(ctx context.Context, p *inventory.DeletePayload) (res string, err error) {
	s.logger.Print("inventory.delete")
	return
}
```
APIのIF(リクエスト/レスポンス)が確定した状態のものが生成される。
開発者はデザインで確定されたIFを実現できるよう、この雛形ファイルに対して中身のロジックを実装していく。

### cmd/inventory_system/main.go に①DB接続処理 ②.env読込処理 を追加
`goa example`コマンドで生成された`cmd/inventory_system/main.go`内のmain関数はアプリ起動時に実行される関数である。
①DB接続処理 ②.env読込処理 をアプリ起動時に実行したいので、このmain関数内に処理を追加する。
#### ①DB接続処理
```go
package main

import (
    "inventory-system/api/model/database"
    // その他import割愛
)

func main() {
    // ①DB接続処理
    database.SetupDb()
}
```
#### ②.env読込処理
`godotenv`を利用して、ルートディレクトリ/.envを読み込む。

https://github.com/joho/godotenv

インストール
```
go get github.com/joho/godotenv
```
main関数に追加
```go
package main

import (
    "github.com/joho/godotenv"
    // その他import割愛
)

func main() {
    // ②.envロード処理
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	} else {
		log.Println("LOAD .env file")
	}
}
```
最終的にはこんな感じ。
```golang
package main

import (
    "inventory-system/api/model/database"
	"github.com/joho/godotenv"
)

func main() {
	// Load .env file
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	} else {
		log.Println("LOAD .env file")
	}
	// Setup Database
	database.SetupDb()

}
```

### repository作成
・inventory-system/api/model/database/repository/product.go を作成する。
```go
package repository

import (
	"inventory-system/api/model/database"
	"inventory-system/api/model/database/entity"
	inventory "inventory-system/gen/inventory"
	"log"
)

func SaveProduct(p *inventory.CreateProductPayload) {
	product := entity.Product{
		ProductName:        p.ProductName,
		ProductDescription: *p.ProductDescription,
		ProductMinStock:    *p.ProductMinStock,
	}
	database.Db.Create(&product)
}

func UpdateProduct(p *inventory.UpdateProductPayload) {
	// find
	var product entity.Product
	if err := database.Db.Where("product_id = ?", p.ProductID).First(&product).Error; err != nil {
		log.Fatal("NOT FOUND PRODUCT")
	}
	// update
	product.ProductName = *p.ProductName
	product.ProductDescription = *p.ProductDescription
	product.ProductMinStock = *p.ProductMinStock
	database.Db.Save(&product)
}

func FindProduct(id int) entity.Product {
	var product entity.Product
	if err := database.Db.Where("product_id = ?", id).First(&product).Error; err != nil {
		log.Fatal("NOT FOUND PRODUCT")
	}
	return product
}

func DeleteProduct(id int) {
	// find
	var product entity.Product
	if err := database.GetDb().Where("product_id = ?", id).First(&product).Error; err != nil {
		log.Fatal("NOT FOUND PRODUCT")
	}
	// delete
	database.Db.Delete(&product)
}
```

### inventory.go(雛形ファイル)を編集
・各エンドポイントよりrepositoryが提供するCRUD処理を呼び出すように修正する。
```go
package inventorysystem

import (
	"context"
	"inventory-system/api/model/database/repository"
	inventory "inventory-system/gen/inventory"
	"log"
)

// inventory service example implementation.
// The example methods log the requests and return zero values.
type inventorysrvc struct {
	logger *log.Logger
}

// NewInventory returns the inventory service implementation.
func NewInventory(logger *log.Logger) inventory.Service {
	return &inventorysrvc{logger}
}

// Create implements create.
func (s *inventorysrvc) Create(ctx context.Context, p *inventory.CreateProductPayload) (res string, err error) {
	repository.SaveProduct(p)
	s.logger.Print("inventory.create")
	return
}

// Update implements update.
func (s *inventorysrvc) Update(ctx context.Context, p *inventory.UpdateProductPayload) (res string, err error) {
	repository.UpdateProduct(p)
	s.logger.Print("inventory.update")
	return
}

// Find implements find.
func (s *inventorysrvc) Find(ctx context.Context, p *inventory.FindPayload) (res *inventory.Findproductresult, err error) {
	var product = repository.FindProduct(p.ProductID)
	res = &inventory.Findproductresult{
		ProductID:          &p.ProductID,
		ProductName:        &product.ProductName,
		ProductDescription: &product.ProductDescription,
		ProductMinStock:    &product.ProductMinStock,
	}
	s.logger.Print("inventory.find")
	return
}

// Delete implements delete.
func (s *inventorysrvc) Delete(ctx context.Context, p *inventory.DeletePayload) (res string, err error) {
	repository.DeleteProduct(p.ProductID)
	s.logger.Print("inventory.delete")
	return
}
```
gormの使い方は以下参照。

https://gorm.io/docs/index.html

https://qiita.com/gold-kou/items/45a95d61d253184b0f33

# ④ 動作確認
ものは揃ったので、ビルドして実行してみる。
```
// 実行ファイル(inventory_system)を生成
go build ./cmd/inventory_system

//　実行
./inventory_system
```
実行！
```
% go build ./cmd/inventory_system
% ./inventory_system 
2022/05/05 16:01:49 LOAD .env file
2022/05/05 16:01:49 CONNECTED TO DB
[inventorysystem] 16:01:49 HTTP "Create" mounted on POST /products
[inventorysystem] 16:01:49 HTTP "Update" mounted on PUT /products/{productId}
[inventorysystem] 16:01:49 HTTP "Find" mounted on GET /products/{productId}
[inventorysystem] 16:01:49 HTTP "Delete" mounted on DELETE /products/{productId}
[inventorysystem] 16:01:49 serving gRPC method inventory.Inventory/Create
[inventorysystem] 16:01:49 serving gRPC method inventory.Inventory/Update
[inventorysystem] 16:01:49 serving gRPC method inventory.Inventory/Find
[inventorysystem] 16:01:49 serving gRPC method inventory.Inventory/Delete
[inventorysystem] 16:01:49 HTTP server listening on "localhost:8008"
[inventorysystem] 16:01:49 gRPC server listening on "localhost:8088"
```
・アプリ起動時に.envファイル読込処理が走っていること
・アプリ起動時にDB接続処理が走っていること
が確認できる。

以下のcurlコマンドで動作確認実施。

create
```
curl -X POST -H "Content-Type: application/json" -d '{"productName": "createTest","productDescription": "createTest","productMinStock": 111}' localhost:8008/products
```
update
```
curl -X PUT -H "Content-Type: application/json" -d '{"productName": "updateTest","productDescription": "updateTest","productMinStock": 99911}' localhost:8008/products/1
```
find
```
curl localhost:8008/products/1
```
delete
```
curl -X DELETE localhost:8008/products/1
```

# 自分用メモ

init関数、main関数の違い

https://www.geeksforgeeks.org/main-and-init-function-in-golang/#:~:text=The%20main%20purpose%20of%20the,initialized%20in%20the%20global%20context.