33
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Go7Advent Calendar 2019

Day 13

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

Last updated at Posted at 2019-10-07

こんにちは。
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公式ドキュメント
GORM公式ドキュメント
Gin と GORM で作るオレオレ構成 API

#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サーバーを立ち上げる記述をしています。

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を記述します。

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との接続の記述します。

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

この記述がポイントです。
モデルに何か追加される度に記述することでテーブルを自動で作成します。

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のリレーションを考えます。

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"`
}
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用の構造体を作りました。

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"`
}
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. ルーティングの設定

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

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を記述します。

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
}
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を使います。以下に記述します。

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
}
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が、
スクリーンショット 2019-10-07 17.12.54.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

としてサーバーを立ち上げます。

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を叩くと、

スクリーンショット 2019-10-07 17.24.46.png として、返ってくるのが確認できました。

他にもpostmanとかで楽にAPI叩いてみてください。

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

33
38
2

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
33
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?