LoginSignup
1

More than 1 year has passed since last update.

posted at

updated at

Organization

Rails.cache.fetchのブロック内でreturn句を使用してはいけないようですよ

はじめに

この記事は、Railsを触って1年未満の初心者が記載した内容となっております。
なお、私の使用している環境が古い(Rails 4)ため、最新のバージョンでは同様の問題が発生しない可能性があります。

サンプルソース

下のサンプルソースでは、testsテーブルに対してcol1,col2,col3の順番で検索を行い、検索条件に該当するデータが見つかった場合に後続の処理を実行せずに検索結果を返却するようにしています。
なお、検索結果はキャッシュに保持(30.minutes)し、指定した期間内に再度実行した場合には、データベースへのアクセスは行われずにキャッシュかに持したデータが返却されることを期待しています。

def get_testdata( col1 , col2 , col3 )
  Rails.cache.fetch("test-#{col1}-#{col2}-#{col3}", expires_in: 30.minutes ) do

    test = Test.where(col1: col1).first
    return test if test

    test = Test.where(col2: col2).first
    return test if test

    test = Test.where(col3: col3).first
    return test if test

    nil
  end
end

それでは、Railsのコンソール上からこの処理を実際に動かしてみましょう。

1回目

[2] pry(main)> get_testdata("test1","test2","test3")
Cache read: test-test1-test2-test3 ({:expires_in=>1800 seconds})
Cache generate: test-test1-test2-test3 ({:expires_in=>1800 seconds})
  Test Load (0.7ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col1` = 'test1'  ORDER BY `tests`.`id` ASC LIMIT 1
  Test Load (0.4ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col2` = 'test2'  ORDER BY `tests`.`id` ASC LIMIT 1
  Test Load (0.4ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col3` = 'test3'  ORDER BY `tests`.`id` ASC LIMIT 1
=> #<Test id: 1, col1: "test3", col2: "test3", col3: "test3">
[3] pry(main)> 

おぉ、データが取得できました。
SQLが3回実行されているため、col3の条件にのみ一致するデータがあったようです。
次に同じ条件で実行した場合にはキャッシュから取得されるため、SQLが実行されないはずです。

2回目

[3] pry(main)> get_testdata("test1","test2","test3")
Cache read: test-test1-test2-test3 ({:expires_in=>1800 seconds})
Cache generate: test-test1-test2-test3 ({:expires_in=>1800 seconds})
  Test Load (7.1ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col1` = 'test1'  ORDER BY `tests`.`id` ASC LIMIT 1
  Test Load (105.7ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col2` = 'test2'  ORDER BY `tests`.`id` ASC LIMIT 1
  Test Load (54.5ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col3` = 'test3'  ORDER BY `tests`.`id` ASC LIMIT 1
=> #<Test id: 1, col1: "test3", col2: "test3", col3: "test3">
[4] pry(main)> 

... 1回目と同じSQLが実行されているように見えますね。
気のせいかもしれないのでもう1回実行してみましょう。

3回目

[4] pry(main)> get_testdata("test1","test2","test3")
Cache read: test-test1-test2-test3 ({:expires_in=>1800 seconds})
Cache generate: test-test1-test2-test3 ({:expires_in=>1800 seconds})
  Test Load (0.7ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col1` = 'test1'  ORDER BY `tests`.`id` ASC LIMIT 1
  Test Load (0.4ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col2` = 'test2'  ORDER BY `tests`.`id` ASC LIMIT 1
  Test Load (0.4ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col3` = 'test3'  ORDER BY `tests`.`id` ASC LIMIT 1
=> #<Test id: 1, col1: "test3", col2: "test3", col3: "test3">
[5] pry(main)> 

... 気のせいではなかったようです。
キャッシュが上手く機能していないため、毎回データベースにアクセスしているようですね。
キャッシュ確認用にソースを簡単にしたものを作成します。

サンプルソース(キャッシュ確認用)

def get_testdata2( col1 )
  Rails.cache.fetch("test2-#{col1}", expires_in: 30.minutes ) do
    Test.where(col1: col1).first
  end
end

というわけで、キャッシュ確認用にサンプルソースを作成しました。
これで動かなければ実行環境に問題がある可能性があります。

1回目

[2] pry(main)> get_testdata2("test3")
Cache read: test2-test3 ({:expires_in=>1800 seconds})
Cache generate: test2-test3 ({:expires_in=>1800 seconds})
  Test Load (0.7ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col1` = 'test3'  ORDER BY `tests`.`id` ASC LIMIT 1
Cache write: test2-test3 ({:expires_in=>1800 seconds})
=> #<Test id: 1, col1: "test3", col2: "test3", col3: "test3">
[3] pry(main)> 

ちゃんとデータが取得できたようです。
前回の実行時では出力されていなかった、「Cache write」の文字が実行結果に出力されています。

2回目

[3] pry(main)> get_testdata2("test3")
Cache read: test2-test3 ({:expires_in=>1800 seconds})
Cache fetch_hit: test2-test3 ({:expires_in=>1800 seconds})
=> #<Test id: 1, col1: "test3", col2: "test3", col3: "test3">
[4] pry(main)>

おぉ、SQLが実行されずにデータが取得できました。
キャッシュが正しく機能しているため、実行環境は問題ないようです。
次は少しずつ処理を足して、どこで問題が発生しているかを確認していきます。

サンプルソース(return確認用)

def get_testdata3( col1 )
  Rails.cache.fetch("test3-#{col1}", expires_in: 30.minutes ) do
    return Test.where(col1: col1).first
  end
end

サンプルソース(キャッシュ確認用)にreturnのみを追加してみました。
それでは実行していきましょう。

1回目

[2] pry(main)> get_testdata3("test3")
Cache read: test3-test3 ({:expires_in=>1800 seconds})
Cache generate: test3-test3 ({:expires_in=>1800 seconds})
  Test Load (0.9ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col1` = 'test3'  ORDER BY `tests`.`id` ASC LIMIT 1
=> #<Test id: 1, col1: "test3", col2: "test3", col3: "test3">
[3] pry(main)>

ちゃんとデータが取得できたようです。
キャッシュ確認用のサンプルソースで出力された「Cache write」の文字が実行結果に出力されなくなっています。

2回目

[3] pry(main)> get_testdata3("test3")
Cache read: test3-test3 ({:expires_in=>1800 seconds})
Cache generate: test3-test3 ({:expires_in=>1800 seconds})
  Test Load (0.7ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col1` = 'test3'  ORDER BY `tests`.`id` ASC LIMIT 1
=> #<Test id: 1, col1: "test3", col2: "test3", col3: "test3">
[4] pry(main)> 

最初のサンプルソースと同様に2回目も1回目と同じSQLが実行されることが確認されました。
return句を使用してキャッシュ対象のブロックを抜けてしまっていることが原因のようですね。
それでは、サンプルソースをreturnを使用しないように修正しましょう。

サンプルソース(修正版)

def get_testdata( col1 , col2 , col3 )
  Rails.cache.fetch("test-#{col1}-#{col2}-#{col3}", expires_in: 30.minutes ) do
    test = Test.where(col1: col1).first
    test = Test.where(col2: col2).first if test.blank?
    test = Test.where(col3: col3).first if test.blank?
  end
end

returnが問題ならば、これで上手く行くはず!
それでは実行して行きましょう。

1回目

[2] pry(main)> get_testdata("test1","test2","test3")
Cache read: test-test1-test2-test3 ({:expires_in=>1800 seconds})
Cache generate: test-test1-test2-test3 ({:expires_in=>1800 seconds})
  Test Load (0.7ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col1` = 'test1'  ORDER BY `tests`.`id` ASC LIMIT 1
  Test Load (0.5ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col2` = 'test2'  ORDER BY `tests`.`id` ASC LIMIT 1
  Test Load (0.6ms)  SELECT  `tests`.* FROM `tests`  WHERE `tests`.`col3` = 'test3'  ORDER BY `tests`.`id` ASC LIMIT 1
Cache write: test-test1-test2-test3 ({:expires_in=>1800 seconds})
=> #<Test id: 1, col1: "test3", col2: "test3", col3: "test3">
[3] pry(main)> 

最初に実行した時と同様にSQLが実行されてデータが取得できています。
実行結果に「Cache write」の文字が出力されているため、期待できそうです。

2回目

[3] pry(main)> get_testdata("test1","test2","test3")
Cache read: test-test1-test2-test3 ({:expires_in=>1800 seconds})
Cache fetch_hit: test-test1-test2-test3 ({:expires_in=>1800 seconds})
=> #<Test id: 1, col1: "test3", col2: "test3", col3: "test3">
[4] pry(main)>

やった!!!
SQLが実行されずにデータを取得することができました。
これにより、もともとの処理においてもキャッシュが正しく機能し、データベースへアクセスすることなくデータが取得できるようになりました。

まとめ

Rails.cache.fetchで実行しているブロック内でreturn句を使用して処理を終了した場合には、キャッシュにデータが保存されないことが確認されました。

作成していた当初は、returnを記載しただけでキャッシュが機能しなくなるとは思っていませんでした。
よくよく考えてみると、returnが実行されたタイミングでメソッド毎抜けてしまうわけですから、何がキャッシュされるのだろうということもありますが...
まぁ、無事に問題が解決してよかったです。

これが、同様の問題が発生している人の参考にでもなれば良いかと思います。

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
1