beegoとgormでチャットapiを作成する

flutterのチュートリアル?codelabs(https://codelabs.developers.google.com/codelabs/flutter)
でチャットアプリの作成をやっている中で、チャットのメッセージ情報を腹持ちしているのをapiから取得したいと思いbeegoでサクッとapiを作成しました。

コード(https://github.com/fu-yuta/go_friendly_chat/tree/create_chat_api)

# 環境
go: v1.17
beego: v1.12.1
gorm: v1.9.16
mysql:  v8.0.27 for macos11.6

# フォルダ構成
```shell
├── conf
│   └── app.conf
├── controllers
│   ├── chat.go
│   ├── requests
│   │   └── chat.go
│   └── responses
│       └── chat.go
├── go.mod
├── main.go
├── models
│   └── chat.go
└── routers
    └── router.go
```

`bee api {アプリ名}`で作成されるテンプレートの構成を使用しています。

# DB操作
## DBへの接続
接続情報を`conf/app.conf`に記述する。

```conf
db_driver_name = mysql
db_name = chat_db
db_user_name = root
db_user_password = root
db_host = tcp(127.0.0.1:3306)
```

`model/chat.go`でDBに接続(初期化)する。

```go:model/chat.go
import (
	"errors"
	"fmt"
	"go_friendly_chat/controllers/requests"
	"log"

	"github.com/astaxie/beego"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
)

var db *gorm.DB

func init() {
	db = setupDB()
}

func setupDB() *gorm.DB {
	DBDriverName := beego.AppConfig.String("db_driver_name")
	DBName := beego.AppConfig.String("db_name")
	DBUserName := beego.AppConfig.String("db_user_name")
	DBUserPassword := beego.AppConfig.String("db_user_password")
	DBHost := beego.AppConfig.String("db_host")

	connectionInfo := fmt.Sprintf("%s:%s@%s/%s", DBUserName, DBUserPassword, DBHost, DBName)
	db, err := gorm.Open(DBDriverName, connectionInfo)

	if err != nil {
		log.Println(err.Error())
	}

	return db

}
```

## DBへの操作
`models/chat.go`にDBへの各操作を記述する

### chatデータの定義
今回は、`ユーザーの名前`と`メッセージ`を持つデータを定義する。

```go:models/chat.go
type Chat struct {
	Id       uint   `gorm:"primary_key"`
	UserName string `gorm:"size:255"`
	Message  string `gorm:"size:255"`
}
```

`Id uint gorm:"primary_key"`で定義した値は、データの保存の際などにgormがユニークでいい感じに連番(1からスタート)を振ってくれるようでした。

### レコードの取得
全てのデータを検索する時は`Find`にchatデータのスライスを送る。

```go:models/chat.go
func GetAllChats() []Chat {
	var allChats []Chat
    // SELECT * FROM chat
	db.Find(&allChats)
	return allChats
}
```

単一データの検索は`First`にchatデータとidの値を送る。
見つからなかった場合はidが0となる。

```go:models/chat.go
func GetChat(id string) (*Chat, error) {
	var chat Chat
    // SELECT * FROM chat WHERE id = 10;(入力したidが10の場合)        
	db.First(&chat, id)
	if chat.Id == 0 {
		log.Println("not found chat error")
		return nil, errors.New("not found chat error")
	}

	return &chat, nil
}
```

### レコードの保存
`NewRecord`でchatデータをDBに保存するレコードに変換して、`Create`で保存する
 
```go:models/chat.go
func AddChat(c Chat) (*Chat, error) {
	if !db.NewRecord(c) {
		log.Println("NewRecord error")
		return nil, errors.New("NewRecord error")
	}

    // INSERT INTO `chat` (`id`,`user_name`,`message`) VALUES ("Id", "UserName", "Message")
	err := db.Create(&c).Error
	if err != nil {
		log.Println("Create error")
		return nil, err
	}

	return &c, nil
}
```

### レコードの更新
更新するデータを`First`で取得してから、`Save`で値を更新する。

```go:models/chat.go
func UpdateChat(id string, updateChat requests.UpdateChat) (*Chat, error) {
	var chat Chat
	db.First(&chat, id)
	if chat.Id == 0 {
		log.Println("not found chat error")
		return nil, errors.New("not found chat error")
	}

	chat.Message = updateChat.Message

    // UPDATE chat SET user_name=UserName, message=Message WHERE id=Id;
	err := db.Save(&chat).Error
	if err != nil {
		log.Println("Update error")
		return nil, err
	}
	return &chat, nil
}
```

### レコードの削除
更新するデータを`First`で取得してから、`Delete`で値を削除する。

```go:models/chat.go
func DeleteChat(id string) (*Chat, error) {
	var chat Chat
	db.First(&chat, id)
	if chat.Id == 0 {
		log.Println("not found chat error")
		return nil, errors.New("not found chat error")
	}
    // DELETE from chat where id = Id;
	err := db.Delete(&chat).Error
	if err != nil {
		log.Println("Delete error")
		return nil, err
	}

	return &chat, nil
}
```

# Controllerの追加
apiのリクエストに合わせて、先程の`model/chat.go`のメソッドをそれぞれ呼び出す。

```go:controllers/chat.go
package controllers

import (
	"encoding/json"
	"go_friendly_chat/controllers/requests"
	"go_friendly_chat/controllers/responses"
	"go_friendly_chat/models"
	"log"

	"github.com/astaxie/beego"
)

type ChatController struct {
	beego.Controller
}

// @Title CreateChatMessage
// @Description create message
// @Param	body		body 	requests.Chat	true		"body for user content"
// @Success 200 {object} responses.Chat
// @Failure 403 : requests.Chat is empty
// @Failure 500 internal server error
// @router / [post]
func (c *ChatController) Post() {
	var chat requests.Chat

	err := json.Unmarshal(c.Ctx.Input.RequestBody, &chat)
	if err != nil {
		log.Println("Chat Post json.Unmarshal error")
		c.Ctx.Output.SetStatus(403)
		c.ServeJSON()
	}

	newChat, err := models.AddChat(models.Chat{
		Id:       0,
		UserName: chat.UserName,
		Message:  chat.Message,
	})
	if err != nil {
		log.Println("AddChat error")
		c.Ctx.Output.SetStatus(500)
		c.ServeJSON()
	}

	res := responses.Chat{
		Id:       int(newChat.Id),
		UserName: newChat.UserName,
		Message:  newChat.Message,
	}
	c.Data["json"] = res

	c.ServeJSON()
}

// @Title GetAll
// @Description get all Chats
// @Success 200 {object} responses.Chats
// @router / [get]
func (c *ChatController) GetAll() {
	chats := models.GetAllChats()
	var res responses.Chats
	for _, chat := range chats {
		res.Chats = append(res.Chats, responses.Chat{
			Id:       int(chat.Id),
			UserName: chat.UserName,
			Message:  chat.Message,
		})

	}
	c.Data["json"] = res
	c.ServeJSON()
}

// @Title Get
// @Description get chat by id
// @Param	id		path 	string	true
// @Success 200 {object} responses.Chat
// @Failure 403 :id is empty
// @Failure 404 :chat is not found
// @router /:id [get]
func (c *ChatController) Get() {
	id := c.GetString(":id")
	if id != "" {
		chat, err := models.GetChat(id)
		if err != nil {
			log.Println("chat is not found")
			c.Ctx.Output.SetStatus(404)
			c.ServeJSON()
		} else {
			res := responses.Chat{
				Id:       int(chat.Id),
				UserName: chat.UserName,
				Message:  chat.Message,
			}
			c.Data["json"] = res
		}
		c.ServeJSON()
	} else {
		log.Println("id is empty error")
		c.Ctx.Output.SetStatus(403)
		c.ServeJSON()
	}
}

// @Title Update
// @Description update the chat
// @Param	id		path 	string	true
// @Param	body		body 	requests.UpdateChat	true
// @Success 200 {object} responses.Chat
// @Failure 403 :id is not int
// @Failure 500 internal server error
// @router /:id [put]
func (c *ChatController) Put() {
	id := c.GetString(":id")
	if id != "" {
		var req requests.UpdateChat
		json.Unmarshal(c.Ctx.Input.RequestBody, &req)
		updateChat, err := models.UpdateChat(id, req)
		if err != nil {
			c.Ctx.Output.SetStatus(500)
			c.ServeJSON()
		}
		res := responses.Chat{
			Id:       int(updateChat.Id),
			UserName: updateChat.UserName,
			Message:  updateChat.Message,
		}
		c.Data["json"] = res
		c.ServeJSON()
	} else {
		log.Println("id is empty error")
		c.Ctx.Output.SetStatus(403)
		c.ServeJSON()
	}
}

// @Title Delete
// @Description delete the chat
// @Param	id		path 	string	true
// @Success 200 {object} responses.Chat
// @Failure 403 id is empty
// @Failure 500 internal server error
// @router /:id [delete]
func (c *ChatController) Delete() {
	id := c.GetString(":id")
	if id != "" {
		deleteChat, err := models.DeleteChat(id)
		if err != nil {
			log.Println("Delete error")
			c.Ctx.Output.SetStatus(500)
			c.ServeJSON()
		}
		res := responses.Chat{
			Id:       int(deleteChat.Id),
			UserName: deleteChat.UserName,
			Message:  deleteChat.Message,
		}
		c.Data["json"] = res
		c.ServeJSON()
	} else {
		log.Println("id is empty error")
		c.Ctx.Output.SetStatus(403)
		c.ServeJSON()
	}
}

```

# リクエスト/レスポンス用のデータを定義する。

リクエスト用

```go:controllers/requests/chat.go
package requests

type Chat struct {
	UserName string `json:"user_name"`
	Message  string `json:"message"`
}

type UpdateChat struct {
	Message string `json:"message"`
}
```

レスポンス用

```go:controllers/responses/chat.go
package responses

type Chat struct {
	Id       int    `json:"id"`
	UserName string `json:"user_name"`
	Message  string `json:"message"`
}

type Chats struct {
	Chats []Chat `json:"chats"`
}

```

# ルーティングの設定
`Controller`へのルーティングを`routers/router.go`に追加する

```go:routers/router.go
package routers

import (
	"go_friendly_chat/controllers"

	"github.com/astaxie/beego"
)

func init() {
	ns := beego.NewNamespace("/v1",
		beego.NSNamespace("/chat",
			beego.NSInclude(
				&controllers.ChatController{},
			),
		),
	)
	beego.AddNamespace(ns)
}
```

# データベースの準備
下記を作成する。
データベース: `chat_db`
テーブル: `chats`

```sql:COLUMNS
+-----------+--------------+------+-----+---------+----------------+
| Field     | Type         | Null | Key | Default | Extra          |
+-----------+--------------+------+-----+---------+----------------+
| id        | int unsigned | NO   | PRI | NULL    | auto_increment |
| user_name | varchar(255) | YES  |     | NULL    |                |
| message   | varchar(255) | YES  |     | NULL    |                |
+-----------+--------------+------+-----+---------+----------------+
```

gormのマイグレーションで自動で作成することも可能
先程の`models/chat.go`で定義した、Chatデータを元にテーブルを作成してくれる。

```go
db.AutoMigrate(&models.Chat{})
```

今回はサーバーを起動する前に、毎回マイグレーションするようにしている。（マイグレーション処理は分離したほうが良い気がしますが…）

```go:main.go
package main

import (
	"fmt"
	"go_friendly_chat/models"
	_ "go_friendly_chat/routers"
	"log"

	"github.com/astaxie/beego"
	"github.com/jinzhu/gorm"
)

func main() {
	db := setupDB()
	db.AutoMigrate(&models.Chat{})

	if beego.BConfig.RunMode == "dev" {
		beego.BConfig.WebConfig.DirectoryIndex = true
		beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
	}
	beego.Run()
}

func setupDB() *gorm.DB {
	dbDriverName := beego.AppConfig.String("db_driver_name")
	dbName := beego.AppConfig.String("db_name")
	dbUserName := beego.AppConfig.String("db_user_name")
	dbUserPassword := beego.AppConfig.String("db_user_password")
	dbHost := beego.AppConfig.String("db_host")

	connectTemplate := "%s:%s@%s/%s"
	connect := fmt.Sprintf(connectTemplate, dbUserName, dbUserPassword, dbHost, dbName)
	db, err := gorm.Open(dbDriverName, connect)

	if err != nil {
		log.Println(err.Error())
	}

	return db
```

# apiの確認
サーバーを起動して

```shell
bee run -downdoc=true -gendoc=true
```

swaggerから確認できる.。
http://127.0.0.1:8080/swagger/

# おわりに
`bee api {アプリ名}`でapiのテンプレートを作成してくれて、起動したらswaggerからすぐに動作確認できるのでサクッとapi開発できました。

今回はDBへの基本的なCRUD操作しかやらなかったですが、より複雑な処理は公式ドキュメントを参照してください。
(https://gorm.io/ja_JP/docs/index.html)