以下のコードについて何か気をつけることがないか考えてみましょう。
class BookService
class << self
def latest_book_id
latest_book.id
end
def latest_book_name
latest_book.name
end
def latest_book
@latest_book ||= Book.last
end
end
end
このコードは、latest_book
の内容をクラスインスタンス変数を使ってメモ化しています。
これによりBookService.latest_book_id
やBookService.latest_book_name
を実行した際に初回のみBookの参照が行われるのでSQLの発行を抑えることができます。
実際にirbを使って試すと以下のようになります。
irb(main):016:0> BookService.latest_book_id
(0.6ms) SELECT sqlite_version(*)
Book Load (0.3ms) SELECT "books".* FROM "books" ORDER BY "books"."id" DESC LIMIT ? [["LIMIT", 1]]
=> 1
irb(main):017:0> BookService.latest_book_name
=> "test"
irb(main):018:0> BookService.latest_book_id
=> 1
余計なSQL発行を抑えられるのでgood!といきたいのですが、実際はこのようなコードを書くとバグになる可能性があります。
それを確認するために、まずはbooksテーブルに新規レコードを追加してみましょう。
irb(main):019:0> Book.create!(name: "book2")
TRANSACTION (0.1ms) begin transaction
Book Create (1.5ms) INSERT INTO "books" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "book2"], ["created_at", "2023-03-30 13:37:35.307343"], ["updated_at", "2023-03-30 13:37:35.307343"]]
TRANSACTION (1.1ms) commit transaction
=>
#<Book:0x0000000107c0dd40
id: 2,
name: "book2",
created_at: Thu, 30 Mar 2023 13:37:35.307343000 UTC +00:00,
updated_at: Thu, 30 Mar 2023 13:37:35.307343000 UTC +00:00>
これでレコード追加が完了しました。では改めてBookService.latest_book_id
を実行します。
irb(main):020:0> BookService.latest_book_id
=> 1
最新のbooksテーブルのidが2であるにも関わらず、1が返ってきてしまいました。
これは、先ほどメモ化したlatest_bookが使われてしまっているためです。
このようにクラスインスタンス変数やクラス変数を使うと意図せず以前の値を使い回してしまう恐れがあります。
Railsアプリの場合はプロセスごとにクラスインスタンス変数やクラス変数の値を保持しているので、異なるリクエストに対しても以前のリクエストの値を使い回してしまうというわけです。
スレッドセーフにはならいのでマルチスレッドも同様に注意が必要です。
というわけで、クラスインスタンス変数やクラス変数を使う際は本当にそのクラスで使い回して良い値(リクエストやスレッドごとに変わらない値)に限って使った方が良いでしょう。
参考