sqlmockでEager loadingを利用したコードのテストを書く際の注意事項

GORMのPreload()メソッドを使いEager loadingする処理を実装しました。対象処理のテストコード実装時の注意点をメモします。

Eager loadingについて

Eager loadingは「関連データを事前に読み込む」機能で、データベースに対する問い合わせの数(クエリ数)を減らすことでパフォーマンスを改善するための手法です。

「関連データ」というのは、たとえば下記のようなFKで繋がっているリレーションシップを持つテーブルに格納されているデータのことを指します。

CREATE TABLE `articles` (
  `id` int PRIMARY KEY AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `content` text NOT NULL,
  `published_at` datetime NOT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  `deleted_at` datetime
);

CREATE TABLE `draft_articles` (
  `id` int PRIMARY KEY AUTO_INCREMENT,
  `article_id` int NOT NULL UNIQUE,
  `title` varchar(255) NOT NULL,
  `content` text NOT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL
);

CREATE TABLE `tags` (
  `id` int PRIMARY KEY AUTO_INCREMENT,
  `name` varchar(10) NOT NULL
);

CREATE TABLE `articles_tags` (
  `id` int PRIMARY KEY AUTO_INCREMENT,
  `article_id` int NOT NULL,
  `tag_id` int NOT NULL,
  UNIQUE KEY `articles_tags_idx_00` (`article_id`, `tag_id`),
  CONSTRAINT `articles_tags_article_id_fk` FOREIGN KEY (`article_id`) REFERENCES `articles` (`id`) ON DELETE CASCADE,
  CONSTRAINT `articles_tags_tag_id_fk` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
);

Eager loadingを行うと、それぞれの関連データに対して個別のSELECTクエリが発行されます。各関連データを一つのSELECTクエリでまとめて読み込むことは可能ですが、それは"JOIN"と呼ばれる別の操作となります。

実際に上記のテーブルに対しGORMのPreload()メソッドを使ってEager loadingを行うと、下記のようなSELECT文が発行されます。

// Go側の実装
query := tx.Preload(clause.Associations).
	Where("articles.id = ? AND deleted_at IS NULL", id).
	Where("articles.published_at IS NOT NULL").
	Take(&article)
-- 発行されるSQL
SELECT * FROM `articles` WHERE (articles.id = ? AND deleted_at IS NULL) AND articles.published_at IS NOT NULL LIMIT 1

SELECT * FROM `draft_articles` WHERE `draft_articles`.`article_id` = ?

SELECT * FROM `articles_tags` WHERE `articles_tags`.`article_id` = ?

SELECT * FROM `tags` WHERE `tags`.`id` IN (?,?)

Eager LoadingはN+1問題の解消やWebアプリケーションのパフォーマンス向上のために用いられます。ただし、必要以上に多くのデータをEager Loadすると、必要ないデータまで読み込んでしまうため逆にパフォーマンスに影響を与える可能性もあります。そのため、どのデータをEager Loadするかは慎重に選ぶべきです。

sqlmockについて

sqlmockはDBをモック化してテストコードを実装できるようにするためのGoのパッケージです。

発行されるSQLと期待値(SQLの実行結果や期待するエラー)を評価する仕組みになっており、DBに接続せずにテストすることが可能になります。

CIのパフォーマンスが悪化しないなどの利点がある一方、テストDBを用意して実際にQueryを流す場合と比べて精度が若干劣ります。

Eager loadingを行う処理のテストコードを書く際に留意すること

発行されすすべてのクエリを評価すること

sqlmockの仕組み上、発行されるすべてのクエリを正しい順番、内容で実行されているか評価する必要があります。

上記の例の場合だと4つのクエリが発行しているので、4つのmock.ExpectQuery()を用意する必要があります。

-- 期待されるクエリすべてにmock.ExpectQuery()を書く
SELECT * FROM `articles` WHERE (articles.id = ? AND deleted_at IS NULL) AND articles.published_at IS NOT NULL LIMIT 1

SELECT * FROM `draft_articles` WHERE `draft_articles`.`article_id` = ?

SELECT * FROM `articles_tags` WHERE `articles_tags`.`article_id` = ?

SELECT * FROM `tags` WHERE `tags`.`id` IN (?,?)

そもそもEager loadingが適しているかどうか考えること

そもそもJOINしたほうが速い可能性があったりします。

発行されるクエリが多いとテストコードを書くのも大変なので、Eager loadingを使わずに済のであれば、それにこしたことはないです。

まとめ

最近あまりアウトプットできてなかったので意識して知見をだしていこう。