LoginSignup
0

クラスインスタンス変数、クラス変数を使う時に気をつけること

Posted at

以下のコードについて何か気をつけることがないか考えてみましょう。

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_idBookService.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アプリの場合はプロセスごとにクラスインスタンス変数やクラス変数の値を保持しているので、異なるリクエストに対しても以前のリクエストの値を使い回してしまうというわけです。
スレッドセーフにはならいのでマルチスレッドも同様に注意が必要です。

というわけで、クラスインスタンス変数やクラス変数を使う際は本当にそのクラスで使い回して良い値(リクエストやスレッドごとに変わらない値)に限って使った方が良いでしょう。

参考

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
What you can do with signing up
0