【Go】Ginを使ってCRUDなAPIサーバーを作ってみた

こんにちは。
golangの勉強を兼ねてCRUDなAPIサーバーを作りました。
フレームワークにGinを使っています。

開発環境:

|  | Name | Version | What |
|:--|:--|:--|:--|
| backend | golang | 1.12.5 | 高級言語 |
|| gin | 1.4.0 | Webフレームワーク |
|| gorm | 1.9.8 | ORM |
| DB | Postgresql | 11.5 | データベース |

今回作成したリポジトリ https://github.com/katsuomi/gin-like-twitter-api

参考: 
[Gin公式ドキュメント](https://godoc.org/github.com/gin-gonic/gin)
[GORM公式ドキュメント](http://gorm.io/docs/index.html)
[Gin と GORM で作るオレオレ構成 API](https://qiita.com/Asuforce/items/0bde8cabb30ac094fcb4)

#1. 環境構築
##1.1 Dockerのダウンロード
下記より、Docker For Mac か Docker For Windowsをインストールして下さい。
https://docs.docker.com/install/

##1.2 Dockerの設定

Dockerfileを記述します。
Goの開発環境の構築・GORMやpsqlのinstallを行うような設定を記述しています。

```:Dockerfile
# ベースとなるDockerイメージ指定
FROM golang:latest
# コンテナ内に作業ディレクトリを作成
RUN mkdir -p $GOPATH/src/github.com/katsuomi/gin-like-twitter-api
# コンテナログイン時のディレクトリ指定
WORKDIR $GOPATH/src/github.com/katsuomi/gin-like-twitter-api
# ホストのファイルをコンテナの作業ディレクトリに移行
ADD . $GOPATH/src/github.com/katsuomi/gin-like-twitter-api
# 必要なパッケージをイメージにインストールする
RUN go get -u github.com/gin-gonic/gin && \
  go get github.com/jinzhu/gorm && \
  go get github.com/jinzhu/gorm/dialects/postgres
```
docker-compose.ymlを記述します。
WebサーバーとDBサーバーを立ち上げる記述をしています。

```yml:docker-compose.yml
version: '3.7' # composeファイルのバーション指定
services:
  gin-like-twitter-api: # service名
    build: . # ビルドに使用するDockerfileがあるディレクトリ指定
    tty: true # コンテナの起動永続化
    volumes:
      - .:/go/src/github.com/katsuomi/gin-like-twitter-api # マウントディレクトリ指定
    ports:
      - "8080:8080"
  db:
    image: postgres:alpine
    environment:
      POSTGRES_USER: gin-like-twitter-api
      POSTGRES_PASSWORD: gin-like-twitter-api
      POSTGRES_DB: gin-like-twitter-api
    ports:
      - 5432:5432 
```
main.goを記述します。

```go:main.go
package main

import (
	"github.com/katsuomi/gin-like-twitter-api/db"
	"github.com/katsuomi/gin-like-twitter-api/server"
)

func main() {
	db.Init()
	server.Init()
	db.Close()
}
```

#2. DBとの接続
db/db.goにDBとの接続の記述します。

```go:db/db.go
func autoMigration() {
    db.AutoMigrate(&models.User{})
    db.AutoMigrate(&models.Post{})
}
```
この記述がポイントです。
モデルに何か追加される度に記述することでテーブルを自動で作成します。

```go:db/db.go
package db

import (
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/postgres" // Use PostgreSQL in gorm
	"github.com/katsuomi/gin-like-twitter-api/models"
)

var (
	db  *gorm.DB
	err error
)

// Init is initialize db from main function
func Init() {
	db, err = gorm.Open("postgres", "host=db port=5432 user=gin-like-twitter-api dbname=gin-like-twitter-api password=gin-like-twitter-api sslmode=disable")
	if err != nil {
		panic(err)
	}
	autoMigration()
	user := models.User{
		ID:    1,
		Name:  "aoki",
		Posts: []models.Post{{ID: 1, Content: "tweet1"}, {ID: 2, Content: "tweet2"}},
	}
	db.Create(&user)
}

// GetDB is called in models
func GetDB() *gorm.DB {
	return db
}

// Close is closing db
func Close() {
	if err := db.Close(); err != nil {
		panic(err)
	}
}

func autoMigration() {
	db.AutoMigrate(&models.User{})
	db.AutoMigrate(&models.Post{})
}

```
#3. modelの作成
・userはたくさんのpostを持っているような1:Nのリレーションを考えます。

```go:models/user.go
package models

// User is user models property
type User struct {
	ID    uint   `json:"id" binding:"required"`
	Name  string `json:"name" binding:"required"`
	Posts []Post `json:"posts"`
}
```

```go:models/post.go
package models

// Post is post models property
type Post struct {
	ID      uint   `json:"id" binding:"required"`
	Content string `json:"content" binding:"required"`
	User    User   `json:"-" binding:"required"`
	UserID  uint   `gorm:"not null"  json:"user_id"`
}
```

としました。

上記のUser,Postという構造体をそのままAPIとして返すのは何かと面倒なので、api用の構造体を作りました。

```go:form/api/user.go
package api

type User struct {
	ID   uint   `json:"id" binding:"required"`
	Name string `json:"name" binding:"required"`
}

type UserPosts struct {
	ID    uint   `json:"id" binding:"required"`
	Name  string `json:"name" binding:"required"`
	Posts []Post `json:"posts"`
}
```

```go:form/api/post.go
package api

type Post struct {
	ID      uint   `json:"id" binding:"required"`
	Content string `json:"content" binding:"required"`
	UserID  uint   `gorm:"not null"  json:"user_id"`
}

```

#4. ルーティングの設定

次にルーティングを記述します。

```go:server/server.go
package server

import (
	"github.com/gin-gonic/gin"
	"github.com/katsuomi/gin-like-twitter-api/controllers"
)

// Init is initialize server
func Init() {
	r := router()
	r.Run()
}

func router() *gin.Engine {
	r := gin.Default()

	u := r.Group("/users")
	{
		ctrl := controllers.UserController{}
		u.GET("", ctrl.Index)
		u.POST("", ctrl.Create)
		u.GET("/:id", ctrl.Show)
		u.PUT("/:id", ctrl.Update)
		u.DELETE("/:id", ctrl.Delete)
	}

	p := r.Group("/posts")
	{
		ctrl := controllers.PostController{}
		p.GET("", ctrl.Index)
		p.POST("", ctrl.Create)
		p.GET("/:id", ctrl.Show)
		p.PUT("/:id", ctrl.Update)
		p.DELETE("/:id", ctrl.Delete)
	}

	return r
}
```
User,PostモデルにおいてCRUD操作ができるような構成です。

#5. コントローラーの設定

次にcontrollerを記述します。

```go:controllers/post_controller.go
package controllers

import (
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
	"github.com/katsuomi/gin-like-twitter-api/form/api"
	"github.com/katsuomi/gin-like-twitter-api/models"
	"github.com/katsuomi/gin-like-twitter-api/models/repository"
)

// Controller is user controlller
type PostController struct{}

// Index action: GET /posts
func (pc PostController) Index(c *gin.Context) {
	var u repository.PostRepository
	p, err := u.GetAll()
	if err != nil {
		c.AbortWithStatus(404)
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	} else {
		c.JSON(200, p)
	}
}

// Create action: POST /posts
func (pc PostController) Create(c *gin.Context) {
	var u repository.PostRepository
	in := api.Post{}
	if err := c.BindJSON(&in); err != nil {
		return
	}
	in2 := &models.Post{
		ID:      in.ID,
		Content: in.Content,
		UserID:  in.UserID,
	}
	p, err := u.CreateModel(in2)
	if err != nil {
		c.AbortWithStatus(400)
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	} else {
		c.JSON(201, p)
	}
}

// Show action: Get /posts/:id
func (pc PostController) Show(c *gin.Context) {
	id := c.Params.ByName("id")
	var p repository.PostRepository
	idInt, _ := strconv.Atoi(id)
	post, err := p.GetByID(idInt)

	if err != nil {
		c.AbortWithStatus(400)
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	} else {
		c.JSON(200, post)
	}
}

// Update action: Put /posts/:id
func (pc PostController) Update(c *gin.Context) {
	id := c.Params.ByName("id")
	var u repository.PostRepository
	idInt, _ := strconv.Atoi(id)
	p, err := u.UpdateByID(idInt, c)

	if err != nil {
		c.AbortWithStatus(404)
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	} else {
		c.JSON(200, p)
	}
}

// Delete action: DELETE /posts/:id
func (pc PostController) Delete(c *gin.Context) {
	id := c.Params.ByName("id")
	var u repository.PostRepository
	idInt, _ := strconv.Atoi(id)
	if err := u.DeleteByID(idInt); err != nil {
		c.AbortWithStatus(403)
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.JSON(200, gin.H{"success": "ID" + id + "の投稿を削除しました"})
	return
}
```

```go:controllers/user_controller.go
package controllers

import (
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
	"github.com/katsuomi/gin-like-twitter-api/models/repository"
)

// Controller is user controlller
type UserController struct{}

// Index action: GET /users
func (pc UserController) Index(c *gin.Context) {
	var u repository.UserRepository
	p, err := u.GetAll()
	if err != nil {
		c.AbortWithStatus(404)
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	} else {
		c.JSON(200, p)
	}
}

// Create action: POST /users
func (pc UserController) Create(c *gin.Context) {
	var u repository.UserRepository
	p, err := u.CreateModel(c)

	if err != nil {
		c.AbortWithStatus(400)
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	} else {
		c.JSON(201, p)
	}
}

// Show action: Get /users/:id
func (pc UserController) Show(c *gin.Context) {
	id := c.Params.ByName("id")
	var u repository.UserRepository
	idInt, _ := strconv.Atoi(id)
	user, err := u.GetByID(idInt)

	if err != nil {
		c.AbortWithStatus(400)
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	} else {
		c.JSON(200, user)
	}
}

// Update action: Put /users/:id
func (pc UserController) Update(c *gin.Context) {
	id := c.Params.ByName("id")
	var u repository.UserRepository
	idInt, _ := strconv.Atoi(id)
	p, err := u.UpdateByID(idInt, c)

	if err != nil {
		c.AbortWithStatus(404)
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	} else {
		c.JSON(200, p)
	}
}

// Delete action: DELETE /users/:id
func (pc UserController) Delete(c *gin.Context) {
	id := c.Params.ByName("id")
	var u repository.UserRepository
	idInt, _ := strconv.Atoi(id)
	if err := u.DeleteByID(idInt); err != nil {
		c.AbortWithStatus(403)
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.JSON(200, gin.H{"success": "ID" + id + "のユーザーを削除しました"})
	return
}
```

#6. psqlから必要なデータを持ってくる

ORMにはGORMを使います。以下に記述します。

```go:models/repository/user_psql.go
package repository

import (
	"github.com/gin-gonic/gin"
	"github.com/katsuomi/gin-like-twitter-api/db"
	"github.com/katsuomi/gin-like-twitter-api/models"
)

// Service procides user's behavior
type UserRepository struct{}

// User is alias of entity.User struct
type User models.User

type UserProfile struct {
	Name string
	Id   int
}

// GetAll is get all User
func (_ UserRepository) GetAll() ([]UserProfile, error) {
	db := db.GetDB()
	var u []UserProfile
	if err := db.Table("users").Select("name, id").Scan(&u).Error; err != nil {
		return nil, err
	}
	return u, nil
}

// CreateModel is create User model
func (_ UserRepository) CreateModel(c *gin.Context) (User, error) {
	db := db.GetDB()
	var u User
	if err := c.BindJSON(&u); err != nil {
		return u, err
	}
	if err := db.Create(&u).Error; err != nil {
		return u, err
	}
	return u, nil
}

// GetByID is get a User by ID
func (_ UserRepository) GetByID(id int) (models.User, error) {
	db := db.GetDB()
	var me models.User
	if err := db.Where("id = ?", id).First(&me).Error; err != nil {
		return me, err
	}
	var posts []models.Post
	db.Where("id = ?", id).First(&me)
	db.Model(&me).Related(&posts)
	me.Posts = posts

	return me, nil
}

// UpdateByID is update a User
func (_ UserRepository) UpdateByID(id int, c *gin.Context) (models.User, error) {
	db := db.GetDB()
	var u models.User
	if err := db.Where("id = ?", id).First(&u).Error; err != nil {
		return u, err
	}
	if err := c.BindJSON(&u); err != nil {
		return u, err
	}
	u.ID = uint(id)
	db.Save(&u)

	return u, nil
}

// DeleteByID is delete a User by ID
func (_ UserRepository) DeleteByID(id int) error {
	db := db.GetDB()
	var u User

	if err := db.Where("id = ?", id).Delete(&u).Error; err != nil {
		return err
	}

	return nil
}
```

```go:models/repository/post_psql.go
package repository

import (
	"github.com/gin-gonic/gin"
	"github.com/katsuomi/gin-like-twitter-api/db"
	"github.com/katsuomi/gin-like-twitter-api/form/api"
	"github.com/katsuomi/gin-like-twitter-api/models"
)

// Service procides post's behavior
type PostRepository struct{}

// Post is alias of entity.Post struct
type Post api.Post

// GetAll is get all Post
func (_ PostRepository) GetAll() ([]Post, error) {
	db := db.GetDB()
	var p []Post

	if err := db.Find(&p).Error; err != nil {
		return nil, err
	}

	return p, nil
}

// CreateModel is create Post model
func (_ PostRepository) CreateModel(p *models.Post) (*models.Post, error) {
	db := db.GetDB()
	if err := db.Create(&p).Error; err != nil {
		return p, err
	}
	return p, nil
}

// GetByID is get a Post by ID
func (_ PostRepository) GetByID(id int) (models.Post, error) {
	db := db.GetDB()
	var p models.Post
	if err := db.Where("id = ?", id).First(&p).Error; err != nil {
		return p, err
	}
	return p, nil
}

// UpdateByID is update a Post
func (_ PostRepository) UpdateByID(id int, c *gin.Context) (api.Post, error) {
	db := db.GetDB()
	var p api.Post
	if err := db.Where("id = ?", id).First(&p).Error; err != nil {
		return p, err
	}
	userID := p.UserID
	if err := c.BindJSON(&p); err != nil {
		return p, err
	}
	fmt.Printf("%+V", p)
	p.ID = uint(id)
	p.UserID = userID
	db.Save(&p)

	return p, nil
}

// DeleteByID is delete a Post by ID
func (_ PostRepository) DeleteByID(id int) error {
	db := db.GetDB()
	var p Post

	if err := db.Where("id = ?", id).Delete(&p).Error; err != nil {
		return err
	}

	return nil
}
```

#7. サーバーを立ち上げる

さぁ、サーバーを立ち上げてみましょう。

dockerが、
<img width="242" alt="スクリーンショット 2019-10-07 17.12.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270696/7bdce20f-0972-9e0a-e4ed-89152bc27d2c.png">

となっていることを確認できたら、

```
# Dockerイメージの作成
$ docker-compose build

# Dockerコンテナ起動
$ docker-compose up -d

# 確認
$ docker-compose ps

# コンテナのシェルに入る
$ docker-compose exec gin-like-twitter-api /bin/bash 

# サーバーの立ち上げ
$ go run main.go
```
としてサーバーを立ち上げます。

```go:db.go
user := models.User{
 ID:    1,
 Name:  "aoki",
 Posts: []models.Post{{ID: 1, Content: "tweet1"}, {ID: 2, Content: "tweet2"}},
}
db.Create(&user)
```
で作られたものが表示されるかを確認します。

GET /usersを叩くと、

<img width="946" alt="スクリーンショット 2019-10-07 17.24.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270696/63fdbe19-5cf9-19aa-552d-250cd940f5f0.png">
として、返ってくるのが確認できました。

他にも[postman](https://www.getpostman.com/downloads/)とかで楽にAPI叩いてみてください。

以上になります。
ご指摘等コメントにて頂けるととても嬉しいです。