Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 簡易的にBunとマイグレーションを導入 #96

Open
wants to merge 31 commits into
base: feat/project-user
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e8d3432
chore: 既存のスキーマを削除
yukikamome316 Nov 8, 2023
9dfaed6
feat: Bun型のmodelを定義
yukikamome316 Nov 8, 2023
b28fb97
feat: cmd/serverにmain.goを移動
yukikamome316 Nov 8, 2023
a5bc00d
fix: 実行対象を訂正
yukikamome316 Nov 8, 2023
4a87659
feat: Bunの導入
yukikamome316 Nov 8, 2023
0a3c034
chore: 既存のmigrationとmainを削除
yukikamome316 Nov 8, 2023
fdbfdfc
feat: migrationsを追加
yukikamome316 Nov 8, 2023
0ed4e53
feat: cmd/migrateを実装
yukikamome316 Nov 8, 2023
f0f7b32
feat: migrate.shを追加
yukikamome316 Nov 8, 2023
9be18c4
fix: ビルドチェックの対象を修正
yukikamome316 Nov 8, 2023
7f7966e
refactor: cmd/serverの処理をhandlerに切り離した
yukikamome316 Nov 9, 2023
4906994
refactor: http.StatusInternalServerErrorを使うようにした
yukikamome316 Nov 9, 2023
345c129
fix: 配列を初期化
yukikamome316 Nov 9, 2023
a461cac
Merge branch 'feat/project-user-migration-temp' of github.com:saitama…
yukikamome316 Nov 9, 2023
4476a0c
fix: デバッグ用のfmt.Printfを削除
yukikamome316 Nov 9, 2023
38bdb86
fix: Collationなどの不要な設定を削除
yukikamome316 Nov 9, 2023
c0f4a45
Merge branch 'feat/project-user' of github.com:saitamau-maximum/maxit…
yukikamome316 Dec 14, 2023
b1c5fe0
fix: main.goの削除
yukikamome316 Dec 14, 2023
a74b0df
feat: DBのマイグレーションに関する項目を追加
yukikamome316 Dec 14, 2023
d0c0a95
fix: 他の項目に合わせて句読点を消した
yukikamome316 Dec 14, 2023
3ea8912
fix: 誤字の訂正
yukikamome316 Dec 14, 2023
7d205da
fix: prod buildのエントリーの修正
yukikamome316 Dec 14, 2023
19d21dd
fix: migrator.shに名称を変更
yukikamome316 Dec 14, 2023
6e41a65
feat: マイグレーション関するコメントを補足説明
yukikamome316 Dec 15, 2023
5d95203
fix: 内部リンクの追加
yukikamome316 Dec 15, 2023
7b7aa50
fix: インデントのミスを修正
yukikamome316 Dec 15, 2023
d1f77d7
fix: 無駄なコロンの削除
yukikamome316 Dec 18, 2023
33961aa
feat: down migrationについて追記
yukikamome316 Dec 18, 2023
0dc2318
feat: down migrationの具体例を追加
yukikamome316 Dec 18, 2023
11c47e7
fix: インデントをスペースに変換
yukikamome316 Dec 18, 2023
7371424
fix: 誤字の訂正
yukikamome316 Dec 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,5 @@ jobs:
run: go mod download
- name: Backend Build Check
working-directory: ./backend
run: go build -o main .



run: go build -o main ./cmd/server/main.go

87 changes: 86 additions & 1 deletion README.md
yukikamome316 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,103 @@ MaximumメンバーがWeb研究部の活動として、Twitterのようなマイ

`./scripts/reset-db.sh` でDBデータを削除する

### DBのマイグレーション

> [!IMPORTANT]
> マイグレーションを初めて実行する場合、まずはデータベースのデータを削除する必要があります。これは、上記の「[DBデータの削除](#dbデータの削除)」にあるコマンドを使用して行えます。
> データの削除が完了したら、 `./scripts/migrator.sh init` を実行して、マイグレーションを行うための必要な情報をDB上に作成することができます。
> 詳しくは「[データベースのマイグレーションとモデル](#データベースのマイグレーションとモデル)」を参照してください。

- `./scripts/migrator.sh migrate` を実行することで、データベースのスキーマを最新の状態に更新できる

- `./scripts/migrator.sh rollback` を実行することで、データベースのスキーマを1つ前の状態に戻すことができる

- `./scripts/migrator.sh create_go <任意のコメント>` を実行することで、新しいマイグレーションを作成できる

- `./scripts/migrator.sh status`を実行することで、現在のマイグレーションの状態を確認できる

(他にもコマンドがありますが、詳しくは[この記事](https://zenn.dev/suetak/articles/5b3110358645b7)に書いてあるので確認してみてください。)

### デプロイ

`./scripts/deploy.sh` で本番環境にデプロイする
(マイグレーションなど特別なオペレーションが必要な場合もある)

## バックエンド処理の構造と役割

backend 配下は次のようなディレクトリとファイルで構成されています。

- `cmd`: このディレクトリは、アプリケーションが始まる場所で、Goのmain関数が含まれています。ここには、例えばmigrateとserverという2つのサブディレクトリがあります。`cmd/server/main.go`は、サーバーを起動するためのプログラムで、アプリケーションが始まるときに最初に実行されます。一方、`cmd/migrate/main.go`は、データベースの構造(スキーマ)を更新するためのマイグレーションを実行するプログラムで、これは`scripts/migrator.sh`によって実行されます。これらのプログラムは、アプリケーションの動作とデータの管理を制御します。
- `Dockerfile`: このファイルは、アプリケーションのDockerイメージをビルドするための指示を含んでいます。
- `external`: このディレクトリには、外部サービスとのインターフェースが含まれています。例えば、`discord.go`はDiscordとの通信を処理します。
- `handler`: このディレクトリには、HTTPリクエストを処理するためのハンドラが含まれています。
- `migrations`: このディレクトリには、データベースのマイグレーションが含まれています。これらのマイグレーションは、データベースのスキーマを変更するために使用されます。
- `model`: このディレクトリには、データベースのテーブルを表すGoの構造体が含まれています。これらの構造体は、データベースとのやり取りを容易にします。
- `public`: このディレクトリには、公開される静的ファイルが含まれています。

## データベースのマイグレーションとモデル

データベースのマイグレーションは、データベースのスキーマを管理する手段です。新しいテーブルを追加したり、既存のテーブルを変更したい場合は、マイグレーションを作成して実行します。

マイグレーションは`migrations`ディレクトリ内のGoファイルに記述されます。各マイグレーションファイルには、「アップ」操作と「ダウン」操作が含まれています。アップ操作はマイグレーションを適用するための操作を定義し、ダウン操作はマイグレーションをロールバックするための操作を定義します。

データベースのモデルは`model`ディレクトリ内のGoファイルに定義されます。各モデルはデータベースのテーブルを表すGoの構造体です。これらのモデルはデータベースとのやり取りを容易にします。

### 新しいカラムやテーブルを追加する

新しいカラムを追加する場合、対応するモデルのGo構造体に新しいフィールドを追加します。次に、新しいマイグレーションを作成して、データベースのテーブルに新しいカラムを追加します。

新しいテーブルを追加するには、新しいモデルのGo構造体を作成します。次に、新しいマイグレーションを作成して、新しいテーブルをデータベースに追加します。

これらの操作は、`scripts/migrator.sh`を使用して行います。詳細な手順は以下の通りです。

1. **モデルの作成**: `model`ディレクトリに新しいGoファイルを作成します。このファイルには、新しいテーブルを表すGoの構造体を定義します。

2. **マイグレーションの作成**: `./scripts/migrator.sh create_go <任意のコメント>`を実行して、マイグレーション用のGoファイルを作成します。コメントはGitのコミットメッセージのように、「create users table」などと更新内容が分かりやすいと良いです。実行すると、`migrations`ディレクトリに日時と指定したコメントが合わさったような名前のGoファイルが作成されます。例えば、 `./scripts/migrator.sh create_go create initial tables` を実行して`migrations`ディレクトリに`20231109002750_create_initial_tables.go`を作成しました。

3. **マイグレーションの処理**: 作成されたGoファイルにマイグレーションの処理を書きます。例えば、`20231109002750_create_initial_tables.go`では以下のようなup migrationの処理を書いています。

```go
fmt.Print(" [up migration] ")
_, err := db.NewCreateTable().Model((*model.Post)(nil)).Exec(ctx)
if err != nil {
return err
}
_, err = db.NewCreateTable().Model((*model.User)(nil)).Exec(ctx)
return err
```

これは`model/post.go`と`model/user.go`に定義されているモデルをデータベースに追加する処理です。このように、Bunのmigration機能では実際にアップデートの処理を手動でup migrationに書く必要があります。

また、down migrationの処理はup migrationの処理を完全に打ち消せるように書きます。
例えば、`20231109002750_create_initial_tables.go`ではup migrationで`posts`テーブルと`users`テーブルを作成します。よって、down migrationではこれらのテーブルを削除するような処理を書きます。

```go
fmt.Print(" [down migration] ")
_, err := db.NewDropTable().Model((*model.Post)(nil)).IfExists().Exec(ctx)
if err != nil {
return err
}
_, err = db.NewDropTable().Model((*model.User)(nil)).IfExists().Exec(ctx)
return err
```

このようにすることでマイグレーションをロールバックすることができるようになるため、バージョン管理が容易になるというメリットがあります。
yukikamome316 marked this conversation as resolved.
Show resolved Hide resolved

4. **マイグレーションの実行**: `./scripts/migrator.sh migrate`を実行して、新しいマイグレーションを適用します。

以上の手順により、新しいカラムやテーブルをデータベースに追加することができます。

より具体的な操作は[この記事](https://zenn.dev/suetak/articles/5b3110358645b7)に説明があるので、参考にしてみてください。


## スプリント

毎週月曜日にあるWeb研究会の講義の間を1スプリントと定義する。

## バージョニング

バージョンは1スプリントでマイナーバージョン x.X.x を上げることにする。
それよりも細かい単位の変更 (スプリント中だけど緊急で修正箇所が浮上したなど)でリリースが必要な場合、パッチバージョン x.x.X を上げることにする。
それよりも細かい単位の変更スプリント中だけど緊急で修正箇所が浮上したなど)でリリースが必要な場合、パッチバージョン x.x.X を上げることにする。

メジャーバージョンに関しては区切りが良くなったタイミングであげるで良い。
4 changes: 2 additions & 2 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ FROM golang:1.20-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 go build -o main .
RUN CGO_ENABLED=0 go build -o main ./cmd/server/main.go


FROM alpine AS prod
Expand All @@ -27,4 +27,4 @@ COPY go.mod go.sum ./
RUN go mod download
EXPOSE 8000

CMD ["go", "run", "main.go"]
CMD ["go", "run", "./cmd/server/main.go"]
228 changes: 228 additions & 0 deletions backend/cmd/migrate/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package main

import (
"context"
"database/sql"
"fmt"
"log"
"os"
"strings"

"github.com/go-sql-driver/mysql"
"github.com/saitamau-maximum/maxitter/backend/migrations"

"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/mysqldialect"
"github.com/uptrace/bun/extra/bundebug"
"github.com/uptrace/bun/migrate"

"github.com/urfave/cli/v2"
)

const MIGRATION_TABLE = "bun_migrations"

func getEnv(key, fallback string) string {
value, ok := os.LookupEnv(key)
if !ok {
value = fallback
}
return value
}

func connectDB() (*sql.DB, error) {
user := getEnv("MYSQL_USER", "user")
password := getEnv("MYSQL_PASSWORD", "password")
host := getEnv("MYSQL_HOST", "localhost")
port := getEnv("MYSQL_PORT", "3306")
dbname := getEnv("MYSQL_DATABASE", "db")

c := mysql.Config{
User: user,
Passwd: password,
Net: "tcp",
Addr: fmt.Sprintf("%s:%s", host, port),
DBName: dbname,
}

db, err := sql.Open("mysql", c.FormatDSN())
if err != nil {
return nil, err
}

return db, nil
}

func main() {
db, err := connectDB()
if err != nil {
panic(err)
}

bunDB := bun.NewDB(db, mysqldialect.New())
bunDB.AddQueryHook(bundebug.NewQueryHook(
bundebug.WithEnabled(false),
bundebug.FromEnv(""),
))

defer bunDB.Close()

if err := checkMigrationsTable(context.Background(), bunDB); err != nil {
log.Println("Migrations table does not exist \n\n\t run `./scripts/migrator.sh init` first")
return
}

app := &cli.App{
Name: "bun",

Commands: []*cli.Command{
newDBCommand(migrate.NewMigrator(bunDB, migrations.Migrations)),
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}

func newDBCommand(migrator *migrate.Migrator) *cli.Command {
return &cli.Command{
Name: "db",
Usage: "database migrations",
Subcommands: []*cli.Command{
{
Name: "init",
Usage: "create migration tables",
Action: func(c *cli.Context) error {
return migrator.Init(c.Context)
},
},
{
Name: "migrate",
Usage: "migrate database",
Action: func(c *cli.Context) error {
if err := migrator.Lock(c.Context); err != nil {
return err
}
defer migrator.Unlock(c.Context)

group, err := migrator.Migrate(c.Context)
if err != nil {
return err
}
if group.IsZero() {
fmt.Printf("there are no new migrations to run (database is up to date)\n")
return nil
}
fmt.Printf("migrated to %s\n", group)
return nil
},
},
{
Name: "rollback",
Usage: "rollback the last migration group",
Action: func(c *cli.Context) error {
if err := migrator.Lock(c.Context); err != nil {
return err
}
defer migrator.Unlock(c.Context)

group, err := migrator.Rollback(c.Context)
if err != nil {
return err
}
if group.IsZero() {
fmt.Printf("there are no groups to roll back\n")
return nil
}
fmt.Printf("rolled back %s\n", group)
return nil
},
},
{
Name: "lock",
Usage: "lock migrations",
Action: func(c *cli.Context) error {
return migrator.Lock(c.Context)
},
},
{
Name: "unlock",
Usage: "unlock migrations",
Action: func(c *cli.Context) error {
return migrator.Unlock(c.Context)
},
},
{
Name: "create_go",
Usage: "create Go migration",
Action: func(c *cli.Context) error {
name := strings.Join(c.Args().Slice(), "_")
mf, err := migrator.CreateGoMigration(c.Context, name)
if err != nil {
return err
}
fmt.Printf("created migration %s (%s)\n", mf.Name, mf.Path)
return nil
},
},
{
Name: "create_sql",
Usage: "create up and down SQL migrations",
Action: func(c *cli.Context) error {
name := strings.Join(c.Args().Slice(), "_")
files, err := migrator.CreateSQLMigrations(c.Context, name)
if err != nil {
return err
}

for _, mf := range files {
fmt.Printf("created migration %s (%s)\n", mf.Name, mf.Path)
}

return nil
},
},
{
Name: "status",
Usage: "print migrations status",
Action: func(c *cli.Context) error {
ms, err := migrator.MigrationsWithStatus(c.Context)
if err != nil {
return err
}
fmt.Printf("migrations: %s\n", ms)
fmt.Printf("unapplied migrations: %s\n", ms.Unapplied())
fmt.Printf("last migration group: %s\n", ms.LastGroup())
return nil
},
},
{
Name: "mark_applied",
Usage: "mark migrations as applied without actually running them",
Action: func(c *cli.Context) error {
group, err := migrator.Migrate(c.Context, migrate.WithNopMigration())
if err != nil {
return err
}
if group.IsZero() {
fmt.Printf("there are no new migrations to mark as applied\n")
return nil
}
fmt.Printf("marked as applied %s\n", group)
return nil
},
},
},
}
}

func checkMigrationsTable(ctx context.Context, db *bun.DB) error {
if os.Args[2] == "init" {
return nil
}

if _, err := db.NewSelect().Table(MIGRATION_TABLE).Exists(ctx); err != nil {
return err
}

return nil
}
Loading
Loading