🌊

GormGenのFieldRelateを使用して テーブルの関連を表した構造体を生成する

2023/03/24に公開

はじめに

レスキューナウでは、バックエンドの開発に主にGo言語を使用していて、データベースとのやり取りにはGORMを使用しています。
GormGenを使用して、接続しているデータベースからGORMで使用する構造体を自動生成することで、開発効率を向上させていました。ですが、テーブル同士の関連を生成することができなかったため、手動で構造体を編集する必要がありました。
GormGenの gen.FieldRelateを使用することで手動で関連を定義する方法を調べました。

GORMについてはこちらの使い方を御覧ください
https://gorm.io/docs/

GormGenとは

GormGenは、Go言語のORMライブラリのGORMのプロジェクトの一つで
https://gorm.io/gen/index.html
既存のデータベーステーブルをGo言語の構造体にマッピングしたい場合に使用します。生成するコマンドを実行すると、GORMがデータベースのスキーマを解析し、各テーブルに対応するGORMで使用できるGo言語の構造体を自動的に生成します。
GormGenは生成される構造体のフィールド名やタグのカスタマイズも可能です。生成されたコードは、指定されたディレクトリに保存されます。

GormGenの使い方

インストール

パッケージはこちら
https://pkg.go.dev/gorm.io/gen

使用するテーブル

サンプルで使用するテーブル
users playlists songsのテーブルがあり
1対多の関係になっています。

CREATE TABLE SQL
CREATE TABLE
CREATE TABLE IF NOT EXISTS `users` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` TEXT NULL,
  `birthday` VARCHAR(45) NULL,
  `address` TEXT NULL,
  `created_at` DATETIME NOT NULL,
  `updated_at` DATETIME NULL,
  `deleted_at` DATETIME NULL,
  PRIMARY KEY (`id`))
ENGINE = InnoDB;

CREATE TABLE IF NOT EXISTS `playlists` (
  `id` BIGINT NOT NULL,
  `user_id` BIGINT UNSIGNED NOT NULL,
  `name` VARCHAR(45) NULL,
  `created_at` DATETIME NOT NULL,
  `updated_at` DATETIME NULL,
  `deleted_at` DATETIME NULL,
  PRIMARY KEY (`id`),
  INDEX `fk_playlists_user1_idx` (`user_id` ASC),
  CONSTRAINT `fk_playlists_user1`
    FOREIGN KEY (`user_id`)
    REFERENCES `user` (`id`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB;

CREATE TABLE IF NOT EXISTS `songs` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `playlists_id` BIGINT NOT NULL,
  `title` TEXT NULL,
  `track` TEXT NULL,
  `created_at` DATETIME NOT NULL,
  `updated_at` DATETIME NULL,
  `deleted_at` DATETIME NULL,
  PRIMARY KEY (`id`),
  INDEX `fk_songs_playlists1_idx` (`playlists_id` ASC),
  CONSTRAINT `fk_songs_playlists1`
    FOREIGN KEY (`playlists_id`)
    REFERENCES `playlists` (`id`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB;

簡単な使い方

DBに接続してすべてのテーブルの構造体を出力します

package main

import (
 "gorm.io/driver/mysql"
 "gorm.io/gen"
 "gorm.io/gorm"
)

func main() {
  g := gen.NewGenerator(gen.Config{
    OutPath: "./gen/query",
    Mode: gen.WithoutContext
  })
  gormdb, _ := gorm.Open(mysql.Open("root:@(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"))
  g.UseDB(gormdb)
  g.ApplyBasic(g.GenerateAllTable()...)
  g.Execute()
}

Gormで接続したDBを使用して、g.GenerateAllTable()ですべてのテーブルを対象として
実行すると 、指定した./gen/queryに テーブルの構造体が出力されます

出力結果

生成された構造体
user.go
package model

import (
	"time"
	"gorm.io/gorm"
)

const TableNameUser = "user"
type User struct {
	ID        int64          `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
	Name      string         `gorm:"column:name" json:"name"`
	Birthday  string         `gorm:"column:birthday" json:"birthday"`
	Address   string         `gorm:"column:address" json:"address"`
	CreatedAt time.Time      `gorm:"column:created_at;not null" json:"created_at"`
	UpdatedAt time.Time      `gorm:"column:updated_at" json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"column:deleted_at" json:"deleted_at"`
}

// TableName User's table name
func (*User) TableName() string {
	return TableNameUser
}
playlists.go
package model

import (
	"time"
	"gorm.io/gorm"
)

const TableNamePlaylists = "playlists"
type Playlists struct {
	ID        int64          `gorm:"column:id;primaryKey" json:"id"`
	UserID    int64          `gorm:"column:user_id;not null" json:"user_id"`
	Name      string         `gorm:"column:name" json:"name"`
	CreatedAt time.Time      `gorm:"column:created_at;not null" json:"created_at"`
	UpdatedAt time.Time      `gorm:"column:updated_at" json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"column:deleted_at" json:"deleted_at"`
}

// TableName Playlists's table name
func (*Playlists) TableName() string {
	return TableNamePlaylists
}

songs.go
package model

import (
	"time"
	"gorm.io/gorm"
)

const TableNameSongs = "songs"
type Songs struct {
	ID          int64          `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
	PlaylistsID int64          `gorm:"column:playlists_id;not null" json:"playlists_id"`
	Title       string         `gorm:"column:title" json:"title"`
	Track       string         `gorm:"column:track" json:"track"`
	CreatedAt   time.Time      `gorm:"column:created_at;not null" json:"created_at"`
	UpdatedAt   time.Time      `gorm:"column:updated_at" json:"updated_at"`
	DeletedAt   gorm.DeletedAt `gorm:"column:deleted_at" json:"deleted_at"`
}

// TableName Songs's table name
func (*Songs) TableName() string {
	return TableNameSongs
}

すべてテーブルを指定して構造体の出力を実行しましたが、
テーブル間に関連がある場合、それぞれのテーブルの構造体を生成しただけでは、その関連性を表現することができません。
GormGenを使用してデータベースのテーブルからGo言語の構造体を自動生成する場合、テーブル間の1対多の関係を含めた構造体を生成するためには、それぞれのテーブルとその関連を指定する必要があります。

テーブルの関連を持った構造体生成

package main

import (
 "gorm.io/driver/mysql"
 "gorm.io/gen"
 "gorm.io/gorm"
)

func main() {
  g := gen.NewGenerator(gen.Config{
    OutPath: "./gen/query",
    Mode: gen.WithoutContext
  })
  gormdb, _ := gorm.Open(mysql.Open("root:@(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"))
  
 g.UseDB(gormdb)

 var fieldOpts []gen.FieldOpt
 allModel := g.GenerateAllTable(fieldOpts...)

 songs := g.GenerateModel("songs")
 playlists := g.GenerateModel("playlists", append(
  fieldOpts, gen.FieldRelate(field.HasMany, "Songs", songs,
   &field.RelateConfig{
    GORMTag: "foreignKey:PlaylistID",
   }))...,
 )

 users := g.GenerateModel("user", append(
  fieldOpts, gen.FieldRelate(field.HasMany, "Playlists", playlists,
   &field.RelateConfig{
    RelateSlice: true,
    GORMTag:     "foreignKey:UserID",
   }))...,
 )


 g.ApplyBasic(users, playlists, songs)
 g.ApplyBasic(allModel...)
 g.Execute()

}

関連するフィールドに対してgen.FieldRelateを使用します。
対象のフィールドに外部キーを追加し、テーブル間の関連を明示的に表現することができます。

このコードでは

songs := g.GenerateModel("songs")

で関連するテーブルを定義して

gen.FieldRelate(field.HasMany, "Songs", songs,&field.RelateConfig{
GORMTag: "foreignKey:PlaylistID",
   }))...,

で関連付けを明示的に行っています、field.HasManyで 1対多の関係
"Songs"の箇所で構造体のfield名を定義します
GORMTag: "foreignKey:PlaylistID"で fieldに対してforeignKeyの指定を行うことで
テーブル間の関連をもった構造体を生成することができます。

HasMany以外にもHasOne BelongsTo Many2Manyを指定できます

const (
    HasOne    RelationshipType = RelationshipType(schema.HasOne)    // HasOneRel has one relationship
    HasMany   RelationshipType = RelationshipType(schema.HasMany)   // HasManyRel has many relationships
    BelongsTo RelationshipType = RelationshipType(schema.BelongsTo) // BelongsToRel belongs to relationship
    Many2Many RelationshipType = RelationshipType(schema.Many2Many) // Many2ManyRel many to many relationship
)

https://gorm.io/gen/associations.html

生成された構造体
user.go
package model

import (
	"time"
	"gorm.io/gorm"
)
const TableNameUser = "user"
type User struct {
	ID        int64          `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
	Name      string         `gorm:"column:name" json:"name"`
	Birthday  string         `gorm:"column:birthday" json:"birthday"`
	Address   string         `gorm:"column:address" json:"address"`
	CreatedAt time.Time      `gorm:"column:created_at;not null" json:"created_at"`
	UpdatedAt time.Time      `gorm:"column:updated_at" json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"column:deleted_at" json:"deleted_at"`
	Playlists []Playlists    `gorm:"foreignKey:UserID" json:"playlists"`
}

// TableName User's table name
func (*User) TableName() string {
	return TableNameUser
}
playlists.go
package model

import (
	"time"
	"gorm.io/gorm"
)

const TableNamePlaylists = "playlists"
type Playlists struct {
	ID        int64          `gorm:"column:id;primaryKey" json:"id"`
	UserID    int64          `gorm:"column:user_id;not null" json:"user_id"`
	Name      string         `gorm:"column:name" json:"name"`
	CreatedAt time.Time      `gorm:"column:created_at;not null" json:"created_at"`
	UpdatedAt time.Time      `gorm:"column:updated_at" json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"column:deleted_at" json:"deleted_at"`
	Songs     []Songs        `gorm:"foreignKey:PlaylistID" json:"songs"`
}

// TableName Playlists's table name
func (*Playlists) TableName() string {
	return TableNamePlaylists
}

songs.go
package model

import (
	"time"
	"gorm.io/gorm"
)

const TableNameSongs = "songs"
type Songs struct {
	ID          int64          `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
	PlaylistsID int64          `gorm:"column:playlists_id;not null" json:"playlists_id"`
	Title       string         `gorm:"column:title" json:"title"`
	Track       string         `gorm:"column:track" json:"track"`
	CreatedAt   time.Time      `gorm:"column:created_at;not null" json:"created_at"`
	UpdatedAt   time.Time      `gorm:"column:updated_at" json:"updated_at"`
	DeletedAt   gorm.DeletedAt `gorm:"column:deleted_at" json:"deleted_at"`
}

// TableName Songs's table name
func (*Songs) TableName() string {
	return TableNameSongs
}

おわり

gen.FieldRelateを使用することで、テーブル間の関連を明示的に定義することができます。
generator自体に設定を記載していくことで、手動で構造体を編集する必要がなくなり、ファイルをメンテする手間が減らせます。

レスキューナウテックブログ

Discussion