Help us understand the problem. What is going on with this article?

「Golang」 ×「Gorm」でシンプルに「Mysql」を操作する

REST APIサーバから受け取ったリクエストパラメータの内容を元にリレーショナルデータベース(Mysql)を更新する」という流れで、説明を進めています。

以下の記事の続編という形で投稿を行っておりますので、仕様などの前提条件はこちらで確認していただけるとありがたいです。

「Golang」 ×「gorilla/mux router」でシンプルなREST APIサーバーを開発する

データベース(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;

コード実装

バージョン情報

ORMマッピング

SQL文でデータベースを操作することも可能ですが、今回は「コードを簡素化する」・「単純なDB操作のみ行う」ためにORMでマッピングします。
データベースの複雑な操作が必要な場合はSQLをそのまま書いたほうが、メンテナンスしやすいと思います。
Gormを採用した理由としては「スター数が多い」「ドキュメントが充実している」からです。

root上でgoモジュールインストール
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の接続情報は一箇所にまとめ読み込むようにすることで可読性が向上します。

root上でgoモジュールインストール
go get gopkg.in/ini.v1
config.ini
[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
config/config.go
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リクエストが行われると、コントローラ内からモデルが呼び出され、データベースの値を参照・更新。
最終的にレスポンスが返されるといった流れになります。

main.go
package main

import (
    "merchandise_control_system/controllers"
)

func main() {
    controllers.StartWebServer()
}
controllers/webserver.go
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)
}

models/middleware.go
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"
}
DB(Mysql)
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"
}
DB(Mysql)
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で実装していきたいと思います。

stranger1989
React、Typescript、AWSを駆使してWebアプリケーションの開発を行っています。
moff
介護施設、リハビリ施設用向けアプリを開発・運営するIoTスタートアップ
https://jp.moff.mobi/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした