見出し画像

【Go】Go+Gin+GORM+PosrgraSQLでAPIサーバつくる

はじめに

🌾 文法についてはこちら

GoでAPIを作ります。
以下など参考になりますが、Goになれてないころに実装してみたら
DB接続等で結構ハマったので、要所を解説していきます

ライブラリを入れる

go installのほうが良いらしいですが、参考にした資料がのきなみgo getなのと、一部うまくいかないものもあったのでコマンドはよしなに。

# gin
go install github.com/gin-gonic/gin@latest
# cors設定
go get github.com/gin-contrib/cors
# postgresドライバ
go get github.com/lib/pq
# ORMapper
go get github.com/jinzhu/gorm
# WebSocket
go get github.com/olahol/melody

設定

できることは以下です。

◼️起動ポートの設定
◼️CORSのためのヘッダ設定,プリフライトリクエストのためにOPTIONSメソッドは必須
◼️エンドポイントごとにHTTPメソッド、関数を設定

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"
	"プロジェクト/service"
	"time"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
)

func main() {
	log.Printf("hello")

	r := gin.Default()

	// create CORS config
	r.Use(cors.New(cors.Config{
		AllowOrigins: []string{
			"http://localhost:3000",
			"http://127.0.0.1:3000",
			"http://CORS許可したいドメイン.com",
		},
		AllowMethods: []string{
			"POST",
			"GET",
			"OPTIONS", // for preflight request
		},
		AllowHeaders: []string{
			"Access-Control-Allow-Credentials",
			"Access-Control-Allow-Headers",
			"Content-Type",
			"Content-Length",
			"Accept-Encoding",
			"Authorization",
		},
		AllowCredentials: true,           // need cookie
		MaxAge:           24 * time.Hour, // preflight request's result chache term
	}))

	// regist API endpoints
	rg := r.Group("/AAA")

	rg.POST("/ping", ping) // localhost:8080/AAA/ping になる

	// usr
	usr := service.NewUsr() // serviceレシーバでのメソッドを登録していく
	rg1 := rg.Group("/usr")
	rg1.POST("/signup", usr.Signup) // URIは/AAA/usr/signup
	rg1.POST("/signin", usr.Signin)

	r.Run(":8080") // ポート指定して起動
}

あとはgo run .とかgo run main.goして
curl -X POST localhost:8080/AAA/ping
とかで試せます。

API処理を作成

レシーバでいい感じにクラスごとにまとめるような雰囲気で作ってみています。
レシーバについてまとめた記事はこちら
具体的な処理の詳細はレポ公開しているのでどうぞ

ネストしたテーブル取得_1対N

親テーブル:子テーブルが1:NのJOINでの抽出や
1:N:M:...といった、ネストした結合での抽出について

すっごい分かりにくかった、けど以下2記事で解決しました。
うまくいく参考記事がなかなか調べても出てこなかったので苦労した。。
最終的に公式リファレンスが最強なんだなって、改めて思いました。

まず入れ子にする構造を作ります。
GORMのタグで、referencesとforeignKeyを使う。
今回はreferencesがなくていけた。勝手にPKを参照したのかな。。?

// 親テーブルの構造
type CateTags struct {
	Id      int
	UsrId   int
	Name    string
	DelFlg  bool
	MstTags []MstTag `gorm:"foreignKey:CategoryId"` // 子テーブルのcategory_idを参照するということ
}
func (CateTags) TableName() string { return "mst_category" }

// 子テーブルの構造。こいつが複数件日も付いてくる。
type MstTag struct {
	Id         int    // タグID
	UsrId      int    // ユーザID
	Name       string // タグ名
	DelFlg     bool   // 削除フラグ
	CategoryId int    // カテゴリID
}
func (MstTag) TableName() string {
	return "mst_tag"
}

// 親を取得しようとする。
var cate []model.CateTags
// CateTagsの中にある、子テーブル要素の変数名を指定
// 子テーブルへのWHERE句があるなら先に指定
db.Preload("MstTags", "del_flg = ?", false).Find(&cate, "mst_category.usr_id = ? AND mst_category.del_flg = false", id)
// Findでは親テーブルへのWHERE句が指定できる

これにより

SELECT * FROM mst_category
LEFT JOIN mst_tag ON mst_tag.category_id = mst_category.id
WHERE
mst_category.usr_id = ?
AND mst_category.del_flg = false
AND mst_tag.del_flg = false

ポイントが、子テーブルへのWHERE句は先に指定しなくちゃいところ。
内部では2回SELECTをしているっぽいので、先に子テーブルをSELECTしてるのがPreloadのとこ、Findの時には親テーブルから取得しているので、子テーブルへの条件を指定したらエラーになった。

ネストしたテーブル取得_1対N対M...

分かりやすさのために、一部どうでもいいカラムを省略します。

いちばん親テーブル > 子供テーブルN件 > さらに、孫がN✖️M件 - 孫と1対1の結合

こういう構造になってます。

// いちばん親
type AllData struct {
	Id         int `gorm:"references:id"` // これと
	UsrId      int
	DelFlg     bool
	AllContent []ContentTags `gorm:"foreignKey:CategoryId"` // 子供のcategory_idが結ばれる
}
func (AllData) TableName() string { return "mst_category" }

// 一階層目の子供
type ContentTags struct {
	Id         int    `gorm:"references:id"` // さらに子供と結合。これと
	Contents   string // コンテンツ
	CategoryId int
	DelFlg     bool      // 削除フラグ
	Tags       []RefTags `gorm:"foreignKey:ContentId"` // 子供のcontent_idを結ぶ
}
func (ContentTags) TableName() string { return "trn_contents" }

// さらに子供
type RefTags struct {
	Id        int    // タグ付けID
	ContentId int    // コンテンツID
	TagId     int    `gorm:"references:tag_id"`
	MstTags   MstTag `gorm:"foreignKey:Id"`
}
func (RefTags) TableName() string { return "trn_contents_tag" }

// RefTagsと1対1の、ふつうな結合
type MstTag struct {
	Id         int    // タグID
	UsrId      int    // ユーザID
	Name       string // タグ名
	DelFlg     bool   // 削除フラグ
	CategoryId int    // カテゴリID
}
func (MstTag) TableName() string {
	return "mst_tag"
}

これをSELECTするにはやはりPreloadを使用します。

var result []model.AllData
db.Preload("AllContent", "trn_contents.del_flg = ?", false).Preload("AllContent.Tags.MstTags", "mst_tag.del_flg = ?", false).Find(&result, "mst_category.usr_id = ? AND mst_category.del_flg = false", id)

ネストの階層が深くなる場合、Preloadの第一引数に指定する文字が
子供.孫のように、ドットでどんどん結んでいくようです。

途中でのWHERE句の指定がなければ

のように、ネストの一番下までのPreloadの記載が1発あればOKです。
子テーブルへのWHERE句指定はあいかわらず、Preloadで対象としているタイミングで行います。
今回の例だと、
・AllContentのとき→[]ContentTags型なので、trn_contentsへの指定
・AllContent.Tags.MstTagsのとき→MstTag型なのでmst_tagへの指定
・最後のFindのとき→大元の型がAllDataなので、mst_categoryへの指定
といった具合です。

テーブルからstruct作るツール

スプシにテーブル定義情報を貼り付けて、structとかNewの関数作るやつを作りました
他にもいろいろ便利ツールは作りたい

ソース書く時の便利スニペット

vimでcoc-snippetsを使ってるので、Ulti.snippet系ならそのまま使えると思います

# A valid snippet should starts with:
#
#		snippet trigger_word [ "description" [ options ] ]
#
# and end with:
#
#		endsnippet
#
# Snippet options:
#
#		b - Beginning of line.
#		i - In-word expansion.
#		w - Word boundary.
#		r - Regular expression
#		e - Custom context snippet
#		A - Snippet will be triggered automatically, when condition matches.
#
# Basic example:
#
#		snippet emitter "emitter properties" b
#		private readonly ${1} = new Emitter<$2>()
#		public readonly ${1/^_(.*)/$1/}: Event<$2> = this.$1.event
#		endsnippet
#
# Online reference: https://github.com/SirVer/ultisnips/blob/master/doc/UltiSnips.txt

snippet reciever "Create Reciever Templete" b
// ==================
// struct def
// ==================
type ${1} struct {
	// TODO members
}
type ${2} interface {
	// TODO methods
}
func New${2}() ${2} {
	return &${1}{}
}
// ==================
// Imprementation
// ==================
// TODO use meth with snippets
meth
endsnippet

snippet insert "gorm insert" b
record := ${1:"struct init"}
result := db.Create(&record)
if result.Error != nil {
	log.Fatal(result.Error.Error())
}
endsnippet

snippet update "gorm update merge" b
var target ${1:"struct"}
db.First(&target, ${2:"primary key"})
target.${3:"upd column"} = ${4:"new value"}
result := db.Save(&target)
if result.Error != nil {
	log.Fatal(result.Error.Error())
}
endsnippet

snippet delete_one "gorm delete by pk" b
db.Delete(&${1:"struct"}{}, ${2:"primary key"})
endsnippet

snippet select_one "gorm select by pk" b
var row ${1:"struct"}
db.First(&row, ${2:"primary key"})
endsnippet

snippet select_where_one "gorm select where one record" b
var row ${1:"struct"}
db.Where("${2:column} = ?", ${3:"value"}).First(&row)
endsnippet

全体

レポはこちら

記事には書きませんでしたが、Gin+melodyでのWebSocketを使ったチャット機能も作ってます。
チャット部屋ごとに、DB読み書きありなのでぜひ参考にしていただければと思います。


ALHについて知る



↓ ↓ ↓ 採用サイトはこちら ↓ ↓ ↓


↓ ↓ ↓ コーポレートサイトはこちら ↓ ↓ ↓


↓ ↓ ↓ もっとALHについて知りたい? ↓ ↓ ↓