[RUBY] It looks like you shouldn't use the return clause inside a Rails.cache.fetch block

Introduction

This article is written by beginners who have been using Rails for less than a year. Please note that my environment is old (Rails 4), so the latest version may not have the same problem.

Sample source

In the sample source below, the tests table is searched in the order of col1, col2, col3, and if data that matches the search conditions is found, the search results are returned without performing subsequent processing. I will. The search results are kept in the cache (30.minutes), and if it is executed again within the specified period, it is expected that the data held in the cache will be returned without accessing the database. I will.

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

Now, let's actually run this process from the Rails console.

First time

[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)> 

Oh, I got the data. Since SQL is executed 3 times, it seems that there was data that matched only the condition of col3. The next time you run it under the same conditions, it will be retrieved from the cache and the SQL should not be executed.

Second time

[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)> 

... It looks like the same SQL as the first time is being executed. It may be due to your feelings, so let's try it again.

Third time

[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)> 

... it didn't seem to be my fault. It looks like you're accessing the database every time because the cache isn't working well. Create a simplified source for cache checking.

Sample source (for cache check)

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

That's why I created a sample source for checking the cache. If this does not work, there may be a problem with the execution environment.

First time

[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)> 

It seems that the data was acquired properly. The characters "Cache write", which were not output at the time of the previous execution, are output in the execution result.

Second time

[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)>

Oh, I was able to get the data without executing SQL. The execution environment doesn't seem to be a problem because the cache is working properly. Next, we'll add some processing little by little to see where the problem is occurring.

Sample source (for return confirmation)

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

I added only return to the sample source (for cache check). Let's do it.

First time

[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)>

It seems that the data was acquired properly. The characters "Cache write" output in the sample source for cache confirmation are no longer output in the execution result.

Second time

[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)> 

It was confirmed that the same SQL as the first time is executed the second time as well as the first sample source. It seems that the cause is that you have exited the cached block using the return clause. Now let's modify the sample source so that it doesn't use return.

Sample source (modified version)

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

If return is a problem, this should work! Let's do it.

First time

[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)> 

The SQL is executed and the data is acquired in the same way as when it was first executed. Since the characters "Cache write" are output in the execution result, you can expect it.

Second time

[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)>

Hooray! !! !! I was able to get the data without executing the SQL. As a result, the cache works properly even in the original processing, and data can be retrieved without accessing the database.

Summary

It has been confirmed that no data is saved in the cache when the process is terminated using the return clause in the block running in Rails.cache.fetch.

When I first created it, I didn't expect the cache to stop working just by mentioning a return. If you think about it carefully, each method will be exited when the return is executed, so you may wonder what is cached ... Well, I'm glad that the problem was solved safely.

I hope this will be helpful for those who are experiencing similar problems.

Recommended Posts

It looks like you shouldn't use the return clause inside a Rails.cache.fetch block
Try the training online (it looks like self-study support)
How to create a placeholder part to use in the IN clause