「REST API
サーバから受け取ったリクエストパラメータの内容を元にリレーショナルデータベース(Mysql
)を更新する」という流れで、説明を進めています。
以下の記事の続編という形で投稿を行っておりますので、仕様などの前提条件はこちらで確認していただけるとありがたいです。
データベース(Mysql)準備
コードを実装する前に、Mysql
上でデータベースの作成を行います。
※ Mysql
自体導入していない場合は、インストールが必要です。詳細は他のサイトで解説されているのでここでは割愛します。
# ルートユーザーでログイン
mysql -uroot
# mysql shell上
CREATE DATABASE { データベース名 };
# ログインユーザー作成
GRANT ALL PRIVILEGES ON { データベース名 }.* TO 'username'@'localhost' IDENTIFIED BY 'password';
# 一度mysqlからログアウトして、再度作成したユーザーでログイン
mysql -u username -p
# 念の為、データベースが作成されているか確認
SHOW DATABASES;
コード実装
バージョン情報
- go 1.14
- github.com/jinzhu/gorm v1.9.12
- gopkg.in/ini.v1 v1.56.0
ORMマッピング
SQL
文でデータベースを操作することも可能ですが、今回は「コードを簡素化する」・「単純なDB操作のみ行う」ためにORM
でマッピングします。
データベースの複雑な操作が必要な場合はSQL
をそのまま書いたほうが、メンテナンスしやすいと思います。
Gorm
を採用した理由としては「スター数が多い」「ドキュメントが充実している」からです。
go get github.com/jinzhu/gorm
フォルダ構成
merchandise_control_system
├── config
│ └── config.go
├── controllers
│ └── webserver.go
├── models
│ └── middleware.go
├── config.ini
├── go.mod
├── go.sum
└── main.go
サーバポート情報・データベース接続情報をconfigに記載する
APIサーバのポート番号、DBの接続情報は一箇所にまとめ読み込むようにすることで可読性が向上します。
go get gopkg.in/ini.v1
[db]
db_driver_name = mysql
db_name = { データベース名 }
db_user_name = { データベースユーザー名 }
db_user_password = { データベースパスワード }
db_host = 127.0.0.1
db_port = 3306
[api]
server_port = 8080
package config
import (
"gopkg.in/ini.v1"
"log"
"os"
)
type ConfigList struct {
DbDriverName string
DbName string
DbUserName string
DbUserPassword string
DbHost string
DbPort string
ServerPort int
}
var Config ConfigList
func init() {
cfg, err := ini.Load("config.ini")
if err != nil {
log.Printf("Failed to read file: %v", err)
os.Exit(1)
}
Config = ConfigList{
DbDriverName: cfg.Section("db").Key("db_driver_name").String(),
DbName: cfg.Section("db").Key("db_name").String(),
DbUserName: cfg.Section("db").Key("db_user_name").String(),
DbUserPassword: cfg.Section("db").Key("db_user_password").String(),
DbHost: cfg.Section("db").Key("db_host").String(),
DbPort: cfg.Section("db").Key("db_port").String(),
ServerPort: cfg.Section("api").Key("server_port").MustInt(),
}
}
コード全体
イメージとしては、main.go
からコントローラを呼び出し、APIサーバを起動。
APIリクエストが行われると、コントローラ内からモデルが呼び出され、データベースの値を参照・更新。
最終的にレスポンスが返されるといった流れになります。
package main
import (
"merchandise_control_system/controllers"
)
func main() {
controllers.StartWebServer()
}
package controllers
import (
"encoding/json"
"fmt"
"github.com/gorilla/mux"
"io/ioutil"
"log"
"merchandise_control_system/config"
"merchandise_control_system/models"
"net/http"
"strconv"
)
type DeleteResponse struct {
Id string `json:"id"`
}
func rootPage(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to the Go Api Server")
fmt.Println("Root endpoint is hooked!")
}
func fetchAllItems(w http.ResponseWriter, r *http.Request) {
var items []models.Item
// modelの呼び出し
models.GetAllItems(&items)
responseBody, err := json.Marshal(items)
if err != nil {
log.Fatal(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(responseBody)
}
func fetchSingleItem(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var item models.Item
// modelの呼び出し
models.GetSingleItem(&item, id)
responseBody, err := json.Marshal(item)
if err != nil {
log.Fatal(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(responseBody)
}
func createItem(w http.ResponseWriter, r *http.Request) {
reqBody, _ := ioutil.ReadAll(r.Body)
var item models.Item
if err := json.Unmarshal(reqBody, &item); err != nil {
log.Fatal(err)
}
// modelの呼び出し
models.InsertItem(&item)
responseBody, err := json.Marshal(item)
if err != nil {
log.Fatal(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(responseBody)
}
func deleteItem(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
// modelの呼び出し
models.DeleteItem(id)
responseBody, err := json.Marshal(DeleteResponse{Id: id})
if err != nil {
log.Fatal(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(responseBody)
}
func updateItem(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
reqBody, _ := ioutil.ReadAll(r.Body)
var updateItem models.Item
if err := json.Unmarshal(reqBody, &updateItem); err != nil {
log.Fatal(err)
}
// modelの呼び出し
models.UpdateItem(&updateItem, id)
convertUintId, _ := strconv.ParseUint(id, 10, 64)
updateItem.Model.ID = uint(convertUintId)
responseBody, err := json.Marshal(updateItem)
if err != nil {
log.Fatal(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(responseBody)
}
func StartWebServer() error {
fmt.Println("Rest API with Mux Routers")
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/", rootPage)
router.HandleFunc("/items", fetchAllItems).Methods("GET")
router.HandleFunc("/item/{id}", fetchSingleItem).Methods("GET")
router.HandleFunc("/item", createItem).Methods("POST")
router.HandleFunc("/item/{id}", deleteItem).Methods("DELETE")
router.HandleFunc("/item/{id}", updateItem).Methods("PUT")
return http.ListenAndServe(fmt.Sprintf(":%d", config.Config.ServerPort), router)
}
package models
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"log"
"merchandise_control_system/config"
"time"
)
type Model struct {
ID uint `gorm:"primary_key" json:"id"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at"`
}
type Item struct {
Model
JanCode string `gorm:"size:255" json:"jan_code,omitempty"`
ItemName string `gorm:"size:255" json:"item_name,omitempty"`
Price int `json:"price,omitempty"`
CategoryId int `json:"category_id,omitempty"`
SeriesId int `json:"series_id,omitempty"`
Stock int `json:"stock,omitempty"`
Discontinued bool `json:"discontinued"`
ReleaseDate *time.Time `json:"release_date,omitempty"`
}
var Db *gorm.DB
func GetAllItems(items *[]Item) {
Db.Find(&items)
}
func GetSingleItem(item *Item, key string) {
Db.First(&item, key)
}
func InsertItem(item *Item) {
Db.NewRecord(item)
Db.Create(&item)
}
func DeleteItem(key string) {
Db.Where("id = ?", key).Delete(&Item{})
}
func UpdateItem(item *Item, key string) {
Db.Model(&item).Where("id = ?", key).Updates(
map[string]interface{}{
"jan_code": item.JanCode,
"item_name": item.ItemName,
"price": item.Price,
"category_id": item.CategoryId,
"series_id": item.SeriesId,
"stock": item.Stock,
"discontinued": item.Discontinued,
"release_date": item.ReleaseDate,
})
}
// データベースの初期化
func init() {
var err error
dbConnectInfo := fmt.Sprintf(
`%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local`,
config.Config.DbUserName,
config.Config.DbUserPassword,
config.Config.DbHost,
config.Config.DbPort,
config.Config.DbName,
)
// configから読み込んだ情報を元に、データベースに接続します
Db, err = gorm.Open(config.Config.DbDriverName, dbConnectInfo)
if err != nil {
log.Fatalln(err)
} else {
fmt.Println("Successfully connect database..")
}
// 接続したデータベースにitemsテーブルを作成します
Db.Set("gorm:table_options", "ENGINE = InnoDB").AutoMigrate(&Item{})
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println("Successfully created table..")
}
}
デモ
APIサーバを起動します。と同時にデータベース接続、テーブル作成が行われます。
go run main.go
Postmanからリクエストを送ってみます。(curlコマンドでもOKです)。
【POST】 /item
最初はデータベースが空のため、データを作成します。
{
"jan_code": "32739028488888",
"item_name": "item_1",
"price": 1000,
"category_id": 1,
"series_id": 1,
"stock": 1000,
"discontinued": true,
"release_date": "2020-05-31T07:00:00.660666+09:00"
}
{
"id": 1,
"created_at": "2020-05-31T13:46:09.932124+09:00",
"updated_at": "2020-05-31T13:46:09.932124+09:00",
"deleted_at": null,
"jan_code": "32739028488888",
"item_name": "item_1",
"price": 1000,
"category_id": 1,
"series_id": 1,
"stock": 1000,
"discontinued": true,
"release_date": "2020-05-31T07:00:00.660666+09:00"
}
select * from items;
+----+---------------------+---------------------+------------+----------------+-----------+-------+-------------+-----------+-------+--------------+---------------------+
| id | created_at | updated_at | deleted_at | jan_code | item_name | price | category_id | series_id | stock | discontinued | release_date |
+----+---------------------+---------------------+------------+----------------+-----------+-------+-------------+-----------+-------+--------------+---------------------+
| 1 | 2020-05-31 13:46:10 | 2020-05-31 13:46:10 | NULL | 32739028488888 | item_1 | 1000 | 1 | 1 | 1000 | 1 | 2020-05-31 07:00:01 |
+----+---------------------+---------------------+------------+----------------+-----------+-------+-------------+-----------+-------+--------------+---------------------+
1 row in set (0.00 sec)
【GET】 /item/1
{
"id": 1,
"created_at": "2020-05-31T13:46:10+09:00",
"updated_at": "2020-05-31T13:46:10+09:00",
"deleted_at": null,
"jan_code": "32739028488888",
"item_name": "item_1",
"price": 1000,
"category_id": 1,
"series_id": 1,
"stock": 1000,
"discontinued": true,
"release_date": "2020-05-31T07:00:01+09:00"
}
【GET】 /items
[
{
"id": 1,
"created_at": "2020-05-31T13:46:10+09:00",
"updated_at": "2020-05-31T13:46:10+09:00",
"deleted_at": null,
"jan_code": "32739028488888",
"item_name": "item_1",
"price": 1000,
"category_id": 1,
"series_id": 1,
"stock": 1000,
"discontinued": true,
"release_date": "2020-05-31T07:00:01+09:00"
}
]
【PUT】 /item/1
{
"jan_code": "3273902878656",
"item_name": "item_1_update",
"price": 12000,
"category_id": 1,
"series_id": 1,
"stock": 10,
"discontinued": false,
"release_date": "2020-05-30T10:30:19.978603+09:00"
}
{
"id": 1,
"created_at": null,
"updated_at": "2020-05-31T14:26:04.165559+09:00",
"deleted_at": null,
"jan_code": "3273902878656",
"item_name": "item_1_update",
"price": 12000,
"category_id": 1,
"series_id": 1,
"stock": 10,
"discontinued": false,
"release_date": "2020-05-30T10:30:19.978603+09:00"
}
select * from items;
+----+---------------------+---------------------+------------+---------------+---------------+-------+-------------+-----------+-------+--------------+---------------------+
| id | created_at | updated_at | deleted_at | jan_code | item_name | price | category_id | series_id | stock | discontinued | release_date |
+----+---------------------+---------------------+------------+---------------+---------------+-------+-------------+-----------+-------+--------------+---------------------+
| 1 | 2020-05-31 14:25:15 | 2020-05-31 14:26:04 | NULL | 3273902878656 | item_1_update | 12000 | 1 | 1 | 10 | 0 | 2020-05-30 10:30:20 |
+----+---------------------+---------------------+------------+---------------+---------------+-------+-------------+-----------+-------+--------------+---------------------+
1 row in set (0.00 sec)
【DELETE】 /item/1
{
"id": "1"
}
まとめ
Gorm
を使うと、比較的簡単にデータベース接続、テーブル作成、テーブル操作を行うことができました。
また、ORM
の特徴として、DBを他のDB(例えばPostgreSQL
)に変更したとしてもコードの変更が少なくて済むのもメリットかと思います。
細かいコードメンテナンスは必要ですが、一旦最低限の「APIサーバ起動」と「DB接続」が可能になりました。
今後こちらのサーバサイドコードを成長させながら、「ビュー」部分をReactNative
で実装していきたいと思います。