onemuri.space

gorm で Many to Many を扱う


golang の orm 使うとしたら多くの場合は Gorm ですが、公式ドキュメントでは Associations の挙動がいまいち把握できないことがあったのでサンプルコードとともに理解してみようと思います。

前提の設定

今回は Association を表現する設定として、「本と著者」、「本と出版社」の関係を扱います。

  • 本は複数の著者を持ち、著者は複数の本を持つ
  • 本は一つの出版社を持ち、出版社は複数の本を持つ

です。これを gorm の tag とともに構造体で表現してみます。

type Book struct {
	ID          uint   `gorm:"primary_key;AUTO_INCREMENT;not null;"`
	Title       string `gorm:"not null"`
	PublisherID uint   `gorm:"not null"`
	Publisher   Publisher
	Authors     []Author `gorm:"many2many:author_books"`
}

type Author struct {
	ID    uint   `gorm:"primary_key;AUTO_INCREMENT;not null;"`
	Name  string `gorm:"not null"`
	Books []Book `gorm:"many2many:author_books"`
}

type Publisher struct {
	ID    uint   `gorm:"primary_key;AUTO_INCREMENT;not null;"`
	Name  string `gorm:"not null"`
	Books []Book
}

(many2many) 本は複数の著者を持ち、著者は複数の本を持つ

シンプルに表すなら以下で十分です。 many2many のタグ を設定することで table が作成され、association を設定することができます。

また、型を slice で定義します。

type Book struct {
    ID      uint     `gorm:"primary_key;AUTO_INCREMENT;not null;"`
    Authors []Author `gorm:"many2many:author_books"`
}
type Author struct {
    ID    uint   `gorm:"primary_key;AUTO_INCREMENT;not null;"`
    Books []Book `gorm:"many2many:author_books"`
}

(has one, has many) 本は一つの出版社を持ち、出版社は複数の本を持つ

シンプルに表現するなら以下のようになります。

Book は 外部キーとして PublisherID を持ち、 Publisher を型として取ります(has one)。

Publisher は 複数の Book を持つ、ことを表現するために slice で定義してください(has many)。もし、外部キーを変えたい場合には、foreignkey tag を使うようにしてください。

type Book struct {
	ID          uint   `gorm:"primary_key;AUTO_INCREMENT;not null;"`
	PublisherID uint   `gorm:"not null"`
	Publisher   Publisher
}
type Publisher struct {
	ID    uint   `gorm:"primary_key;AUTO_INCREMENT;not null;"`
	Books []Book
}

migration

さて、前提の構造体と gorm tag の設定が完了しましたので、migrateしてみましょう。gorm の場合は、 AutoMigrate() が使えます。

func migrate(db *gorm.DB) error {
	// 外部キーも設定すべきですが無視します
	if err := db.AutoMigrate(&Book{}).
		AutoMigrate(&Author{}).
		AutoMigrate(&Publisher{}).
		Error; err != nil {
		return err
	}
	return nil
}

seeding

migration が完了したので、次に seed data 作成に参りましょう。ここでも Association 設定の恩恵を受けることにしましょう。

const (
	publisherName = "test-publisher"
	authorName1   = "test-author-1"
	authorName2   = "test-author-2"
	BookTitle1    = "test-book-1"
	BookTitle2    = "test-book-2"
)

func seeds(db *gorm.DB) error {
	if !db.First(&Publisher{Name: publisherName}).RecordNotFound() {
		return nil
	}

	publisher := Publisher{Name: publisherName}
	if err := db.Create(&publisher).Error; err != nil {
		return err
	}

	author1 := Author{Name: authorName1}
	author2 := Author{Name: authorName2}
	if err := db.Create(&author1).Create(&author2).Error; err != nil {
		return err
	}

	// book を作成 & 中間テーブルに作成
	book1 := Book{Title: BookTitle1, PublisherID: publisher.ID}
	book2 := Book{Title: BookTitle2, PublisherID: publisher.ID}
	if err := db.Model(&author1).Association("Books").Append(&book1).Append(&book2).Error; err != nil {
		return err
	}
	if err := db.Model(&author2).Association("Books").Append(&book2).Error; err != nil {
		return err
	}
	return nil
}

データを作るときには Create() を使いますが、many2many の場合には、 Association() でモデルを関連づけて、 Append() します。

これで、book1 のレコードを作るとともに、 author_books テーブルにもデータを追加してくれます。

db.Model(&author1).Association("Books").Append(&book1).Append(&book2)

ただ、 gorm は薄い orm なので、これぐらいのエコシステムなら自分で中間テーブルに追加しても良いと思います。

preload と find

前提を少しおさらいします。

  • 本は複数の著者を持ち、著者は複数の本を持つ
  • 本は一つの出版社を持ち、出版社は複数の本を持つ

でしたね。まずは本が複数の著者を持ち、一つの出版社を持つことを Find してみましょう。Preload に Association を設定した構造体のフィールド名を指定します。

複数の著者を Preload("Authors") で、一つの出版社を Preload("Publisher")) で表現します。

func getBook(db *gorm.DB) error {
	var books []Book
	if err := db.Preload("Publisher").Preload("Authors").Find(&books).Error; err != nil {
		return err
	}
	return nil
}

次に、著者は複数の本を持つことを Preload("Books") で表現します。

func getAuthor(db *gorm.DB) error {
	var authors []Author
	if err := db.Preload("Books").Find(&authors).Error; err != nil {
		return nil
	}
	return nil
}

最後に、編集者は複数の本を持つことを Preload("Books") で表現します。

func getPublisher(db *gorm.DB) error {
	var publishers []Publisher
	if err := db.Preload("Books").Find(&publishers).Error; err != nil {
		return err
	}
	return nil
}

ここまでで、一通り最低限の Association の機能を使うことができるようになっています。

サンプル

docker-compose 付きで手元で簡単に試せるようにしておいたのでとりあえず挙動が見たい人は参考にしてみてください。

https://github.com/mergitto/gorm-association-example

以下のコマンドで一通り実行できます。あとはDBでデータを確認してみてください。

git clone git@github.com:mergitto/gorm-association-example.git
cd gorm-association-example/docker
docker-compose up -d
cd ../
go run main.go

今回はコード上では外部キーが設定してありますが、 AddForeignKey していないので DB上では foreign_key は作られていません。そこら辺は今回の範囲外にしてシンプルにしたかったのでいったん除外してます。

まとめ

Gorm 初めて扱う前に読んだ方は、 association に限らず gorm が最低限使える状態になっていると思います。ぜひ、 golang でも orm 使うこなしていけるようにいろいろ試していきましょう!

自己紹介用画像

Riki Akagi

2019年からDeNAで働いています。GCP(CloudSQL・GAE・Cloud Function etc)とGoでAPI開発に勤んでいます。睡眠やエンジニアリングに関することに興味を持って過ごしているのでその情報を皆さんに共有していけたらなと思っています。

自己紹介の詳細