【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
設定
できることは以下です。
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について知りたい? ↓ ↓ ↓