Go + Echo + Vue.js + GORMで、DB接続する最低限のSPAを実装してみる

Go言語でのSPA開発について、一番基本的なところの実装を押さえておきたかったところに、素晴らしく簡潔にまとまった以下の記事を参考にさせていただきました。

Go + Echo + Vue.js SPAへの第一歩
https://qiita.com/K-juju/items/f6d0eedb9aee5714cb73

少しだけ実装を追加してみたので、そのご紹介です。

なおこのサンプルアプリケーションの基本的な機能については、この記事では説明しませんので、上記の@K-jujuさんの記事を参照ください。

# 追加でやってみた点
1. データをDBに保持し、ORMでデータ作成や取得を行うようにしました。
    - DBMSには、とても気軽に使えるSQLiteを使いました。
    - ORMには、シンプルながら高機能なGORMを使いました。
        - GORMガイド  
        http://gorm.io/ja_JP/docs/index.html
2. 依存モジュール管理にModulesを使うようにしました。
    - （参考）Go言語の依存モジュール管理ツール Modules の使い方  
    https://blog.mmmcorp.co.jp/blog/2019/10/10/go-mod/

# リポジトリ
https://github.com/chihiroyn/go-echo-vue-gorm-single-page-app

# 実装の詳細
## Modulesを導入する
```
$ go mod init
```
で、go.modファイルを作成します。
RubyのBundlerでいうGemfile、Pythonでいうrequirements.txt（PipenvならPipfile）にあたります。

```
$ go build
```
で、依存モジュールを自動でインストールしてくれます。

## GORMを導入する
### Taskモデルを定義する
```golang:tasks.go
package models

import "github.com/jinzhu/gorm"

// Task is a struct containing Task data
type Task struct {
//	ID   int    `json:"id"`
	gorm.Model
	Name string `json:"name"`
}
```

gorm.Modelを使用してみました。

DBマイグレーション(後述)を行なった後、自動生成されるTasksテーブルのレイアウトを、SQLiteのコンソールで確認してみます。

```
$ sqlite3 todo.db

sqlite> .schema tasks
CREATE TABLE IF NOT EXISTS "tasks" ("id" integer primary key autoincrement,"created_at" datetime,"updated_at" datetime,"deleted_at" datetime,"name" varchar(255) );
CREATE INDEX idx_tasks_deleted_at ON "tasks"(deleted_at) ;

sqlite> select * from tasks;
1|2020-01-05 10:34:53.209549+09:00|2020-01-05 10:34:53.209549+09:00||洗濯
2|2020-01-05 10:34:53.211785+09:00|2020-01-05 10:34:53.211785+09:00||掃除
```

レコードを一意に識別するidカラムや、レコードの追加・更新・削除のタイムスタンプを保持するカラムを、GORMが自動的に追加してくれています。
deleted_atに値を保持しているレコードが、GORMでは論理削除したものとみなしてくれます。

ただ、TaskデータをJSON出力する際、JSONにIDが含まれないようになっています。
この副作用（？）が受け入れられない場合は、gorm.Modelを使用することにこだわらない実装もアリかも知れないですね。

### アプリ起動時の初期処理を実装する

GORMのクイックスタートを参考にしました。
http://gorm.io/ja_JP/docs/index.html

```golang:database.go
package models

import (
	"github.com/jinzhu/gorm"
	// to connect to SQLite database
	_ "github.com/jinzhu/gorm/dialects/sqlite"
)

var db *gorm.DB

// InitDB do initialization for database (open, migrate, and set up seeds)
func InitDB() {
	var err error
	db, err = gorm.Open("sqlite3", "todo.db")
	if err != nil {
		panic("failed to connect database.")
	}
	//	defer db.Close()

	// スキーマのマイグレーション
	db.AutoMigrate(&Task{})

	// Delete all records
	db.Unscoped().Delete(&Task{})

	// Create initial records
	db.Create(&Task{
		Name: "洗濯",
	})
	db.Create(&Task{
		Name: "掃除",
	})
}
```
以下を行なっています。

1. スキーマのマイグレーション
2. 初期データのセットアップ

スキーマのマイグレーションは、アプリのデプロイ時ではなく、起動時に行います。
マイグレーションを実装に含めてしまうというのが、モデルベース的なアプローチかも知れません。

```golang:database.go
	// スキーマのマイグレーション
	db.AutoMigrate(&Task{})
```

個人的には、GORMを触って最初に少しばかり衝撃を受けた点でした。
昨今のORMでは一般的なんでしょうかねえ？

注意点としては、db.AutoMigrateで行われるのは、

- 新規テーブルの追加
- 新規カラムの追加

のみで、カラムのテーブルや削除等は自動で行われないという点です。
（↓　GORMのチュートリアルより）
http://gorm.io/ja_JP/docs/migration.html#%E8%87%AA%E5%8B%95%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3

その後、初期データのセットアップを行います。
Railsでいうrake db:seedです。

まずは、既存データはいったんすべて物理削除します。

```golang:database.go
	// Delete all records
	db.Unscoped().Delete(&Task{})
```

ここでは過去に論理削除された（= deleted_atに値を保持している）レコードを含め、Tasksテーブルの全レコードを完全に削除しています。

もし以下のようにすると、物理削除ではなく論理削除となります。
過去のレコードはTasksテーブルに残り、deleted_atに値がセットされることで後続処理で検索されなくなります。
ただこれだと、アプリを起動するたびにゴミデータが増えてしまうことになるので、今回は物理削除としました。

```
	// Delete all records
	db.Delete(&Task{})
```

その後に、初期データのINSERTを行います。

そして、main関数でこの初期処理を行うようにします。

```golang:todo.go
func main() {
	models.InitDB()

	e := echo.New()
...
```

### DBからタスクデータを取得するようにする

フロントページの「タスクを表示する。」ボタンを押した時に、DBからタスクデータを取得して表示するようにします。

```golang:tasks.go
// GetTasks returns all tasks in database as TaskCollection
func GetTasks() (tc TaskCollection) {
	tasks := []Task{}
	db.Find(&tasks)

	tc = TaskCollection{
		Tasks: tasks,
	}
...
```

シンプルな実装です。

1. Tasksテーブルの全レコードを、GORMのdb.FindでTask構造体にセットする。
2. TaskCollection構造体のTasks（= Task構造体のスライス）に、1.をセットする。

これで、データをDBMSに保持しつつ、従来のサンプルアプリケーションと同じ挙動になります！

# ちょっと心残りなこと
アプリからDBへの接続と切断は検索処理の都度行うのではなく、アプリの開始時の接続を都度使い回す実装ですが、

## 1. アプリの終了時にdb.Close()をしていない

GORMのチュートリアルにあるように、models.InitDBで

```golang:database.go
...
func InitDB() {
	var err error
	db, err = gorm.Open("sqlite3", "todo.db")
	if err != nil {
		panic("failed to connect database.")
	}
	defer db.Close()
```

とすると、肝心のフロントページ参照時点でDB接続が切れてしまうため、不可でした。

「Echoのインスタンス終了時にDB接続を切断する」という実装ができるのではと思うのですが、まだやり方が分かっていません。

## 2. gorm.DBインスタンスを使い回すために、InitDBをmodelsパッケージに含めている

modelsというパッケージに、データの永続化についての実装や、SQLiteというDBMS固有の実装を含めているというのは、DDDやクリーンアーキテクチャ的にはちょっとイケテナイかもです。

もう少し勉強したいです。

# その他参考にさせていただいた記事

EchoからGORMでDB接続、について、以下の記事も参考にさせていただきました。

Go言語でEchoを用いて認証付きWebアプリの作成
https://qiita.com/x-color/items/24ff2491751f55e866cf

ありがとうございました！