Go言語で簡単にDBアクセスを実現!GORMを使ってORMを体験しよう-Create編(2)

はじめに

以下の 2 つのエントリで Go 言語で利用可能な O/R マッパーである GORM の概要、導入方法、基本的な使い方とデータの登録方法について学びました。

今回は前回のデータ登録 Create で説明しきれなかった登録機能とアソシエーション(テーブル同士の関連)の設定されているデータの登録方法を学びます。

また、 default キーワードを使ってカラムを作成したときにハマるケースについても説明します。


「Belongs To」アソシエーションと関連モデルの登録方法

belongs to アソシエーションは1対1のモデル関係を表現する際に使用されます。

特に、一方のモデルがもう一方に「属する」 ( 依存している ) 関係を表現します。

例をあげます。

作成中のアプリケーションに users と companies という 2 つのモデルがあるとします。 users モデルは必ず companies テーブルのレコードに依存しているとします。

このような状況で users テーブルと companies テーブルにデータを登録する処理を、 GORM のコードで記述してみます。 ( データベース環境の用意を省略するために、今回は SQLite を使用しています )

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// companies テーブルと対になる
type Company struct {
	gorm.Model
	Name string
}

// users テーブルと対になる
type User struct {
	gorm.Model
	Name      string
	CompanyID uint
	Company   Company
}

func main() {
	// DBに接続
	db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

	// DBマイグレーション(テーブルを作成)
	db.AutoMigrate(&Company{})
	db.AutoMigrate(&User{})

	// (1) users レコードと companies レコードを同時に作成する
	db.Create(&User{
		Name:    "taro",
		Company: Company{Name: "ninten-done"},
	})

	// (2) users レコードと companies レコードを別々に作成する
	saga := Company{
		Name: "saga",
	}
	db.Create(&saga)

	// 事前に作成した companies レコードを使って users レコードを作成する
	db.Create(&[]User{
		// モデルを指定して作成
		User{
			Name:    "jiro",
			Company: saga,
		},
		User{
			Name:    "saburo",
			Company: saga,
		},
		// モデルのIDを指定して作成
		User{
			Name:      "shiro",
			CompanyID: saga.ID,
		},
	})
}

User オブジェクト(構造体だがドキュメントには Object と書かれているのでこの文言を使用します) は所属するモデルをフィールドに保つ必要があります。

そのため Company を設定しますが、加えて CompanyID も必要となります。 CompanyID フィールドは暗黙的に外部キーとして使用されます。

登録された users テーブルと companies テーブルをまとめて取得したい場合は、クエリ実行の際に DB#Preload() あるいは DB#Joins() メソッドを呼び出します。

このあたりのメソッドもよくある O/R マッパー同様の機能となります。

  • DB#Preload() : アソシエーションが設定されているテーブルを複数回の SQL 発行で取得します(引数に指定されたモデル数分)
  • DB#Joins() : アソシエーションが設定されているテーブルを1回の SQL 発行で取得する
1
2
3
4
5
6
	// すべてのユーザー情報を取得
	var users []User
	db.Preload("Company").Find(&users)
	for _, user := range users {
		println(user.Name, user.Company.Name)
	}

User オブジェクトに設定されているアソシエーションである Company を無視させたい場合には、 Omit() メソッドにモデル名を指定しスキップさせることもできます。

1
2
3
4
5
6
7
8
	// Company が設定されているが company_id は null となる
	db.Omit("Company").Create(&User{
		Name: "ichiro",
		Company: Company{
			Name: "SHIKAKU",
		},
	})
}

デフォルト値 ( default )

構造体フィールドのタグに default キーワードを指定して、フィールドの値が “ゼロ値” だった時の値を強制できます。 ( ちなみに構造体の名前はパッケージ外から参照しない場合は小文字から初めて問題ありません )

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type user struct {
	gorm.Model
	Name string `gorm:"default:'名無しさん'"`
	Age  uint   `gorm:"default:999"`
}

func main() {
	// DBに接続
	db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

	db.AutoMigrate(&user{})

	db.Create(&user{Name: "Taro", Age: 20})
	db.Create(&user{Age: 30})
	db.Create(&user{Name: "Jiro"})
	db.Create(&user{Name: "", Age: 0})

	var users []user
	db.Find(&users)
	for _, user := range users {
		println(user.Name, user.Age) // Taro 20, 名無しさん 30, Jiro 999, 名無しさん 999
	}
}

ゼロ値 については注意が必要です。

今回の例では、 string のゼロ値である空文字と、 uint のゼロ値である 0 を指定した場合に default の設定が機能していますが、値に "" や 0 が想定されるような場合に注意しましょう。

特にハマりやすいのは bool 型のフィールドに default タグを設定した場合です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
	"fmt"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	Name    string
	Deleted bool `gorm:"default:true"`
}

func main() {
	// DBに接続
	db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

	db.AutoMigrate(&User{})

	user := User{Name: "Taro", Deleted: false}

	db.Create(&user)

	fmt.Printf("Name: %s, Deleted: %t", user.Name, user.Deleted) // Name: Taro, Deleted: true
}

Deleted フィールドには false をセットしているにも関わらず、最新の情報は true となっています。

bool フィールドのゼロ値が false のため、 default の設定値が利用されています。

これを回避するために sql.NullBool という値を利用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
	"database/sql"
	"fmt"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	Name    string
	Deleted sql.NullBool `gorm:"default:true"`
}

func main() {
	// DBに接続
	db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

	db.AutoMigrate(&User{})

	user := User{Name: "Taro", Deleted: sql.NullBool{Bool: false, Valid: true}}

	db.Create(&user)

	fmt.Printf("Name: %s, Deleted: %t", user.Name, user.Deleted) // Name: Taro, Deleted: {false true}
}

sql.NullBool は構造体となっており、 Bool フィールドに値を保持しています。 NULL の場合には Valid フィールドに false をセットします。 (したがって、 Valid が true の場合には Bool フィールドの値は意味がありません )

今回は false が登録できました。

ひとこと

またまた思ったよりもボリュームがあり説明しきれませんでした。 次のエントリに続きます。