😆

Goでマイグレーションしてみる!!!!

2024/11/14に公開

概要

今回の記事は駆け出しエンジニアと言っていいのかどうかすらわからないくらいに弱々エンジニアの自分が書く記事ですので適当に読んでください!そして何かしらの不備があったら是非指摘してください!お願いします!!自分の経験として最初のデータベースを準備してそれにデータを入れて確認してみるということが非常に難しかったのでそこの言語化をして自分の身になればいいなーと思って書きます!

最近golangを触ってます。1年くらい前に1回golangかっこいいなーって思いながら勉強してみたのですがその時は挫折してしまったのですが最近触ったら意外といけるなってなったのでお楽しみ程度に触ってますー
なんだかんだ1年経つと人間は成長しているものですね笑
特にほかの言語を極めたとかではないんですけどAPIの開発を行ってみたいなと感じたのでGinとかを触ってます。
簡単なCRUDが行えるアプリケーションの作成に関する記事もこれから書いてみたいなーと思っているのでぜひみてみてくださいー
今回の記事では以下の記事を参考に私が学習していてこれってなんだろうとかなった箇所に関して書いていきます。(この方の記事でかなり流れが掴めたと自分では感じてます。)

https://zenn.dev/daino/articles/41524068ce68bf

準備しておく環境

  • Docker Desktopを使用することができること
  • goを実行することができること
  • 何かしらのエディター(これはなくてもいいです)

確かwindowsでDocker Desktopの環境構築するのは少し手間取った気がします笑

https://qiita.com/zembutsu/items/a98f6f25ef47c04893b3

こちらの記事とか参考にしてください!

大まかな流れ

今回のデータベースを準備してそこにカラムを追加することです!
このデータベースとかを準備するのが一番難しいと感じますよね。
それでは流れを見ていきます。

  1. Goのプロジェクトを準備、gormなどをプロジェクト内で使用できるようにしておく
  2. .envファイル、docker-compose.ymlをディレクトリに作成する
  3. docker compose upでデータベースサーバーを立てる
  4. マイグレーションのもとになるファイルを作成する
  5. 実際にマイグレーションを行う
  • .env 環境変数やデータベースにアクセスする際のデータを書いておくファイル。

  • gorm golangのためのORM。これを使用することでSQLを使用しないでコードからSQLを実行することができる。また、マイグレーションなども行うことができる。
    ↓gormの公式ドキュメント

https://gorm.io/ja_JP/docs/index.html

実際にやってみる

goのプロジェクトの準備、依存関係のインストール

まずは使用するプロジェクトのルートディレクトリに移動します。
今回はtodo-testという名前のプロジェクトで作業をしていきます。ここのプロジェクトの名前は自分の好きなように変更してください!
それではまずは

go mod init (プロジェクトの名前)

するとgo.modファイルができるかと思います。このファイルは使用されるモジュールの依存関係を管理しているファイルです。
次に

go get -u github.com/gin-gonic/gin

を実行します。これはGin(golangのフレームワーク)を使用することができるようにするためのコマンドです。これを実行するとターミナルにたくさん文字が表示されてgo.modファイルにもたくさん書きこまれます。加えてgo.sumが作成されているかと思います。これはファイル内のモジュールの依存関係をハッシュ化したものを保存しているらしい...難しい!笑
ただ使いこなすだけではなく依存関係とかそういう細かいところもしっかりわかるようになりたいですね
詳しくは以下の記事を参考にしてください。

https://qiita.com/soicchi/items/2637a9195e64fdc73609

gorm,sqlite用のドライバー、postgressql用のドライバーのインストールをします。以下のコマンドを1行ずつ実行します。

go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
go get -u gorm.io/driver/postgres
go get github.com/joho/godotenv

ここまできたらgo.modファイルは以下のようになっていると思います。これはあくまで1部ですのでご注意ください。

go.mod
module todo-test

go 1.23.2

require (
	github.com/bytedance/sonic v1.12.4 // indirect
	github.com/bytedance/sonic/loader v0.2.1 // indirect
	github.com/cloudwego/base64x v0.1.4 // indirect
    ...

たくさんのパッケージがインストールされているかと思います。今回の記事ではGinを使用することはないのですが、一応インストールしておきました。(今後の記事で使用することになると思うので)

.envファイル、docker-compose.ymlの準備

touch .env
touch docker-compose.yml

を実行してそれぞれファイルをルートディレクトリに作成します。
作成したファイルを次のように編集します

.env
POSTGRES_USER=todouser
POSTGRES_PASSWORD=gintodopass
POSTGRES_DB=todo_database
PGADMIN_DEFAULT_EMAIL=gin@example.com
PGADMIN_DEFAULT_PASSWORD=gintodopass
docker-compose.yml
version: "3.8"
services:
  postgres:
    image: postgres:16-alpine
    container_name: postgres
    ports:
      - 5433:5432
    volumes:
      - ./docker/postgres/init.d:/docker-entrypoint-initdb.d
      - ./docker/postgres/pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
      POSTGRES_DB: ${POSTGRES_DB}
    hostname: postgres
    restart: always
    user: root

  pgadmin:
    image: dpage/pgadmin4
    restart: always
    ports:
      - 81:80
    environment:
      PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
    volumes:
      - ./docker/pgadmin:/var/lib/pgadmin
    depends_on:
      - postgres

docker-compose.ymlファイルの書き方に関してはまだまだ勉強中なので時に書くことはできないです実力不足ですみません笑
言及しておくこととしては、とりあえずdocker-compose.yml${~~~}という記述の部分が.envを読み込んでいるということくらいです。

docker composeでサーバーを立ち上げる

それではいよいよサーバーを立ち上げてみます!DockerDesktopを立ち上げた状態で以下のコマンドを実行します。

docker compose up --build (-d)

-dはつけてもつけなくても大丈夫です。このオプションをつけずに立ち上げるとログ?が表示されてつけないとログ?が表示されます。あれは果たしてログというのかどうかわからないです笑
2回目のdocker composeからは--buildは使用しなくて大丈夫です。
自分はインターンでも毎回

docker compose up -d

のみで使用しています。
--buildを含んだコマンドを実行すると少し時間がかかるかと思いますがコンテナが立ち上がります。(なんかこんな感じの画面がでてしばらく待つと完了します。)

[+] Running 24/13
 ⠇ postgres [⣿⣿⣿⣿⣿⣿⠀⣿⣿⣿⣿] 14.68MB / 97.73MB Pulling                                                                                                                                                                                                                              21.9s 
 ⠇ pgadmin [⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣿⣿⣿⠀⣿] 68.82MB / 160.9MB Pulling

これが完了したらルートディレクトリにdockerというファイルが作成されているかと思います。これが作成されていることを確認したら、
http://localhost:81/
にアクセスしてみましょう

ログイン画面では.envで設定した情報を入力していきましょう。
Emailの欄にはtodo@example.com
Passwordの欄にはtodopassを入力しましょう。

この画面になっていれば成功です!
この画面から中央のAdd New Serverをクリックしてフォームに入力していきます。

これらの情報を入力してこのような画面が出てきたら完了です!

マイグレーションのもとになるファイルを作成する

ここでは実際にデータベースに登録するデータの情報を登録していきます!
まずはルートディレクトリで以下のコマンドを実行します。

mkdir database
cd database
touch todotype.go

ここでは構造体を使用してデータの型を定義していきます。
Title:文字列型
Description:文字列型
Deadline:時間型
とめちゃくちゃシンプルですが定義しておきます!
コードは以下の通りです。

todotype.go
package database

import (
	"time"

	"gorm.io/gorm"
)

type Todo struct {
	gorm.Model
	Title       string
	Description string
	Deadline    time.Time
}

ここでgorm.Modelって何だよってなりましたよね、自分もなりました。
中身を見てみると

model.go
package gorm

import "time"

// Model a basic GoLang struct which includes the following fields: ID, CreatedAt, UpdatedAt, DeletedAt
// It may be embedded into your model or you may build your own model without it
//
//	type User struct {
//	  gorm.Model
//	}
type Model struct {
	ID        uint `gorm:"primarykey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt DeletedAt `gorm:"index"`
}

こんな感じになってます。これはgormが準備してくれているもので勝手にプライマリーキーのIDと作成日時、更新日時、削除日時を更新してくれるというものなのですね。
ちなみにこの削除日時は初期値はnullになっていてここに値(時間)が入力されたということが削除を示しています。完全に削除はしていなく、ソフトデリートなので復元可能な削除であることがわかりますね!
結局のところ、次のようなデータの構造体を定義していることになります。

todotype.go
package database

import (
	"time"

	"gorm.io/gorm"
)

type Todo struct {
	ID          uint `gorm:"primarykey"`
	CreatedAt   time.Time
	UpdatedAt   time.Time
	DeletedAt   DeletedAt `gorm:"index"`
	Title       string
	Description string
	Deadline    time.Time
}

ちなみにgorm:"primarykeyこの部分はメタデータとして読み込まれていてただの文字列として認識されるので補完などは効かないです。ここの情報に関しては今回は何も記述しません。データがnot nullとかnullableなどの基本的な制限から、リレーショナルデータベースの外部キーなどの設定もできます!

実際にマイグレーションを行う

.envファイルに以下のコードを追記します。

.env
DB_HOST=localhost
DB_USER=todouser
DB_PASSWORD=todopass
DB_NAME=todo_database
DB_PORT=5433

ルートディレクトリに移動して以下のコマンドを実行します。

mkdir migration
cd migration
touch migration.go

まだ作成したファイルは触らないでおきます。
次に以下のコマンドを実行します。

cd ../database
touch database.go

コードの流れは後で追います。
作成したdatabase.goを以下のように編集します。

database.go
package database

import (
	"fmt"
	"os"

	"github.com/joho/godotenv"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

func Initialize() {
	err := godotenv.Load()
	if err != nil {
		panic(".envファイルの情報が取得できません")
	}
}

func DatabaseInit() *gorm.DB {
	host := os.Getenv("DB_HOST")
	user := os.Getenv("DB_USER")
	password := os.Getenv("DB_PASSWORD")
	dbname := os.Getenv("DB_NAME")
	port := os.Getenv("DB_PORT")

	if host == "" || user == "" || password == "" || dbname == "" || port == "" {
		panic("必要な環境変数が設定されていません")
	}

	dbinfo := fmt.Sprintf(
		"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Tokyo",
		host,
		user,
		password,
		dbname,
		port,
	)

	db, err := gorm.Open(postgres.Open(dbinfo), &gorm.Config{})

	if err != nil {
		panic("データベースへの接続情報が異なっています")
	}

	return db
}

次に作成してあったmigrationディレクトリのmigration.goを編集します。

migration.go
package main

import (
	"log"
	"todo-test/database"
)

func main() {
	database.Initialize()
	db := database.DatabaseInit()

	err := db.AutoMigrate(&database.Todo{})
	if err != nil {
		panic("migration失敗!")
	}

	log.Println("migration成功!")
}

今回のマイグレーションではmigration.goを実行します。

go run migration.go

を実行すると、まずdatabase.Initialize()を呼び出します。この関数では.envファイルの情報を取得するための準備をしているイメージです。ここでもし準備ができなかったらpanicで処理を中断するようにしています。
次に呼び出されるのはdatabase.DatabaseInit()です。この関数では先ほど準備した.envの内容を取得しています。そして仮にその情報がなければ処理を中断、全て取得できればデータベースへの接続に必要なデータをSprintfでテキスト化して埋め込んでいます。それ以降は基本的にエラーハンドリングとデータベースへの接続の決まった書き方をしています。今回の関数の返り値の型は*gorm.DBなのでdb変数を返すようにしています。
そしてdb.AutoMigrate関数を使用してデータベースのカラムの作成、カラムのデータの型の決定を行います。err := db.AutoMigrate(&database.Todo{})ここで出てきた&database.Todo{}が今回のデータベースの形を決めているということになります!データ型の情報を持たせた構造体のポインタを渡すだけでデータベースを作成してくれるのは非常に便利ですね!
これ以降のコードでもエラーハンドリングを行い、エラーがなければmigration成功!というメッセージがターミナルに表示されるようになっています。
先ほどのコマンドを実行した結果として以下のような出力が得られます!

2024/11/13 23:30:27 migration成功!

これが出力されたら先ほどの

http://localhost:81

にアクセスしてみましょう。
左側のサイドバーを画像のように展開してきます。
todosというところをクリックして、サイドバーの右上の表のような図を押してみると...

こんか感じでデータベースができているのがわかります!
しっかりデータの名前とデータの型が指定できているのがわかりますね!

今回の目的は達成できました!

最後に

今回初めてzennに記事を投稿してみました。よくアウトプットが大切だ!と言われる理由が分かったような気がします。これからも様々な学習したことを投稿していきます。そしてより強いエンジニアになります!
これからは簡単なAPIの開発に関しての記事などを書いてみたいと考えています。今回のTodoのデータに対してリレーショナルデータベースを導入してCRUD(Create, Read, Update, Delete)を実装していこうと思います。ここまで読んでくれてありがとうございました。何か不備などがあればご指摘ください。

Discussion