Go の Gorm 使う

# はじめに

`Go`を勉強し始めたということで、`O/Rマッパー`も体験しておきます。

今回は、[Go で API 作る](https://qiita.com/pure-adachi/items/47dca38f5508c42f35b3)の続きとして、`Gorm`を入れて試してみたいと思います。

# 早速作ってみる

https://github.com/pure-adachi/go-sample-api-server

今回もこれをベースに組み込んでいきます。

[Go で API 作る](https://qiita.com/pure-adachi/items/47dca38f5508c42f35b3)の続きなので、API部分は`gin`を使います。
※`gin`の使い方は、上記Qiita記事で紹介しているので割愛します。

# Gorm を取得する

`Docker`で開発しているので、`docker compose`コマンドとなります。
`DB`は`PostgreSQL`を使ってみます。

```sh
$ docker compose run --rm back go get -u gorm.io/gorm
$ docker compose run --rm back go get -u gorm.io/driver/postgres
```

# DB と接続

取得した`gorm`と`postgres`を`import`

```diff:cmd/main.go
 import (
+	"gorm.io/driver/postgres"
+	"gorm.io/gorm"
 )
```

`DB`と接続するメソッドを作成します。

```go:cmd/main.go
func gormConnect() *gorm.DB {
	dsn := os.Getenv("DATABASE_URL")
	db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})

	db.AutoMigrate(&Todo{})

	return db
}
```

`Heroku`にデプロイも試したかったので、`dsn`は、`DATABASE_URL`の環境変数から取るようにしました。

```
DATABASE_URL=host=postgres user=postgres password=password dbname=go-todo-api-db port=5432 sslmode=disable TimeZone=Asia/Tokyo
```

`todos`テーブルは、`AutoMigrate`を使って作られるようにしました。

```go
db.AutoMigrate(&Todo{})
```

`todos`テーブルはこんな感じで定義しています。

```go:cmd/main.go
type Todo struct {
	gorm.Model
	Title string
}
```

# リスト取得（`GET: /api/todos`）を作ってみる

https://qiita.com/pure-adachi/items/47dca38f5508c42f35b3#%E3%83%AA%E3%82%B9%E3%83%88%E5%8F%96%E5%BE%97api%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B

上記のようにルートを用意し、`getTodos`メソッドが呼ばれるようにします。

`Rails`で言う、`Todo.all`のように、`DB`から取得します。

```go:cmd/main.go
func getTodos(c *gin.Context) {
	var todos [] Todo

	db := gormConnect()

	db.Find(&todos)

	c.JSON(http.StatusOK, gin.H { "todos": todos })
}
```

`Find`メソッドを使います。

https://gorm.io/ja_JP/docs/query.html#%E3%81%99%E3%81%B9%E3%81%A6%E3%81%AE%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B


データが空なので、何も帰ってこないと分かりづらいです。
試しに2つデータを登録して、`GET`で`/api/todos`を叩いてみました。

![スクリーンショット 2022-02-23 14.42.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/a2456f81-fb90-0432-68f5-b60f3a94b1e5.png)

`todos`テーブルを作成する際に使っている`Todo`の型には、`Title`カラムしか指定していないのに、`ID`、`CreatedAt`、`UpdatedAt`、`DeletedAt`カラムが追加されています。

これは、`gorm.Model`に定義されているそうです。

https://gorm.io/ja_JP/docs/models.html#gorm-Model

デフォルトで`ID`カラムは主キーで、自動でインクリメントもしてくれました。

ちなみに`ID`カラム以外の独自に定義したカラムを主キー且つ、自動インクリメントしたい場合は、以下のようにすると出来ました。

```go
Id    uint   `gorm:"primaryKey;autoIncrement`
Title string
```

# リスト追加（`POST: /api/todos`）を作ってみる

https://qiita.com/pure-adachi/items/47dca38f5508c42f35b3#todo%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8Bapi%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B

上記のようにルートを用意し、`addTodo`メソッドが呼ばれるようにします。

```go:cmd/main.go
func addTodo(c *gin.Context) {
	type InputTodo struct {
		Title string
	}

	var inputTodo InputTodo
	c.BindJSON(&inputTodo)

	newTodo := Todo { Title: inputTodo.Title }

	db := gormConnect()

	db.Create(&newTodo)

	c.JSON(http.StatusOK, gin.H { "todo": newTodo })
}
```

`Create`メソッドを使います。

https://gorm.io/ja_JP/docs/create.html#%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B

リクエストを投げるだけでは分かりづらいので、追加した`Todo`をレスポンスとして返すようにしてみました。

`POST`で`/api/todos`を叩いてみます。

![スクリーンショット 2022-02-23 15.04.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/b5b8f3e7-c55a-9cd8-710a-5a06ff873c12.png)

`ID`も自動インクリメントされているし、渡した`Title`が入ったデータが作られていそうです。

# リスト更新(`PATCH: /api/todos/:ID`)を作ってみる

https://qiita.com/pure-adachi/items/47dca38f5508c42f35b3#todo%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8Bapi%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B

上記のようにルートを用意し、`updateTodo`メソッドが呼ばれるようにします。
※パラメタの受け取り方を`Id`から`ID`に変更しました。（`r.PATCH("/api/todos/:ID", updateTodo)`）

```go:cmd/main.go
func updateTodo(c *gin.Context) {
	type InputTodo struct {
		Title string
	}

	Id := c.Param("ID")
	id, _ := strconv.Atoi(Id)

	db := gormConnect()

	var todo Todo

	db.First(&todo, id)

	var inputTodo InputTodo
	c.BindJSON(&inputTodo)

	db.Model(&todo).Update("Title", inputTodo.Title)

	c.JSON(http.StatusOK, gin.H { "todo": todo })
}
```

`Update`メソッドを使います。
`First`や`Find`で取得した後、カラム毎に値を代入して、`Save`もできるそうです。

https://gorm.io/ja_JP/docs/update.html#%E5%8D%98%E4%B8%80%E3%81%AE%E3%82%AB%E3%83%A9%E3%83%A0%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B

`PATCH`で`/api/todos/1`を叩いてみます。

![スクリーンショット 2022-02-23 15.09.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/f994940f-6a5c-bd2b-5bb5-6ba71d2e3f73.png)

渡した`Title`で`ID`が`1`のレコードが更新されていそうです。

# リスト削除(`DELETE: /api/todos/:ID`)を作ってみる

https://qiita.com/pure-adachi/items/47dca38f5508c42f35b3#todo%E3%82%92%E5%89%8A%E9%99%A4%E3%81%99%E3%82%8Bapi%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B

上記のようにルートを用意し、`deleteTodo`メソッドが呼ばれるようにします。
※パラメタの受け取り方を`Id`から`ID`に変更しました。（`r.DELETE("/api/todos/:ID", deleteTodo`）

```go:cmd/main.go
func deleteTodo(c *gin.Context) {
	Id := c.Param("ID")
	id, _ := strconv.Atoi(Id)

	db := gormConnect()

	var todo Todo

	db.Delete(&todo, id)
}
```

`Delete`メソッドを使います。

https://gorm.io/ja_JP/docs/delete.html#%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E5%89%8A%E9%99%A4

`DELETE`で`/api/todos/1`を叩いてみます。
削除されたことが分かりづらいので、`GET`で`/api/todos`も叩いてみます。

![スクリーンショット 2022-02-23 15.25.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304414/8beb12e2-1e77-865d-2e31-9375fb182599.png)

`ID`が`1`のレコードが削除されていそうです。

# おまけ（`Cross-Domain`問題）

今回紹介した方法で、リクエストを送っているツールは、`Insomnia`を使っています。
仮に、フロントエンドアプリからリクエストを送るとしたら、`Cross-Domain`問題が発生すると思います。

その場合は、`cors`の設定を行います。

```sh
$ docker compose run --rm back go get -u github.com/gin-contrib/cors
```

```diff
 import (
+	"github.com/gin-contrib/cors"
 )


 r := gin.Default()

+r.Use(cors.New(cors.Config{
+	AllowOrigins: []string{os.Getenv("CORS_ORIGIN")},
+	AllowMethods: []string{"PUT", "PATCH", "DELETE"},
+}))
```

`GET`と`POST`までは問題なかったですが更新の`PATCH`あたりからこの問題に直面しました。
適宜、使用するHTTPリクエストメソッドを追加してください。

# おまけ（`Heroku`の`Go`のバージョン認識）

今回使用している`Go`のバージョンは、`1.17.1`です。

筆者は、Dockerをそのまま`Heroku`にデプロイしていますが、ライブラリインストールでエラーが発生しました。

https://github.com/pure-adachi/go-todo-api/blob/main/heroku.yml

```
../codon/tmp/cache/go-path/pkg/mod/gorm.io/gorm@v1.23.1/logger/sql.go:105:45: rv.IsZero undefined (type reflect.Value has no field or method IsZero)
note: module requires Go 1.14
```

`Go`のバージョンを`1.14`以降にしたら良さそうですが、`1.17.1`の想定です。

`go.mod`にも`1.17`と記載されています。

https://github.com/pure-adachi/go-todo-api/blob/main/go.mod#L3

`Heroku`の`Build Log`を遡ってみると、`1.12.17`を使っていることが判明

```
-----> 
 !!    The go.mod file for this project does not specify a Go version
 !!    
 !!    Defaulting to go1.12.17
 !!    
 !!    For more details see: https://devcenter.heroku.com/articles/go-apps-with-modules#build-configuration
 !!    
-----> Using go1.12.17
```

`Heroku`の環境変数に、`GOVERSION=1.17.1`を追加すると解決しました。

https://github.com/heroku/heroku-buildpack-go/issues/301#issuecomment-632663726

# おわりに

今回のソースはこちらの[GitHub](https://github.com/pure-adachi/go-todo-api)に上げてます。

# 参考文献

- [GORMガイド](https://gorm.io/ja_JP/docs/index.html)
- [Go言語でORM触ってみた - Qiita](https://qiita.com/chan-p/items/cf3e007b82cc7fce2d81)