BeegoによるWebアプリケーション開発① on docker

#はじめに

Webアプリを作りたくなりました．
せっかくなら初挑戦のGo言語でがんばります！！
今回は<b>掲示板もどき</b>を目標にしましょう．
具体的な説明は少なめです．すいません( 一一)

#開発環境

ubuntu 16.04
docker version : 19.03.1

#docker関連

はじめに，開発環境の構築を行います．
`Go`とWEBフレームワークである`Beego`と`MySQL`を導入していきます．
初期フォルダ構成は↓

```:フォルダ構成
chat/
  |- docker-compose.yml
  |- Go/
     |- Dockerfile
  |- MySQL/
     |- Dockerfile
```

docker-composeの設定ファイルは↓

```chat/docker-compose.yml
version: '2'
services:
  db:
    build:
      context: ./MySQL
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: test_db
      MYSQL_USER: test
      MYSQL_PASSWORD: password
    container_name: go_db
    ports:
      - "3306:3306"
    tty: true
    volumes:
      - ./MySQL/mysql_init:/docker-entrypoint-initdb.d
      - ./MySQL/mysql_data:/var/lib/mysql
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci --skip-character-set-client-handshake
  app:
    build:
      context: ./Go
    volumes:
      - "./Go:/go/src/app"
    container_name: go_app
    ports:
      - "8080:8080"
    tty: true
    depends_on:
      - db
```

私の環境では，version:"2"ですが，"3"の場合は変更すれば動くのかな？

```:Go/Dockerfile
FROM golang:1.12.6

ENV GOPATH $GOPATH:/go
ENV PATH $PATH:$GOPATH/bin
WORKDIR /go/src/app

RUN apt-get update
RUN go get "github.com/go-sql-driver/mysql"
RUN go get "github.com/beego/bee"
RUN go get "github.com/astaxie/beego"
```
```:MySQL/Dockerfile
FROM mysql:5.7
```

これで，初期ファイルは整いましたので，実際に環境を立ち上げていきます．

```:./chat
docker-compose up -d --build
```

`docker ps`で確認してみましょう．
無事2つのコンテナ（go_app・go_db）が立ち上がったでしょうか？

#アプリケーション作成
##ファイル自動生成
まずは，go_appコンテナに移動し，ファイルを自動作成していきましょう．

```
docker exec -it go_app bash
/go/src/app#
```

Beegoは，`scaffold`で一気にテンプレートファイルを作成することができます．
（ginにもあるのですが，つまづいて諦めました．）
これにより，`ruby on rails`のようなコード生成が可能で，便利です！

```
bee generate scaffold comment -fields="content:string" -driver=mysql -conn="test:password@tcp(db:3306)/test_db"
```

出てくる質問に，すべて`y`で答えます．
最終的に，`Scaffold successfully generated!`と出力されれば完了です．
ローカルでもフォルダの更新ができるように，`chmod 777 -R .`すると楽です．
ちなみに，`bee help`や`bee help generate`でコマンドの確認ができます．

##周辺ファイル作成
次に，周辺のファイルを作っていきます．
適宜フォルダも作成します．
( `bee api sample -driver=mysql -conn=test:password@tcp"(db:3306)"/test_db）`である程度サンプル作れます．)

```conf/app.conf
appname = app
httpport = 8080
runmode = dev
autorender = true
copyrequestbody = true
EnableDocs = true
driver = mysql
sqlconn = test:password@tcp(db:3306)/test_db
```

```main.go
package main

import (
	_ "app/routers"
	"time"
	"fmt"
	"github.com/astaxie/beego/orm"
	"github.com/astaxie/beego"
	_ "github.com/go-sql-driver/mysql"
)

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

func init() {
	orm.RegisterDriver(beego.AppConfig.String("driver"), orm.DRMySQL)
	orm.RegisterDataBase("default", beego.AppConfig.String("driver"), beego.AppConfig.String("sqlconn")+"?charset=utf8")
	err := orm.RunSyncdb("default", false, false)
	if err != nil {
		fmt.Println(err)
	}
    // あとで使います
	beego.AddFuncMap("dateformatJst", func(in time.Time) string {
		in = in.Add(time.Duration(9) * time.Hour)
		return in.Format("2006-01-02 15:04:05")
	})
}
```

```routers/router.go
package routers

import (
	"app/controllers"

	"github.com/astaxie/beego"
)

func init() {
	// beego.Router("/", &controllers.MainController{}) // あとで使います
	ns := beego.NewNamespace("/test_db",
		beego.NSNamespace("/comment",
			beego.NSInclude(
				&controllers.CommentController{},
			),
		),
	)
	beego.AddNamespace(ns)
}
```


```models/comment.go
package models

import (
	"errors"
	"fmt"
	"reflect"
	"strings"
	"time"

	"github.com/astaxie/beego/orm"
)

type Comment struct {
	Id      int64  `orm:"auto"`
	Content string `orm:"size(128)"`
	Created time.Time `orm:"auto_now_add;type(datetime)"`   // 追加
	Updated time.Time `orm:"auto_now;type(datetime)"`       // 追加
}

// func init() { ・・・

// あとで使うので，末尾に追加
func GetAllComments() ([]Comment, error) {
    o := orm.NewOrm()
		var l []Comment
		_, err := o.QueryTable(new(Comment)).All(&l)
		return l, err
}
```
自動生成されるGetAllComment()をそのまま使いたかったのですが，引数がよく分からず諦めました．．．
コメント頂きたい！


##サーバー起動
では，この状態でサーバーを起動してみます．

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

後半のオプションを付けることにより，swaggerがダウンロードされ，設定ファイルが更新されます（？）．
ブラウザで，`http://localhost:8080/swagger/`にアクセスしてみます．

![swagger.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343913/e8d8e2ff-00a8-5827-95a1-77ace97afa02.png)


```:フォルダ構成
Go
|-- Dockerfile
|-- app
|-- conf
|   `-- app.conf
|-- controllers
|   `-- comment.go
|-- database
|   `-- migrations
|       `-- 20190810_063851_comment.go
|-- lastupdate.tmp
|-- main.go
|-- models
|   `-- comment.go
|-- routers
|   |-- commentsRouter_controllers.go
|   `-- router.go
|-- static                            //今回は利用しないが，この階層に設置しておくと便利
|   |-- css
|   |-- img
|   `-- js
|-- swagger
`-- views
    `-- comment
        |-- create.tpl
        |-- edit.tpl
        |-- index.tpl
        `-- show.tpl

```
一度目だと，テーブルが作成されないみたいなので，いったん`ctrl C`してサーバーを再起動します．（オプションはなくてもOK，たぶん）

では，初期データを投入してみましょう．
swaggerのcomment内のPOSTを選択し，`try it out`からexecuteです．
`localhost:8080/test_db/comment`にアクセスして，データが保存されたことを確認してみましょう．

##Chat Appの作成

最後に，アプリのメインファイルを書いていきます．


```controllers/comment.go
package controllers

import (
	"app/models"
	"encoding/json"
	"errors"
	"strconv"
	"strings"

	"github.com/astaxie/beego"
)

//  CommentController operations for Comment
type CommentController struct {
	beego.Controller
}

// URLMapping ...
func (c *CommentController) URLMapping() {
	c.Mapping("Post", c.Post)
	c.Mapping("GetOne", c.GetOne)
	c.Mapping("GetAll", c.GetAll)
	c.Mapping("Put", c.Put)
	c.Mapping("Delete", c.Delete)
}

// Post ...
// @Title Post
// @Description create Comment
// @Param	body		body 	models.Comment	true		"body for Comment content"
// @Success 201 {int} models.Comment
// @Failure 403 body is empty
// @router / [post]
func (c *CommentController) Post() {
	var v models.Comment
	if c.GetString("content")==""{
		json.Unmarshal(c.Ctx.Input.RequestBody, &v)
	}else{
		v = models.Comment{
			Content: c.GetString("content"),
		}
	}
	if _, err := models.AddComment(&v); err == nil {
		c.Ctx.Output.SetStatus(201)
		c.Data["json"] = v
		if c.GetString("content")!=""{
			c.Redirect("/", 302)
		}
	} else {
		c.Data["json"] = err.Error()
	}
	c.ServeJSON()
}

// GetOne ...
// ・・・

```

追加の部分ですが，Swaggerからのリクエストってjsonらしく，また，formからのsubmitはまた別みたいです．
ajaxを使ってformからjsonを送れるらしいです．（今回はパスします）
というわけで，APIかformかで条件分岐を立てています．（これだと，フォームに何も入力しないで送信するとバグります．）

```routers/router.go
package routers

import (
	"app/controllers"

	"github.com/astaxie/beego"
)

func init() {
	beego.Router("/", &controllers.MainController{})           // 追加
	ns := beego.NewNamespace("/test_db",
		beego.NSNamespace("/comment",
			beego.NSInclude(
				&controllers.CommentController{},
			),
		),
	)
	beego.AddNamespace(ns)
}
```
```controllers/main.go
package controllers

import (
	"github.com/astaxie/beego"
	"app/models"
)

type MainController struct {
	beego.Controller
}

func (c *MainController) Get() {
	c.Data["Website"] = "Chat App"
	c.Data["Email"] = "chat-app@gmail.com"
	data, _ := models.GetAllComments()
	c.Data["Comment"] = data
	c.TplName = "comment/index.tpl"
}
```

いつもBootstrapを使っているのですが，華麗なネイティブアプリを目指したく，Onsen UIを使ってみました．
が，全然使いこなせてませんね（笑）

```views/comment/index.tpl
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <title>chat app</title>

  <link rel="stylesheet" href="https://unpkg.com/onsenui/css/onsenui.css">
  <link rel="stylesheet" href="https://unpkg.com/onsenui/css/onsen-css-components.min.css">
  <script src="https://unpkg.com/onsenui/js/onsenui.min.js"></script>

</head>

<body>
  <ons-page>
    <ons-toolbar>
      <div class="center">
        {{.Website}}
      </div>
    </ons-toolbar>
    <form id="comment" action="/test_db/comment/" method="post">
      <div style="text-align: center; margin-top: 30px;">
        <textarea class="textarea" name="content" type="text" rows="4" placeholder="please post!"></textarea>
        <p style="margin-top: 30px;">
          <button type="submit" class="button">send</button>
        </p>
      </div>
    </form>
    {{range .Comment}}
    <div>
      <div class="card">
        <h3 class="card__title">{{.Content}}</h3>
        <div class="card__content">{{.Created|dateformatJst}}</div>
      </div>
    </div>
    {{end}}
  </ons-page>
</body>
</html>
```

#完成！！
Go langやBeegoを理解するのにやっとで，10日くらいかかりました．．．
こんなでも，かなり嬉しいですね(^^♪

<img width="320" alt="20190810_104134000_iOS.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343913/c900d7eb-051f-9e60-6442-15dddf61188f.png">

他に実装したいことは，ユーザーとのリレーション，編集・削除ですかね．
まあ，なんとか難所は乗り越えられたかなと思うので，今後も頑張っていきます！

##参照サイト
[Go製フレームワーク（BEEGO）の使い方](https://qiita.com/ken0renaissance/items/89a64ea5ae955892d9f2)
[Beego入門（モデル定義・データベース接続・マイグレーション）](https://qiita.com/Kazuki0902Fw/items/04dc7f5e3661ea790179)
[go言語での開発始めてみる〜beego で掲示板っぽいもの作ってみる編〜](https://seroku.jp/wp/go%E8%A8%80%E8%AA%9E%E3%81%A7%E3%81%AE%E9%96%8B%E7%99%BA%E5%A7%8B%E3%82%81%E3%81%A6%E3%81%BF%E3%82%8B%E3%80%9Cbeego-%E3%81%A7%E6%8E%B2%E7%A4%BA%E6%9D%BF%E3%81%A3%E3%81%BD%E3%81%84%E3%82%82%E3%81%AE/)
ありがとうございました(_ _)