GolangにORM導入(go-pg migration編)

#はじめに
GolangのORMであるgo-pgの導入です。本記事ではgo-pgのチュートリアルに沿ってマイグレーションを実行します。
※go-pgは現在メンテナンスモードのため、bunへの移行が推奨されています。今から学ばれている方はbunを利用することをお勧めいたします。私は諸事情があり、あえてgo-pgで開発をしています。

#環境情報
PC：Mac(CPUはintel製)
Go：1.17.6 ←Goのver大事。versionは1.16以降をインストールしてください。
開発エディタ：Visual Studio Code

#ディレクトリ構成
```shell:ディレクトリ構成
~/go/src/go-pg-migrate $ tree
.
├── docker-compose.yml
├── examples
│   ├── 1_initial.go
│   ├── 2_add_id.go
│   ├── 3_seed_data.go
│   └── main.go
├── go.mod
└── go.sum

1 directory, 7 files
```

#Hands-On スタート
$GOPATH/src配下にgo-pg-migrateディレクトリを作成。

##ライブラリのインストール
```shell:ライブラリのインストール
~/go/src/go-pg-migrate $ go mod init
go: creating new go.mod: module go-pg-migrate

~/go/src/go-pg-migrate $ go get github.com/go-pg/migrations/v8
go: downloading github.com/go-pg/migrations/v8 v8.1.0
go: downloading github.com/go-pg/migrations v6.7.3+incompatible
go get: added github.com/go-pg/migrations/v8 v8.1.0
go get: added github.com/go-pg/pg/v10 v10.4.0
go get: added github.com/go-pg/zerochecker v0.2.0
go get: added github.com/golang/protobuf v1.4.3
go get: added github.com/jinzhu/inflection v1.0.0
go get: added github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc
go get: added github.com/vmihailenco/bufpool v0.1.11
go get: added github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1
go get: added github.com/vmihailenco/tagparser v0.1.2
go get: added go.opentelemetry.io/otel v0.13.0
go get: added golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
go get: added golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0
go get: added golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7
go get: added google.golang.org/appengine v1.6.7
go get: added google.golang.org/protobuf v1.25.0
go get: added mellium.im/sasl v0.2.1
```

##DBの準備
```docker:docker-compose.ymlの作成
version: '3.1'

services:
  postgres:
    container_name: postgres
    image: postgres:13
    ports:
      - 5432:5432
    volumes:
      - ./.data/postgres:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
```
```shell:DBスキーマ(pg_migrations_example)の作成
~/go/src/go-pg-migrate $ psql -U postgres -h localhost
Password for user postgres:                  ←Passwordはpostgresです。
psql (14.1, server 13.4 (Debian 13.4-1.pgdg100+1))
Type "help" for help.
postgres=# CREATE DATABASE pg_migrations_example;
CREATE DATABASE
```

##main.goの作成
```go:examples/main.goの作成
package main

import (
	"flag"
	"fmt"
	"os"

	"github.com/go-pg/migrations/v8"
	"github.com/go-pg/pg/v10"
)

const usageText = `This program runs command on the db. Supported commands are:
  - init - creates version info table in the database
  - up - runs all available migrations.
  - up [target] - runs available migrations up to the target one.
  - down - reverts last migration.
  - reset - reverts all migrations.
  - version - prints current db version.
  - set_version [version] - sets db version without running migrations.
Usage:
  go run *.go <command> [args]
`

func main() {
	flag.Usage = usage
	flag.Parse()

	db := pg.Connect(&pg.Options{
		User:     "postgres",
		Password:  "postgres", // Passwordは追加必要。公式サンプルには記載なし。
		Database: "pg_migrations_example",
	})

	oldVersion, newVersion, err := migrations.Run(db, flag.Args()...)
	if err != nil {
		exitf(err.Error())
	}
	if newVersion != oldVersion {
		fmt.Printf("migrated from version %d to %d\n", oldVersion, newVersion)
	} else {
		fmt.Printf("version is %d\n", oldVersion)
	}
}

func usage() {
	fmt.Print(usageText)
	flag.PrintDefaults()
	os.Exit(2)
}

func errorf(s string, args ...interface{}) {
	fmt.Fprintf(os.Stderr, s+"\n", args...)
}

func exitf(s string, args ...interface{}) {
	errorf(s, args...)
	os.Exit(1)
}
```

##マイグレーションファイルの初期化
```go:マイグレーションファイルの初期化
~/go/src/go-pg-migrate/examples $ go run main.go init
version is 0
```
→go_migrationsが作成されたことを確認。
![スクリーンショット 2022-02-18 15.01.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/b724a51c-4ed1-cb40-bb8c-2fc85f7828e8.png)

ちなみに、initを実行せずmain.goを実行しようとする下記のようなエラーが出ます。

```go:main.go実行
~/go/src/go-pg-migrate/examples $ go run main.go 
table "gopg_migrations" does not exist; did you run init?
exit status 1
```

##TBL(my_table)作成
では、実際にTBLを作成しましょう。今回はmy_tableを作成します。

```go:1_initial.goの作成
package main

import (
	"fmt"

	"github.com/go-pg/migrations/v8"
)

func init() {

	migrations.MustRegisterTx(func(db migrations.DB) error {
		fmt.Println("creating table my_table...")
		_, err := db.Exec(`CREATE TABLE my_table()`) //my_tableを作成する。
		return err
	}, func(db migrations.DB) error {
		fmt.Println("dropping table my_table...")
		_, err := db.Exec(`DROP TABLE my_table`)
		return err
	})
}

```

```go:migration実行
~/go/src/go-pg-migrate/examples $ go run *.go                                                    
creating table my_table...
migrated from version 0 to 1
```
→my_tableが作成されていることを確認。
![スクリーンショット 2022-02-18 15.08.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/8b657bb3-3fb4-5587-a84b-d52f2560dd78.png)

##TBLのカラムを修正する。
```go:2_add_id.goの作成
package main

import (
	"fmt"

	"github.com/go-pg/migrations/v8"
)

func init() {
	migrations.MustRegisterTx(func(db migrations.DB) error {
		fmt.Println("adding id column...")
		_, err := db.Exec(`ALTER TABLE my_table ADD id serial`)
		return err
	}, func(db migrations.DB) error {
		fmt.Println("dropping id column...")
		_, err := db.Exec(`ALTER TABLE my_table DROP id`)
		return err
	})
}
```

```go:migration実行
~/go/src/go-pg-migrate/examples $ go run *.go 
adding id column...
migrated from version 1 to 2
```
![スクリーンショット 2022-02-18 15.12.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/ba7fe49e-aca6-6008-2d95-2936bd58f1bb.png)
→my_tableにidカラムが追加されていることを確認。

##レコード追加
```go:3_seed_data.goの作成
package main

import (
	"fmt"

	"github.com/go-pg/migrations/v8"
)

func init() {
	migrations.MustRegisterTx(func(db migrations.DB) error {
		fmt.Println("seeding my_table...")
		_, err := db.Exec(`INSERT INTO my_table VALUES (1)`)
		return err
	}, func(db migrations.DB) error {
		fmt.Println("truncating my_table...")
		_, err := db.Exec(`TRUNCATE my_table`)
		return err
	})
}
```
```go:migration実行
~/go/src/go-pg-migrate/examples $ go run *.go 
seeding my_table...
migrated from version 2 to 3
```
→レコードに値1が格納されていることを確認。
![スクリーンショット 2022-02-18 15.16.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/a501fdce-584e-b906-180c-b157e7e95e6a.png)

##余談
maigration初期化時に作成したgopg_migrationsの状態は下記の通りです。version管理をしっかりやってくれているのですね😁
![スクリーンショット 2022-02-18 15.17.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/8d7047a0-2329-37f8-9872-20025c235788.png)

#追記
他のサンプルでも実行。

```shell:ディレクトリ構成
~/go/src/go-pg $ tree
.
├── docker-compose.yml
├── go.mod
├── go.sum
└── main.go

0 directories, 4 files
```

```go:main.go
package main

import (
	"fmt"

	"github.com/go-pg/pg/v10"
	"github.com/go-pg/pg/v10/orm"
)

type User struct {
    Id     int64
    Name   string
    Emails []string
}

func (u User) String() string {
    return fmt.Sprintf("User<%d %s %v>", u.Id, u.Name, u.Emails)
}

type Story struct {
    Id       int64
    Title    string
    AuthorId int64
    Author   *User
}

func (s Story) String() string {
    return fmt.Sprintf("Story<%d %s %s>", s.Id, s.Title, s.Author)
}

func ExampleDB_Model() {
    db := pg.Connect(&pg.Options{
        User: "postgres",
		Password: "postgres",
		Database: "sample",
    })
    defer db.Close()

    err := createSchema(db)
    if err != nil {
        panic(err)
    }

    user1 := &User{
        Name:   "admin",
        Emails: []string{"admin1@admin", "admin2@admin"},
    }
    _, err = db.Model(user1).Insert()
    if err != nil {
        panic(err)
    }

    _, err = db.Model(&User{
        Name:   "root",
        Emails: []string{"root1@root", "root2@root"},
    }).Insert()
    if err != nil {
        panic(err)
    }

    story1 := &Story{
        Title:    "Cool story",
        AuthorId: user1.Id,
    }
    _, err = db.Model(story1).Insert()
    if err != nil {
        panic(err)
    }

    // Select user by primary key.
    user := &User{Id: user1.Id}
    err = db.Model(user).WherePK().Select()
    if err != nil {
        panic(err)
    }

    // Select all users.
    var users []User
    err = db.Model(&users).Select()
    if err != nil {
        panic(err)
    }

    // Select story and associated author in one query.
    story := new(Story)
    err = db.Model(story).
        Relation("Author").
        Where("story.id = ?", story1.Id).
        Select()
    if err != nil {
        panic(err)
    }

    fmt.Println(user)
    fmt.Println(users)
    fmt.Println(story)
    // Output: User<1 admin [admin1@admin admin2@admin]>
    // [User<1 admin [admin1@admin admin2@admin]> User<2 root [root1@root root2@root]>]
    // Story<1 Cool story User<1 admin [admin1@admin admin2@admin]>>
}

// createSchema creates database schema for User and Story models.
func createSchema(db *pg.DB) error {
    models := []interface{}{
        (*User)(nil),
        (*Story)(nil),
    }

    for _, model := range models {
        err := db.Model(model).CreateTable(&orm.CreateTableOptions{
            Temp: false, //一時作成の場合はTrue
        })
        if err != nil {
            return err
        }
    }
    return nil
}

func main(){
	ExampleDB_Model()
}
```
usersテーブルとstoriesテーブルを追加。
![スクリーンショット 2022-02-18 19.33.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/971bdf62-71d0-cad5-0207-9c0c14421cd5.png)
![スクリーンショット 2022-02-18 19.33.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/8f5a2392-3d02-e748-8c79-943ac095201b.png)



#参考
https://github.com/go-pg/migrations/tree/v6.7.3/example

https://github.com/go-pg/pg

https://github.com/uptrace/bun