##目次
はじめに
・はじめに
CRUD Interface
・Goでよく使われるgormを理解する:Query編
Associations
・Goでよく使われるgormを理解する:Associations編
・Goでよく使われるgormを理解する:Preloading編
##Query
Queryを用いることで、データの抽出条件を細かく設定することができます。
###First
###Find
####FirstとFindの挙動の違い
ⅰ ) 対象のレコードが存在しない
前提となる構造体
type User struct {
Model
UserName *string `gorm:"" json:"userName"`
CompanyID *int `gorm:"" json:"companyId"`
Company *Company `gorm:"" json:"company"`
Languages []*Language `gorm:"many2many:user_languages;association_autoupdate:false" json:"language"`
UserBirthday *time.Time `gorm:"" json:"userBirthday"`
Bio *string `gorm:"" json:"bio"`
GithubAccount *string `gorm:"" json:"githubAccount"`
TwitterAccount *string `gorm:"" json:"twitterAccount"`
}
<Firstの場合>
Firstを用いた検索で対象のレコードが存在しない場合には、エラーになる。
func GetUser(UserID int64) (user User, err error) {
err = db.Set("gorm:auto_preload", true).First(&user, UserID).Error
return user, err
}
SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND ((`users`.`id` = 10)) ORDER BY `users`.`id` ASC LIMIT 1[] 0
※ DB に存在しない user_id = 10 のレコードを検索
Error: record not found
<Findの場合>
Findを用いた検索で対象のレコードが存在しない場合、空の配列([])を返す。
func GetAllUsers(limit int64, offset int64, companyID int64) (ml []*User, err error) {
tx := db.Begin()
if companyID != 0 {
tx = tx.Where("company_id = ?", companyID)
}
err = tx.Find(&ml).Commit().Error
return ml, err
}
SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND ((company_id = ?))[10] 0
※DB に存在しない company_id = 10 のレコードを検索
[]
###Where
gormのドキュメントには、AND検索について以下のような例が載っていますが、
// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
//// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;
gormの場合、Whereをつなげて記述したときのデフォルトがANDなので、以下のように記述することもできます。
db.Where("name = ?", jinzhu).Where("age >= ?", "22").Find(&users)
例えば、以下のようなstructがあったとします。
type User struct {
Model
UserName *string `gorm:"" json:"userName"` // ユーザー名
CompanyID *int `gorm:"" json:"companyId"` // 所属企業ID
Company *Company `gorm:"" json:"company"`
Languages []*Language `gorm:"many2many:user_languages;association_autoupdate:false" json:"language"` // 使用可能言語
UserBirthday *time.Time `gorm:"" json:"userBirthday"` // 生年月日
Bio *string `gorm:"" json:"bio"` // 自己紹介文
GithubAccount *string `gorm:"" json:"githubAccount"` // Githubアカウント
TwitterAccount *string `gorm:"" json:"twitterAccount"` // Twitterアカウント
}
ここで、DBに誕生日(UserBirthday)の登録があり(NULLではなく)、所属企業のID(company_id)が1のユーザーを探す場合、以下のように記述することができます。
func GetAllUsers() (ml []*User, err error) {
tx := db.Begin()
tx = tx.Where("user_birthday is not null").Where("company_id = ?", 1)
err = tx.Find(&ml).Commit().Error
return ml, err
}
SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND ((user_birthday is not null) AND (company_id = ?))[1] 1
gormのドキュメントに記載されている書き方だと、以下のようになりますが、ログを見ても基本的には同じSQLが発行されているのがわかります。
※前述の書き方だと、「user_birthday is not null」と「company_id = ?」がそれぞれ括弧で囲われており、以下の書き方だとその括弧がなくなっているという違いはありますが、括弧は処理のまとまりを表しているだけなので、おそらくパフォーマンス等への影響はないと思います。
func GetAllUsers(limit int64, offset int64) (ml []*User, err error) {
tx := db.Begin()
tx = tx.Where("user_birthday is not null AND company_id = ?", 1)
err = tx.Find(&ml).Commit().Error
return ml, err
}
SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND ((user_birthday is not null AND company_id = ?))[1] 1
###FirstOrInit
最初にマッチしたレコードを得る、または渡された条件 (構造体、マップの場合のみ機能) を元にレコードを作成します
例えば、以下のようなstructがあり、
type User struct {
Model
UserName *string `gorm:"" json:"userName"` // ユーザー名
}
Firstを使用して、DBに登録されていないIDを取得しようとすると、
func GetUser(UserID int64) (user User, err error) {
err = db.First(&user, UserID).Error
return user, err
}
該当するレコードがないため、
SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND ((`users`.`id` = 6)) ORDER BY `users`.`id` ASC LIMIT 1[] 0
通常は、以下のようなエラーが返ってきます。
// => record not found
しかし、FirstOrInit()
を使用すると、
func GetUser(UserID int64) (user User, err error) {
err = db.FirstOrInit(&user, UserID).Error
return user, err
}
該当するレコードがなくても、
SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND ((`users`.`id` = 6)) ORDER BY `users`.`id` ASC LIMIT 1[] 0
設定された内容を元に、仮のレコードを返してくれるみたいです。
※「設定された内容」と書きましたが、今回は設定をしていないので項目はすべてゼロ値になっています。「設定」については、次項のAttrsで行うことができます。
{
"id": 0,
"createdAt": "0001-01-01T00:00:00Z",
"updatedAt": "0001-01-01T00:00:00Z",
"deletedAt": null,
"userName": null,
"companyId": null,
"company": null,
"language": null,
"userBirthday": null,
"bio": null,
"githubAccount": null,
"twitterAccount": null
}
なお、gormのドキュメントには、「レコードが作成されます」と書いてありましたが、DBを確認したところレコード自体は作成されていませんでした。
###Attrs
レコードが取得できたかどうかによって初期化するかどうかが決まります
先ほどと同様、以下のようなstructがあったとします。
type User struct {
Model
UserName *string `gorm:"" json:"userName"` // ユーザー名
}
前項で使用したFirstOrInit()にAttrsの記述を追加して、以下のように書くと、
func GetUser(UserID int64) (user User, err error) {
userName := "No Name"
err = db.Attrs(User{UserName: &userName}).FirstOrInit(&user, UserID).Error
return user, err
}
該当するレコードがある場合は、DBのレコードを返しますが、
{
"id": 1,
"createdAt": "2020-06-01T20:32:19+09:00",
"updatedAt": "2020-06-01T20:32:19+09:00",
"deletedAt": null,
"userName": "user1"
}
SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND ((`users`.`id` = 1)) ORDER BY `users`.`id` ASC LIMIT 1[] 1
該当するレコードがない場合は、Attrsで設定した内容をvalueに含んだ仮のレコードを返します。
{
"id": 0,
"createdAt": "0001-01-01T00:00:00Z",
"updatedAt": "0001-01-01T00:00:00Z",
"deletedAt": null,
"userName": "No Name"
}
SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND ((`users`.`id` = 6)) ORDER BY `users`.`id` ASC LIMIT 1[] 0
###FirstOrCreate
最初にマッチしたレコードを得る、または渡された条件 (構造体、マップの場合のみ機能) を元にレコードを作成します。
gormのドキュメントの説明だけ見ると、FirstOrInitと何が違うの!?と思いますが(笑)、こちらは該当するレコードがない場合、正真正銘DBにレコードが作成されます。
func GetUser(UserID int64) (user User, err error) {
userName := "No Name"
err = db.Attrs(User{UserName: &userName}).FirstOrCreate(&user, UserID).Error
return user, err
}
{
"id": 6,
"createdAt": "2020-06-01T23:19:25.2557082+09:00",
"updatedAt": "2020-06-01T23:19:25.2557082+09:00",
"deletedAt": null,
"userName": "No Name",
"companyId": null,
"company": null,
"language": null,
"userBirthday": null,
"bio": null,
"githubAccount": null,
"twitterAccount": null
}
ログを見ると、たしかに、先ほどまでと同様のSELECT文に加え、INSERT文でAttrsで設定した内容を含むレコードが作成されていることがわかります。
SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND ((`users`.`id` = 6)) ORDER BY `users`.`id` ASC LIMIT 1[] 0
INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`user_name`,`company_id`,`user_birthday`,`bio`,`github_account`,`twitter_account`) VALUES (?,?,?,?,?,?,?,?,?)[2020-06-01 23:19:25.2557082 +0900 JST m=+16.576992801 2020-06-01 23:19:25.2557082 +0900 JST m=+16.576992801 <nil> 0xc0000204d0 <nil> <nil> <nil> <nil> <nil>] 1
##高度なクエリ
###SubQuery
SubQueryを使用することで、より複雑なデータの抽出を行うことができるようになります。
例えば、以下のようなstructがあり、Activity側からTourのIDに応じたActivityを抽出したいとします。
type Activity struct {
Model
ActivityName *string `gorm:"" json:"activityName"`
}
type ActivityPlan struct {
Model
ActivityPlanName *string `gorm:"" json:"activityPlanName"`
TourScheduleID *int `gorm:"" json:"tourScheduleId"`
ActivityID *int `gorm:"" json:"activityId"`
Activity *Activity `gorm:"" json:"activity"`
}
type TourSchedule struct {
Model
TourScheduleDate *time.Time `gorm:"" json:"tourScheduleDate"`
TourID *int `gorm:"" json:"tourId"`
ActivityPlans []*ActivityPlan `gorm:"" json:"activityPlan"`
}
type Tour struct {
Model
TourName *string `gorm:"" json:"tourName"`
TourSchedules []*TourSchedule `gorm:"" json:"tourSchedule"`
}
先ほどまでのものに比べると、抽出の仕方が少々複雑な気がしますね。
TourScheduleはTourと一対多の関係にあり、ActivityPlanはTourScheduleと一対多の関係にあります。また、今回Getのfunctionを記述したいActivityのstructは直接TourIDを持っておらず、TourScheduleのstructにあるTourIDをうまく条件に組み込む必要がありそうです。
例えば、以下のような形はどうでしょう。
func GetAllActivities(tourID int64) (ml []*Activity, err error) {
tx := db.Begin()
// ツアーIDに紐づくActivityを抽出
tx = tx.Where("id in (?)",
tx.Table("activity_plans").
Select("distinct(activity_id)").
Joins("left join tour_schedules on activity_plans.tour_schedule_id = tour_schedules.id").
Where("tour_schedules.tour_id = ?", tourID).
SubQuery())
err = tx.Find(&ml).Commit().Error
return ml, err
}
すると、以下のようなデータが出力されました。
[
{
"id": 7,
"createdAt": "2020-06-01T23:38:47+09:00",
"updatedAt": "2020-06-01T23:38:47+09:00",
"deletedAt": null,
"activityName": "アクティビティG"
},
{
"id": 8,
"createdAt": "2020-06-01T23:38:47+09:00",
"updatedAt": "2020-06-01T23:38:47+09:00",
"deletedAt": null,
"activityName": "アクティビティH"
},
{
"id": 9,
"createdAt": "2020-06-01T23:38:47+09:00",
"updatedAt": "2020-06-01T23:38:47+09:00",
"deletedAt": null,
"activityName": "アクティビティI"
},
{
"id": 10,
"createdAt": "2020-06-01T23:38:47+09:00",
"updatedAt": "2020-06-01T23:38:47+09:00",
"deletedAt": null,
"activityName": "アクティビティJ"
}
]
ログを見ると、activity_plansテーブルとtour_schedulesテーブルをleft joinし、tour_schedulesテーブルのtour_idが1になるレコードを探してきていますね。また、重複しない(distinct)ようにactivity_plansテーブルのactivity_idを抽出し、そのactivity_id郡と一致するactivitiesのレコードを抽出できているので、目的通りの抽出ができていそうです。
このように、SubQueryを使用することで、複雑な抽出条件の設定が可能になります。
SELECT * FROM `activities` WHERE `activities`.`deleted_at` IS NULL AND ((id in ((SELECT distinct(activity_id) FROM `activity_plans` left join tour_schedules on activity_plans.tour_schedule_id = tour_schedules.id WHERE (tour_schedules.tour_id = ?)))))[1] 4