11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Goでよく使われるgormを理解する:Query編

Last updated at Posted at 2020-06-01

##目次
はじめに
はじめに

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
11
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?