お題
前回に引き続き、今回は主にリレーション関係にトライ。
やることはシンプル。↓にあるリレーションのパターンを実際に試して、生成されるテーブル定義を確認する。
http://doc.gorm.io/associations.html
開発環境
# OS
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="17.10 (Artful Aardvark)"
# Golang
$ go version
go version go1.11.2 linux/amd64
バージョンの切り替えはgoenvで行っている。
# Gorm
Version: v1.9.2
実践
各試行共通の準備
RDBはMySQLを対象とする。
ローカルで↓のようなYamlを作っておく。
(dockerとdocker-composeは当然インストール済)
version: '3'
services:
db:
image: mysql:5.7.24
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_USER: testuser
MYSQL_PASSWORD: testpass
MYSQL_DATABASE: testdb
起動。
$ sudo docker-compose up
試しにコンテナ内のDBに接続。
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
831188d7e688 mysql:5.7.24 "docker-entrypoint.s…" 5 minutes ago Up 5 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp gorm_db_1_46a022cef614
$
$ sudo docker exec -it gorm_db_1_46a022cef614 bash
root@831188d7e688:/# mysql -utestuser -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.24 MySQL Community Server (GPL)
〜〜省略〜〜
mysql>
■Belongs To(gorm.Model使用版)
ソース説明
↓に記載のまま構造体を定義。
http://doc.gorm.io/associations.html#belongs-to
// `User` belongs to `Profile`, `ProfileID` is the foreign key
type User struct {
gorm.Model
Profile Profile
ProfileID int
}
type Profile struct {
gorm.Model
Name string
}
両テーブルのマイグレーションと動作確認用にレコード登録。そして、登録内容確認。
func main() {
db, err := gorm.Open("mysql", "testuser:testpass@tcp(127.0.0.1:3306)/testdb?charset=utf8&parseTime=True&loc=Local")
if err != nil {
panic(err)
}
defer db.Close()
db.AutoMigrate(&User{}, &Profile{})
db.Create(&User{Profile: Profile{Name: "dummy"}})
profile := Profile{}
profile.ID = 1
user := User{Profile: profile}
user.ID = 1
user.ProfileID = 1
db.Model(&user).Related(&profile)
fmt.Printf("user: {ID: %v, ProfileID: %v, Profile: {ID: %v, Name: %v}}\n", user.ID, user.ProfileID, user.Profile.ID, user.Profile.Name)
}
実行
$ go run main.go
user: {ID: 1, ProfileID: 1, Profile: {ID: 1, Name: }}
う〜ん、参照サイトにはdb.Model(&user).Related(&profile)
の説明として「SELECT * FROM profiles WHERE id = 111; // 111 is user's foreign key ProfileID」とあったのだけど、ID=1
で登録されたレコードのName="dummy"
が入っていない?
テーブル定義確認
mysql> show create table users;
+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| users | CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
`profile_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_users_deleted_at` (`deleted_at`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1 |
+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql>
mysql> show create table profiles;
+----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| profiles | CREATE TABLE `profiles` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_profiles_deleted_at` (`deleted_at`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1 |
+----------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
自動で外部キー貼ってくれるのかと勘違いした。貼らないんだね。
登録レコード確認
mysql> select * from users;
+----+---------------------+---------------------+------------+------------+
| id | created_at | updated_at | deleted_at | profile_id |
+----+---------------------+---------------------+------------+------------+
| 1 | 2018-12-16 01:32:26 | 2018-12-16 01:32:26 | NULL | 1 |
+----+---------------------+---------------------+------------+------------+
1 row in set (0.00 sec)
mysql> select * from profiles;
+----+---------------------+---------------------+------------+-------+
| id | created_at | updated_at | deleted_at | name |
+----+---------------------+---------------------+------------+-------+
| 1 | 2018-12-16 01:32:26 | 2018-12-16 01:32:26 | NULL | dummy |
+----+---------------------+---------------------+------------+-------+
1 row in set (0.00 sec)
db.Create(&User{Profile: Profile{Name: "dummy"}})
によって↑の2テーブルにレコードが入ったので、そっちの方が驚いた。
構造体「User
」及び「Profile
」は共にgorm.Model
を持たせているのでid
という名のPKは自動生成される仕組みになっている。
なので、それぞれのテーブルに自動採番されたid
値を持つレコードができることは納得。
構造体「User
」には構造体「Profile
」の他にProfileID
という名のプロパティも持たせている。
どうやら、これによって、users
テーブルのprofile_id
カラムがprofiles
テーブルのid
カラムの外部キーであることを意味するようになるらしい。(結果から、そのように読み取れるというだけでソースの裏付けは取れていない・・・)
命名ルールベースの仕掛けのようだけど、ちょっと難しいね。
■Belongs To(独自モデル使用版)
ソース説明
↓を参考に構造体を定義。
http://doc.gorm.io/associations.html#belongs-to
type User struct {
ID string `gorm:primary_key`
Profile Profile `gorm:"foreignkey:ProfileID;association_foreignkey:Refer"`
ProfileID string
}
type Profile struct {
ID string `gorm:primary_key`
Refer string
Name string
}
両テーブルのマイグレーションと動作確認用にレコード登録。そして、登録内容確認。
func main() {
db, err := gorm.Open("mysql", "testuser:testpass@tcp(127.0.0.1:3306)/testdb?charset=utf8&parseTime=True&loc=Local")
if err != nil {
panic(err)
}
defer db.Close()
db.AutoMigrate(&User{}, &Profile{})
db.Create(
&User{
ID: "1",
Profile: Profile{
ID: "1",
Refer: "1",
Name: "dummy",
},
ProfileID: "1",
},
)
profile := Profile{}
profile.ID = "1"
user := User{Profile: profile}
user.ID = "1"
user.ProfileID = "1"
db.Model(&user).Related(&profile)
fmt.Printf("user: {ID: %v, ProfileID: %v, Profile: {ID: %v, Name: %v}}\n", user.ID, user.ProfileID, user.Profile.ID, user.Profile.Name)
}
実行
$ go run main.go
user: {ID: 1, ProfileID: 1, Profile: {ID: 1, Name: }}
やはり、db.Model(&user).Related(&profile)
ではName="dummy"
は取れない?
テーブル定義確認
mysql> show create table users;
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------+
| users | CREATE TABLE `users` (
`id` varchar(255) NOT NULL,
`profile_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql>
mysql> show create table profiles;
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| profiles | CREATE TABLE `profiles` (
`id` varchar(255) NOT NULL,
`refer` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
やはり、外部キーは貼られない。
登録レコード確認
mysql> select * from users;
+----+------------+
| id | profile_id |
+----+------------+
| 1 | 1 |
+----+------------+
1 row in set (0.00 sec)
mysql> select * from profiles;
+----+-------+-------+
| id | refer | name |
+----+-------+-------+
| 1 | 1 | dummy |
+----+-------+-------+
1 row in set (0.00 sec)
mysql> select u.*, p.* from users u, profiles p where u.profile_id = p.id;
+----+------------+----+-------+-------+
| id | profile_id | id | refer | name |
+----+------------+----+-------+-------+
| 1 | 1 | 1 | 1 | dummy |
+----+------------+----+-------+-------+
1 row in set (0.00 sec)
SQL文でJOINすれば両テーブルの全カラムが取れるんだけど。
複数テーブルをJOINしてレコード取得
上述のusers
テーブルとprofiles
テーブルから内部結合して以下のカラムを取得する。
-
users
のid
-
users
のprofile_id
(=profiles
のid
) -
profiles
のname
内部結合用のメソッドがGormに用意してあるものの、タイプセーフな使い方ができない様子。
まず、取得対象のカラムを持つ構造体を定義(まあ、これは必須ではなさそうだけど)して、
type Result struct {
ID string
ProfileID string
Name string
}
こんなふうにベースのテーブル、結合先テーブル(及び結合カラム)、取得カラムを指示してみると、
r := &Result{}
db.Table("users").
Select("users.id, users.profile_id, profiles.name").
Joins("inner join profiles on profiles.id = users.profile_id").
First(r)
fmt.Printf("%#v\n", r)
このように出力される。
&main.Result{ID:"1", ProfileID:"1", Name:"dummy"}
タイプセーフなままJOINする方法あるのかな・・・?
■Has One(独自モデル使用版)
正直なところ、Belongs Toとの違いがわからない・・・。
ソース説明
↓を参考に構造体を定義。
http://doc.gorm.io/associations.html#has-one
type User struct {
ID string `gorm:primary_key`
CreditCardID string
CreditCard CreditCard `gorm:"foreignkey:ID;association_foreignkey:CreditCardID"`
}
type CreditCard struct {
ID string `gorm:primary_key`
Number string
}
構造体「User
」はCreditCardID
を外部キーに構造体「CreditCard
」を持つ関係。
オートマイグレーションと2テーブルへのレコード登録ロジックは下記。
db.AutoMigrate(&User{}, &CreditCard{})
db.Create(
&User{
ID: "u1",
CreditCardID: "c1",
CreditCard: CreditCard{
ID: "c1",
Number: "xxxx-xxxx-xxxx-xxxx",
},
},
)
実行後のテーブル定義確認
このように、やはり、タグに「foreignkey:ID
」と付けようと、オートマイグレーションで外部キーは貼られない。
mysql> show create table users;
+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
| users | CREATE TABLE `users` (
`id` varchar(255) NOT NULL,
`credit_card_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> show create table credit_cards;
+--------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+--------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+
| credit_cards | CREATE TABLE `credit_cards` (
`id` varchar(255) NOT NULL,
`number` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+--------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
実行後の登録レコード確認
mysql> select * from users;
+----+----------------+
| id | credit_card_id |
+----+----------------+
| u1 | c1 |
+----+----------------+
1 row in set (0.01 sec)
mysql> select * from credit_cards;
+----+---------------------+
| id | number |
+----+---------------------+
| c1 | xxxx-xxxx-xxxx-xxxx |
+----+---------------------+
1 row in set (0.01 sec)
プログラムでの登録レコード取得確認
Select文の受け取り口として、
type Result struct {
ID string
CreditCardID string
Number string
}
を定義して、
r := &Result{}
db.Table("users").
Select("users.id, users.credit_card_id, credit_cards.number").
Joins("inner join credit_cards on credit_cards.id = users.credit_card_id").
First(r)
fmt.Printf("%#v\n", r)
これで出力。すると、
$ go run main.go
&main.Result{ID:"u1", CreditCardID:"c1", Number:"xxxx-xxxx-xxxx-xxxx"}
「users
」テーブルと「credit_cards
」テーブルを内部結合してのレコード取得結果が取れる。
■Has Many
ソース説明
↓を参考に構造体を定義。
http://doc.gorm.io/associations.html#has-many
type Organization struct {
ID string `gorm:primary_key`
Name string
Users []User `gorm:"foreignkey:OrganizationID"`
}
type User struct {
ID string `gorm:primary_key`
Name string
OrganizationID string
}
「組織」は複数の「ユーザ」を持つ。「ユーザ」は1つの「組織」に属するという関係。
で、↓のようにして2つの「組織」レコード(及び所属する「ユーザ」レコード)を登録する。
(一応、トランザクション貼る。)
func main() {
db, err := gorm.Open("mysql", "testuser:testpass@tcp(127.0.0.1:3306)/testdb?charset=utf8&parseTime=True&loc=Local")
if err != nil {
panic(err)
}
defer db.Close()
db.LogMode(true)
db.AutoMigrate(&User{}, &Organization{})
ts := db.Begin()
defer ts.Commit()
ts.Create(
&Organization{
ID: "o01",
Name: "SosikiA",
Users: []User{
{ID: "u01", Name: "Sato", OrganizationID: "o01"},
{ID: "u02", Name: "Takahashi", OrganizationID: "o01"},
},
},
)
ts.Create(
&Organization{
ID: "o02",
Name: "SosikiB",
Users: []User{
{ID: "u03", Name: "Kato", OrganizationID: "o02"},
{ID: "u04", Name: "Ishida", OrganizationID: "o02"},
{ID: "u05", Name: "Niwa", OrganizationID: "o02"},
},
},
)
if err := ts.Error; err != nil {
ts.Rollback()
}
}
実行後のテーブル定義確認
やはり、外部キーは貼られない。
mysql> show create table organizations;
+---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------+
| organizations | CREATE TABLE `organizations` (
`id` varchar(255) NOT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> show create table users;
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| users | CREATE TABLE `users` (
`id` varchar(255) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`organization_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
実行後の登録レコード確認
mysql> select * from organizations;
+-----+---------+
| id | name |
+-----+---------+
| o01 | SosikiA |
| o02 | SosikiB |
+-----+---------+
2 rows in set (0.00 sec)
mysql> select * from users;
+-----+-----------+-----------------+
| id | name | organization_id |
+-----+-----------+-----------------+
| u01 | Sato | o01 |
| u02 | Takahashi | o01 |
| u03 | Kato | o02 |
| u04 | Ishida | o02 |
| u05 | Niwa | o02 |
+-----+-----------+-----------------+
5 rows in set (0.01 sec)
プログラムでの登録レコード取得確認
Select文の受け取り口として、
type ResOrgUser struct {
OrganizationID string
OrganizationName string
UserID string
UserName string
}
を定義して、↓のようにして実行。
var r []ResOrgUser
db.Table("organizations").
Select(
"organizations.id AS organization_id, " +
"organizations.name AS organization_name, " +
"users.id AS user_id, " +
"users.name AS user_name").
Joins("inner join users on users.organization_id = organizations.id").
Find(&r)
for idx, res := range r {
fmt.Printf("[%d]%#v\n", idx, res)
}
↓の塩梅でちゃんとレコード取得できることが確認できた。
$ go run main.go
[0]main.ResOrgUser{OrganizationID:"o01", OrganizationName:"SosikiA", UserID:"u01", UserName:"Sato"}
[1]main.ResOrgUser{OrganizationID:"o01", OrganizationName:"SosikiA", UserID:"u02", UserName:"Takahashi"}
[2]main.ResOrgUser{OrganizationID:"o02", OrganizationName:"SosikiB", UserID:"u03", UserName:"Kato"}
[3]main.ResOrgUser{OrganizationID:"o02", OrganizationName:"SosikiB", UserID:"u04", UserName:"Ishida"}
[4]main.ResOrgUser{OrganizationID:"o02", OrganizationName:"SosikiB", UserID:"u05", UserName:"Niwa"}
■Many To Many
紐付けテーブルを自動生成する仕掛け。
タグに仕込んだキーワード「many2many
」により、その先で指示した名前を持つ紐付けテーブルも自動生成する。
ソース説明
↓を参考に構造体を定義。
http://doc.gorm.io/associations.html#many-to-many
type Organization struct {
ID string `gorm:primary_key`
Name string
Users []User `gorm:"many2many:organizations_users"`
}
type User struct {
ID string `gorm:primary_key`
Name string
Organizations []Organization `gorm:"many2many:organizations_users"`
}
↑こんなふうに定義してオートマイグレーションすると...
実行後のテーブル定義確認
mysql> show tables;
+---------------------+
| Tables_in_testdb |
+---------------------+
| organizations |
| organizations_users |
| users |
+---------------------+
3 rows in set (0.00 sec)
構造体とマッピングされる「organizations
」や「users
」とは別にタグで名前を指定した「organizations_users
」も生成されている。
そして、定義は。。。
mysql> desc organizations;
+-------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| id | varchar(255) | NO | PRI | NULL | |
| name | varchar(255) | YES | | NULL | |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)
mysql> desc users;
+-------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| id | varchar(255) | NO | PRI | NULL | |
| name | varchar(255) | YES | | NULL | |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)
mysql> desc organizations_users;
+-----------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+--------------+------+-----+---------+-------+
| user_id | varchar(255) | NO | PRI | NULL | |
| organization_id | varchar(255) | NO | PRI | NULL | |
+-----------------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)
なるほど。これは、いいかも。
まとめ
個人的には、タグでforeignkey
と貼ってもオートマイグレーションでは実テーブル定義に反映されない点はイマイチ。
あと、INSERT時は構造体間の関係性によって複数テーブルの登録を一挙に行えるのはよいけど、複数テーブルからのレコード取得時にタイプセーフな取り方ができない(自分が知らないだけ?)のは難点。
これは自分がこのORMを使いこなせていない(そもそもORMより生SQL使いたい派)のが原因なんだけど、難しいね、ORM。