こんにちは。
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を行うような設定を記述しています。
# ベースとなる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サーバーを立ち上げる記述をしています。
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を記述します。
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との接続の記述します。
func autoMigration() {
db.AutoMigrate(&models.User{})
db.AutoMigrate(&models.Post{})
}
この記述がポイントです。
モデルに何か追加される度に記述することでテーブルを自動で作成します。
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のリレーションを考えます。
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"`
}
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用の構造体を作りました。
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"`
}
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. ルーティングの設定
次にルーティングを記述します。
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を記述します。
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
}
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を使います。以下に記述します。
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
}
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イメージの作成
$ docker-compose build
# Dockerコンテナ起動
$ docker-compose up -d
# 確認
$ docker-compose ps
# コンテナのシェルに入る
$ docker-compose exec gin-like-twitter-api /bin/bash
# サーバーの立ち上げ
$ go run main.go
としてサーバーを立ち上げます。
user := models.User{
ID: 1,
Name: "aoki",
Posts: []models.Post{{ID: 1, Content: "tweet1"}, {ID: 2, Content: "tweet2"}},
}
db.Create(&user)
で作られたものが表示されるかを確認します。
GET /usersを叩くと、
として、返ってくるのが確認できました。他にもpostmanとかで楽にAPI叩いてみてください。
以上になります。
ご指摘等コメントにて頂けるととても嬉しいです。