この記事は何?
注) golang初学者の私が初学者のために書いています。
https://tutorialedge.net/golang/creating-restful-api-with-golang/ のチュートリアル(英語だったが平易な文章で書かれてる。本当に)の内容をベースにして、challengeにあるupdate機能の追加の問題とMySQLを導入してみたという掛け算的内容です。
なので、上記チュートリアルが終了した段階からスタートするため悪しからず。
チュートリアル中のコードで参考になったこと。
https://qiita.com/ngplus6655/items/a38660313383d3ff2136
環境
ubuntu18.04LTS
go1.12.17 linux/amd64
github.com/gorilla/mux
github.com/jinzhu/gorm
updateArticleを追加
まずはチュートリアル中のchallengeの内容を説いてみた
func updateArticle(w http.ResponseWriter, r *http.Request){
vars := mux.Vars(r)
id := vars["id"]
reqBody, _ := ioutil.ReadAll(r.Body)
var updateArticle Article
json.Unmarshal(reqBody, &updateArticle)
for index, article := range Articles {
if article.Id == id {
updateArticle.Id = id
Articles[index] = updateArticle
}
}
}
returnSingleArticleと同様urlのidパラメータから編集するarticleを特定し、直接Articlesスライスの要素に代入しています。
直前にupdateArticle.Id = idとしているのは、パラメータidと送られたjsonデータのidが不一致だとArticleに対してIDがユニークではなくなるため。
マルチプレクサにupdateArticleを登録
myRouter.HandleFunc("/article/{id}", updateArticle).Methods("PUT")
curlしてみる
curl -X PUT -H "Content-Type: application/json" -d '{"id": "1", "Title": "Updated Post", "desc": "the description for my updated post", "content": "my articles content"}' http://localhost:10000/article/1
curl -X GET http://localhost:10000/all
[{"Id":"1","Title":"Updated Post","desc":"the description for my updated post","content":"my articles content"},
{"Id":"2","Title":"Hello 2","desc":"Article Description","content":"Article Content"},
{"Id":"3","Title":"Hello 3","desc":"Article Description","content":"Article Content"}]
意図したとおりにidが1の要素を更新できました。
mysqlの導入
完成形
package main
import (
"fmt"
"log"
"net/http"
"encoding/json"
"io/ioutil"
"strconv"
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
type Article struct {
gorm.Model
Title string `json:"Title"`
Desc string `json:"desc"`
Content string `json:"content"`
}
type Articles []Article
func connectDB() (*gorm.DB, error) {
db, err := gorm.Open("mysql", "ユーザ名:パスワード@/test?charset=utf8&parseTime=True&loc=Local")
return db, err
}
func initDb() *gorm.DB {
db, err := connectDB()
if err != nil {
log.Fatalln("データベースの接続に失敗しました。")
}
return db
}
func idParamToUint(r *http.Request) uint {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"])
var uid uint = uint(id)
return uid
}
func ParseJsonArticle(w http.ResponseWriter ,r *http.Request) Article {
reqBody, _ := ioutil.ReadAll(r.Body)
var article Article
json.Unmarshal(reqBody, &article)
return article
}
func homePage(w http.ResponseWriter, r *http.Request){
fmt.Fprintf(w, "Welcome to the HomePage!")
fmt.Println("Endpoint Hit: homePage")
}
func returnAllArticles(w http.ResponseWriter, r *http.Request){
fmt.Println("Endpoint Hit: returnAllArticles")
db := initDb()
var articles Articles
db.Find(&articles)
json.NewEncoder(w).Encode(articles)
}
func returnSingleArticle(w http.ResponseWriter, r *http.Request){
fmt.Println("called returnSingleArticle")
uid := idParamToUint(r)
db := initDb()
var article Article
db.Where("id = ?", uid).First(&article)
json.NewEncoder(w).Encode(article)
}
func createNewArticle(w http.ResponseWriter, r *http.Request) {
fmt.Println("called createNewArticle")
db := initDb()
article := ParseJsonArticle(w, r)
db.Create(&article)
if db.NewRecord(article) {
log.Println("新規articleの保存に失敗しました。")
}
}
func updateArticle(w http.ResponseWriter, r *http.Request){
fmt.Println("called updateAtricle")
uid := idParamToUint(r)
db := initDb()
updatedArticle := ParseJsonArticle(w, r)
var article Article
db.Where("id = ?", uid).First(&article)
article.Title = updatedArticle.Title
article.Desc = updatedArticle.Desc
article.Content = updatedArticle.Content
db.Save(&article)
}
func deleteArticle(w http.ResponseWriter, r *http.Request) {
fmt.Println("called deleteAtricle")
uid := idParamToUint(r)
db := initDb()
db.Delete(Article{}, "id = ?", uid)
}
func handleRequests() {
myRouter := mux.NewRouter().StrictSlash(true)
myRouter.HandleFunc("/", homePage)
myRouter.HandleFunc("/all", returnAllArticles)
myRouter.HandleFunc("/article/{id}", returnSingleArticle).Methods("GET")
myRouter.HandleFunc("/article", createNewArticle).Methods("POST")
myRouter.HandleFunc("/article/{id}", updateArticle).Methods("PUT")
myRouter.HandleFunc("/article/{id}", deleteArticle).Methods("DELETE")
log.Fatal(http.ListenAndServe(":10000", myRouter))
}
func main() {
fmt.Println("Rest API v2.0 - Mux Routers")
db, err := connectDB()
if err != nil {
log.Fatalln("データベースの接続に失敗しました。")
}
defer db.Close()
db.AutoMigrate(&Article{})
handleRequests()
}
MySQLのインストール
$ sudo apt install mysql-server mysql-client
$ mysql --version
mysql Ver 14.14 Distrib 5.7.30, for Linux (x86_64) using EditLine wrapper
起動
$ sudo service mysql start
初期設定->データベース作成->ユーザ追加
参考 https://qiita.com/houtarou/items/a44ce783d09201fc28f5
$ sudo mysql_secure_installation
$ sudo mysql -u root -p
mysql> CREATE DATABASE test;
mysql> set global validate_password_policy=LOW;
mysql> CREATE USER '名前'@'localhost' IDENTIFIED BY 'パスワード';
mysql> grant all on test.* to ユーザ名@localhost;
Article構造体の再定義
type Article struct {
gorm.Model
Title string `json:"Title"`
Desc string `json:"desc"`
Content string `json:"content"`
}
type Articles []Article
IDやらタイムスタンプをGORMのモデル構造体に切り替えます。
スライスのArticlesはもう使いませんが一括で扱うところが出てくるので型として定義します。
データベース接続
func connectDB() (*gorm.DB, error) {
db, err := gorm.Open("mysql", "ユーザ名:パスワード@/テーブル名?charset=utf8&parseTime=True&loc=Local")
return db, err
}
func initDb() *gorm.DB {
db, err := connectDB()
if err != nil {
log.Fatalln("データベースの接続に失敗しました。")
}
return db
}
/all
func returnAllArticles(w http.ResponseWriter, r *http.Request){
db := initDb()
var articles Articles
db.Find(&articles)
json.NewEncoder(w).Encode(articles)
}
先に定義した構造体をインスタンス化し、db.find関数ですべての行のデータを格納します。
/article/{id} GET
func idParamToUint(r *http.Request) uint {
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"])
var uid uint = uint(id)
return uid
}
func returnSingleArticle(w http.ResponseWriter, r *http.Request){
uid := idParamToUint(r)
db := initDb()
var article Article
db.Where("id = ?", uid).First(&article)
json.NewEncoder(w).Encode(article)
}
urlパラメータのidはstring型ですが、MySQLに登録されているidはuint型です。そのためstring型をuint型に型変換する関数の定義をしています。
strconvパッケージのstringToUintは、uint64型がreturnされてしまうため使わず、いったんint型にしています。
それから、GORMのdb.Where関数つかってidが一致するarticleを一つだけ取り出します。
/article POST
func ParseJsonArticle(w http.ResponseWriter ,r *http.Request) Article {
reqBody, _ := ioutil.ReadAll(r.Body)
var article Article
json.Unmarshal(reqBody, &article)
json.NewEncoder(w).Encode(article)
return article
}
func createNewArticle(w http.ResponseWriter, r *http.Request) {
fmt.Println("called createNewArticle")
db := initDb()
article := ParseJsonArticle(w, r)
db.Create(&article)
if db.NewRecord(article) {
log.Println("新規articleの保存に失敗しました。")
}
}
まずは、関数として(updateでも再利用するため)リクエストボディで送られてくるJSONをパースし、構造体として扱えるようにします。
GORMのCreate関数でMySqlにあたらしい行を追加します。
※ 編集しました(2020/7/16)
log.Fatalln -> log.Printlnに変更しました。
データの保存に失敗したときにプログラムを終了させる必要はないと考えるからです。
/article/{id} PUT
func updateArticle(w http.ResponseWriter, r *http.Request){
uid := idParamToUint(r)
db := initDb()
updatedArticle := ParseJsonArticle(w, r)
var article Article
db.Where("id = ?", uid).First(&article)
article.Title = updatedArticle.Title
article.Desc = updatedArticle.Desc
article.Content = updatedArticle.Content
db.Save(&article)
}
CRUDで最も複雑なupdateですが、今までの組み合わせで実現できました。ここでは、Article構造体を二つインスタンス化させています。
一つはMySQLから引っ張ってきたデータ用、もう一つはリクエストボディから更新後のデータとして入ってくるupdatedArticleで、一つ目のArticleに二つ目のupdatedArticleを代入後Save関数を呼び出しています。
/article/{id} DELETE
func deleteArticle(w http.ResponseWriter, r *http.Request) {
uid := idParamToUint(r)
db := initDb()
db.Delete(Article{}, "id = ?", uid)
}
GORMのDelete関数によって簡単に実装できました
handleRequests関数
func handleRequests() {
myRouter := mux.NewRouter().StrictSlash(true)
myRouter.HandleFunc("/", homePage)
myRouter.HandleFunc("/all", returnAllArticles)
myRouter.HandleFunc("/article/{id}", returnSingleArticle).Methods("GET")
myRouter.HandleFunc("/article", createNewArticle).Methods("POST")
myRouter.HandleFunc("/article/{id}", updateArticle).Methods("PUT")
myRouter.HandleFunc("/article/{id}", deleteArticle).Methods("DELETE")
log.Fatal(http.ListenAndServe(":10000", myRouter))
}
ポイントは、"article/{id}" の.Methods("GET")省略していないところです。
仮に省略してしまうとPUTやDELETEをつけてcurlしても、すべてGETメソッドのreturnSingleArticle関数にルーティングされてしまいます。
まとめ
GORMとGorilla/muxパッケージでいい感じにRestAPIを作れた。
特にGORMに関しては、公式ページが日本語化されていて楽だった。
急いでクライアント側も作らなくては!