5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

複数のプログラミング言語でORMを使ってみる(Go言語GORM編)

Last updated at Posted at 2025-02-25

はじめに

この前、あじ太郎は複数のプログラミング言語で配列宣言する記事を書いてみたら楽しかったので、今度はORMの比較記事も書いてみようと思いました。
ORMをテーマに記事を書くと分量が多くなりそうなので、一言語づつ取り上げます。

第一弾として今回はGo言語のORM「GORM」です。

サンプルコードと実行環境など

サンプルコードを以下にアップしました。
https://github.com/aji-taro/gorm-introduction
Dockerで試せるようにしています。
使い方はREADME.mdに記載したのでご参照ください。

データベースへの接続

GORMのページを参考にしつつ、サンプルコードでは以下のようにしてみました。

main.go
var db *gorm.DB

// データベースに接続する
// https://gorm.io/ja_JP/docs/connecting_to_the_database.html#MySQL
func connectDb() error {
	dataSourceName := os.Getenv("DB_USER") + ":" + os.Getenv("DB_PASSWORD") +
		"@tcp(" + os.Getenv("DB_HOST") + ":" + os.Getenv("DB_PORT") + ")/" +
		os.Getenv("DB_NAME") +
		"?charset=utf8mb4&parseTime=True&loc=Asia%2FTokyo"
	var err error
	db, err = gorm.Open(mysql.Open(dataSourceName), &gorm.Config{})
	if err != nil {
		return fmt.Errorf("gorm.Open error", err)
	}
    
	return nil
}

あじ太郎はPHPを使いますが、Laravel(Eloquent)ではIDやパスワードを設定ファイルに記述するのみで、明示的にOpenする実装とかはしたことがないような気がする。
こういうところは、プログラミング言語やフレームワークの設計者の考えが反映される部分かな。

モデルの定義

以下を定義しました。

main.go
// User テーブルのモデル
// ※参照:モデルを宣言する(https://gorm.io/ja_JP/docs/models.html)
// ※参照:Belongs To(https://gorm.io/ja_JP/docs/belongs_to.html#Belongs-To)
// ※ここでは、上記ページの構造体をコピペしたけどgorm.Modelを使ってもよいかも
type User struct {
	ID           uint           // Standard field for the primary key
	Name         string         // A regular string field
	Email        *string        // A pointer to a string, allowing for null values
	Age          uint8          // An unsigned 8-bit integer
	Birthday     *time.Time     // A pointer to time.Time, can be null
	MemberNumber sql.NullString // Uses sql.NullString to handle nullable strings
	ActivatedAt  sql.NullTime   // Uses sql.NullTime for nullable time fields
	CompanyId    uint
	Belong       Company `gorm:"foreignKey:CompanyId"`
	CreatedAt    time.Time      // Automatically managed by GORM for creation time
	UpdatedAt    time.Time      // Automatically managed by GORM for update time
	ignored      string         // fields that aren't exported are ignored
}

type Company struct {
	ID   uint
	Name string
}

以下、基本的なルールなど

  • 構造体の名称はテーブル名をもとにする(usersテーブルの場合、User構造体にする。PHPのEloquentもクラス名からテーブルを判別するし、こういう仕様がトレンドなのかな。)
  • idはIDにする
  • その他のフィールドはパスカルケースにする(最初の文字が大文字なのはGORMの仕様というより、Go言語では大文字じゃないと他のパッケージからアクセスできないからか。)
  • NULLを許容するカラムの場合、ポインタにする場合とsql.NullStringのように明確にNULLに対応した型もある

テーブル定義

Go言語のマイグレーションといえば、GORMのAutoMigrate、あとはgolang-migrateもありますが、サンプルはMySQLの機能でSQLを実行するようにしました。(SQLにしておくと他の記事でも流用しやすいかなと。)
SQLの内容は、モデルの定義をもとに考えています。

initdb.d/init.sql
CREATE TABLE companies (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(15) NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

INSERT INTO companies (id, name) VALUES (1, 'company-1');
INSERT INTO companies (id, name) VALUES (2, 'company-2');

CREATE TABLE users (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(15) NOT NULL,
  email VARCHAR(255), /* UNIQUE にしてもいいかも */
  age SMALLINT NOT NULL,
  birthday DATE,
  member_number VARCHAR(10),
  activated_at DATETIME,
  company_id BIGINT,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

INSERT INTO users (id, name, age, company_id) VALUES (1, 'yamada taro', 20, 2);

各項目の桁数は適当です。

レコードの作成

いよいよクエリを実行!
レコードを挿入してみます。
参照:https://gorm.io/ja_JP/docs/create.html

main.go
// レコードの作成
// https://gorm.io/ja_JP/docs/create.html
fmt.Println("- Create")
now := time.Now()
newUser := User{Name: "Jinzhu", Age: 18, Birthday: &now}
createResult := db.Create(&newUser);
if createResult.Error != nil {
    fmt.Println(createResult.Error)
    return
}
fmt.Println("inserted data's primary key", newUser.ID)
fmt.Println("inserted records count", createResult.RowsAffected)

サンプルコードはphpMyAdminでテーブルを確認できるようにしています。利用方法はサンプルコードに付属のREADME.mdに記載していますので必要に応じてご利用ください。
phpMyAdmin.png

レコードの取得

GORMのページに記載されている例からいくつか試しました。
参照:https://gorm.io/ja_JP/docs/query.html

main.go
	// レコードの取得 その 1
	// https://gorm.io/ja_JP/docs/query.html
	fmt.Println("- First")
	user01 := User{}
	if result := db.First(&user01, newUser.ID); result.Error != nil {
		fmt.Println(result.Error)
		return
	}
	fmt.Println("user01", user01)

	// レコードの取得 その 2
	fmt.Println("- Where")
	user02 := User{}
	if result := db.Where("name = ?", "jinzhu").First(&user02); result.Error != nil {
		fmt.Println(result.Error)
		return
	}
	fmt.Println("user02", user02)

レコードの更新

トランザクションも使ってみる。
参照(レコードの更新):https://gorm.io/ja_JP/docs/update.html
参照(トランザクション):https://gorm.io/ja_JP/docs/transactions.html#%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B6%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3

main.go
// トランザクション
// https://gorm.io/ja_JP/docs/transactions.html#%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B6%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3
fmt.Println("- Transaction")
db.Transaction(func(tx *gorm.DB) error {
    // レコードの更新
    // https://gorm.io/ja_JP/docs/update.html
    fmt.Println("- Save")
    user02.Name = "jinzhu 2"
    user02.Age = 100
    if result := tx.Save(&user02); result.Error != nil {
        fmt.Println("error", result.Error)
        return errors.New("rollback user2")
    }
    fmt.Println("update user02")
    return nil
})

この記事では試してないけど、トランザクションのネストにも対応している。あれ?そもそもMySQL自体がトランザクションのネストに対応しているのかな?と思ったらSAVEPOINTが使えそう。(別の勉強になった。)

Eager Loading

Eloquent(Laravel)のリレーションはメソッドで定義するけど、GORMの場合は構造体にアソシエーションを含めて定義する。
参照:https://gorm.io/ja_JP/docs/preload.html

main.go
	// Eager Loading(Preload)
	// https://gorm.io/ja_JP/docs/preload.html#Preload
	fmt.Println("- Preload")
	preloadUser := User{}
	if result := db.Preload("Belong").First(&preloadUser, 1); result.Error != nil {
		fmt.Println("error", result.Error)
		return
	}
	fmt.Println("preloadUser.Name", preloadUser.Name )
	fmt.Println("preloadUser.Belong", preloadUser.Belong )

そういえば、過去にLaravelでEager Loadingでメモリを使いすぎるケースがあったので、GORMのPreloadにも注意することもあるのかなと思っていたら、カミナシさんがブログで Preload を使わずに平行処理にした事例をブログに掲載されてました。
https://kaminashi-developer.hatenablog.jp/entry/202204/backend-kaizen

ジョイン

実は、ジョインもアソシエーションを指定してdb.Joins("Company").Find(&users)みたいな書き方ができるけど、あえて別の書き方を試してみました。
参照:https://gorm.io/ja_JP/docs/query.html#Joins

main.go
	// ジョイン
	// https://gorm.io/ja_JP/docs/query.html#Joins
	fmt.Println("- Joins")
	type AliasModel struct {
		UserName string
		CompanyName string
	}
	aliasModel := AliasModel{}
	if result := db.Model(&User{}).Select("users.name as user_name, companies.name as company_name").Joins("JOIN companies ON users.company_id = companies.id").Scan(&aliasModel); result.Error != nil {
		fmt.Println("error", result.Error)
		return
	}

	fmt.Println("aliasModel", aliasModel )

取り組んでみて

ORMを比較するというテーマで作業開始したけど、なんとなくGORMのサイトに記載されている内容を実際に試せる環境を用意したという記事になったなと感じたあじ太郎なのでした。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?