Rista Tech Blog

株式会社リスタの技術?ブログ

[Rails] default_scopeを使ったせいで泣きを見たクレイジーな困難たちを紹介するぜ!

はい、辻(@dim0627)です。

最近はVimを8にしたせいでなんか調子悪くなってどったんばったん大騒ぎしてます。 2期、うまくいくといいですね。

さて、今日はevilと言われるdefault_scopeのことを書きます。

Railsのdefault_scopeは本当にevilなのか?

f:id:dim0627:20171010085930j:plain

まあrails_best_practicesを使ってると怒られますよね、弊社でも導入してますし、default_scopeは使ってません。 有名なのはこの記事ですかね。

Railsのdefault_scopeは悪だ!(default_scope is evil) ということらしい - Qiita

ま、弊社ではdefault_scopeは使ってないとはいえ、個人で開発してるときには使ってみたくなるじゃないですか、だってevilですよ、かっこいいもん。

そしたら痛い目を見たのでその知見を共有したいと思います。

前提

とりあえずこんな感じのModelがあるとしましょう。

これを前提に話を進めますけど、仮定の話なのでコードに矛盾が生じるかもしれませんし、そもそも動かないかもしれません。許してください。

class User < ApplicationRecord
  has_many :organizations, dependent: :destroy
  default_scope -> { where violated: false }
  :
  :
class Organization < ApplicationRecord
  default_scope -> { where finished: true }
  :
  :

ユーザが複数の組織を持つ感じね。 ユーザは違反したかどうかのviolatedカラムを持っていて、組織はfinishedっていう現役かどうかのカラムを持ってるありがちなやつ。

ちなみにviolatedカラムは今回活躍しません。 じゃあ書くなよって思った!?しょうがないじゃん!手元のコードがそうなってるんだもん!

ActiveRecordRelationを呼んだときにunscopedをかけると親とのwhereも消える

僕がまずぶち当たったのはこいつですね、まじかよと思った。

まずunscopedしないパターンね。

irb(main):089:0> User.first.organizations
  User Load (0.8ms)  SELECT  "users".* FROM "users" WHERE "users"."violated" = $1 ORDER BY "users"."id" ASC LIMIT $2  [["violated", "f"], ["LIMIT", 1]]
  Organization Load (0.8ms)  SELECT  "organizations".* FROM "organizations" WHERE "organizations"."finished" = $1 AND "organizations"."user_id" = $2 LIMIT $3  [["finished", "t"], ["user_id", 2], ["LIMIT", 11]]

うんうん。まあそうだよね。でもorganizationsfinishedは見なくていっかなーってなるとunscopedするでしょ。

irb(main):090:0> User.first.organizations.unscoped
  User Load (0.7ms)  SELECT  "users".* FROM "users" WHERE "users"."violated" = $1 ORDER BY "users"."id" ASC LIMIT $2  [["violated", "f"], ["LIMIT", 1]]
  Organization Load (0.5ms)  SELECT  "organizations".* FROM "organizations" LIMIT $1  [["LIMIT", 11]]

うん?

Organization Load (0.5ms) SELECT "organizations".* FROM "organizations" LIMIT $1 [["LIMIT", 11]]

うわ!user_idの絞り込みも消えてる!まじかよ!

回避策

たぶんこう。

Organization.unscoped.where(user_id: User.first.id)

has_manyかつdependent: :destroyで親を殺したときに子が見れなくてviolates foreign key constraintで死ぬ

最近じゃTwitterで「殺」って文字が入ってると凍結されるらしいですよ!気をつけて!

もうこれは掲題の通り。 僕は個人で開発するときは絶対に外部キー制約を付けるマンなので・・・。

Userをdestroyしたときに子をdependent: :destroyで殺しに行くんだけど、そんときにdefault_scopeが効いて殺しに行けないよってやつ。 default_scopeちゃんどいて!そいつ殺せない!

ActiveRecord::InvalidForeignKey: PG::ForeignKeyViolation: ERROR:  update or delete on table "users" violates foreign key constraint "fk_rails_7b93e0061c" on table "organizations"

ほんとこれはもう、ね。

回避策

class User < ApplicationRecord
  has_many :organizations, dependent: :destroy
  default_scope -> { where violated: false }
  before_destroy { Organization.unscoped.where(user_id: id).destroy_all } # add this code.
  :
  :

postgresql - Rails Foreign Key violation deleting has_many relationships with dependent destroy - Stack Overflow

まとめ

いかがでしたでしょうか。

僕が一番キライなタイプのメディアって「まとめ」が最後にあって「いかがでしたでしょうか」で始まるメディアなんですよね。

あーいう記事は大体クソなので読まないでいいです。僕の経験上。

だいたいクラウドソーシングに投げた「2000文字でお願いします〜」みたいな記事の文字数稼ぎのセクションだから。

えーっと、この記事で何が言いたかったかというと「僕のスキルだと手に負えないのでdefault_scopeorderくらいにしとこうと思いました」ということです!

きっと他にもあると思うんですよね、default_scopeで痛い目を見る事例。でも僕はここでフルリファクタリングをかけたので!もうこれ以上は共有できません!

以上だ!

Ristaでは4人目のエンジニアを募集してます

株式会社リスタではたらいてみませんか。

あーあ、ES6も導入されちゃったし、yarnだし、webpackerだし、出遅れちゃいますよ。はやくジョインしないと。(ちなみに僕はyarnwebpackerもよくわかってません!)

AndroidやiOSアプリもあるよ。出したばっかりなのでまだまだ裁量もってやれる範囲が大きいよ。

気になったら気軽にお話を聞きに来てね!

株式会社リスタの採用/求人一覧 - Wantedly