Rails expire low level cache on update - ruby-on-rails

I am using Rails low level cache in my controller but I don't know how to expire the cache when record is updated. Below is snippets of my controller
def show
#user = Rails.cache.fetch("users/params[:id]", expires_in: 2.minutes) do
User.find(params[:id])
end
end
So rails is creating cache fine. But when I update the record I want to expire old cache and create new one. e.g
User.first.touch
I found that rails have cache_key_with_version but I am not sure how to use that with my example
#user = User.first
#user.cache_key_with_version #=> "users/1-20220316023452830286"
I won't have #user object at the first call in my controller so I am not sure how to use #user.cache_key_with_version as my key.
#user = Rails.cache.fetch(#user.cache_key_with_version)
Above code will not work as #user is nil at this stage.
One way I can think of is use after_save callback in model to delete cache key on save. Some thing like this
class User < ApplicationRecord
after_save :delete_cache_key
private
def delete_cache_key
Rails.cache.delete("users/#{self.id}")
end
end
But may be there is better way to solve this.

If you want Rails to get expired when record update, add cache_key_with_version to your cache key, so the controller will be like
def show
#user = User.find(params[:id])
Rails.cache.fetch("users/#{#user.cache_key_with_version}", expires_in: 2.minutes) do
# some other query that need to be done
end
end
loading a record should not be a heavy load, if it is, than use .select(:id, :updated_at) may be a choice
def show
user_cache_key = User.select(:id, :updated_at).find(params[:id]).cache_key_with_version
#user = Rails.cache.fetch("users/#{user_cache_key}", expires_in: 2.minutes) do
User.find(params[:id])
end
end
cache key is made with updated_at and id, so we only these two column to make a cache key with version. When record update, it will update updated_at, too, the previous cache will be discarded

Related

Rails 5 set current_user from external API

I use temporarily Rails as frontend app to communicate with an API.
After the authentication, I set the user_id in a cookie.
I use the her gem to call the User from the API and save it into an instance variable.
The issue is that I do this request on every page I and would like to do it once.
It's like #current_user is reset after each page.
def current_user
#User.find -> Her model
#current_user ||= User.find(cookies.signed[:user_id]) if cookies.signed[:user_id]
end
There no clear solution because your user coming from API. You can try to do something like that:
#remember user attributes without references
session['user'] = #current_user.attributes #remember
#user = OpenStruct(session['user']) #load, allow call #user.name etc, but not #user.posts
#use class variable
class User
include Her::Model
##tmp = {}
def remember
##tmp[id] = self
#call job etc to delete user from tmp to prevent something that reminds "memory leak"
end
def self.local_find(id)
##tmp[id]
end
end
def current_user
#current_user ||= User.local_find(cookies.signed[:user_id]) ||
User.find(cookies.signed[:user_id]) if cookies.signed[:user_id]
end
The main reason not to store(remember) objects in the session(long-term variable) is that if the object structure changes, you will get an exception.

Session in Action Mailer - how to pass it?

Let's say I have a website where people can get a free ebook if they will sign up for a newsletter - after they've done it, I will create a User model and I will show them Edit Form to add some extra details about them.
I don't want to force them to add a password or any other details on the first page because it would decrease conversions and I don't require the additional information either. Also, I don't want them to have forever access to the Edit page so I solved it by assigned a session to them and recognize it through it on the Edit page. This is my controller:
class UsersController < ApplicationController
def create
user = User.new(user_params)
if user.save
session[:user_id] = user.id
UserWorker.perform_in(5.minutes, 'new_user', user.id)
redirect to edit form...
end
end
def edit
#user = User.find(session[:user_id])
end
def update
#user = User.find(session[:user_id])
#user.update!(user_edit_params)
redirect_to user_thank_you_path
end
end
But if they won't add extra information within 10 mins, I will send them an email via ActiveMailer with a link to the Edit form and ask them to do so.
Th question is how could I identify the user through the session and show them the form - how could I do User.find(session[:user_id] via ActionMailer)? Is it actually a correct way or would you recommend a different approach?
One way could be to set a background job to run in 10 minutes.
Inside that job, you would check if they're still "unregistered". You deliver the email if they've yet to complete the registration.
Something like this;
class UsersController < ApplicationController
def create
user = User.new(user_params)
if user.save
session[:user_id] = user.id
RegistrationCompletionReminderWorker.perform_in(10.minutes, user.id)
# redirect to edit form...
end
end
end
class RegistrationCompletionReminderWorker
def perform(user_id)
user = User.find(user_id)
if user.password.nil? # or whatever your logic for registration completion is
UserMailer.registration_reminder(user_id).deliver_now
end
end
end

Accessing current_user in a model in a Rails 3.2 app

I have a Rails 3.2 app. It is a publishing app where we kick off several Sidekiq jobs in response to changes in content. I was calling this from the controller but there's now getting to be multiple points of entry and are now duplicating logic in multiple controllers. The proper place for this to be is in a callback in the model. However, accessing current_user is frowned upon in the model but for things like logging changes or app events, it is critical.
So I have two questions (1) Is there something I'm missing regarding the argument about accessing current_user when you want to be logging changes across complex model structures? and (2) Is the proposed solution here an effective one with last update over 2 years ago in terms of thread-safety? I use a three Unicorn processes on Heroku. https://stackoverflow.com/a/2513456/152825
Edit 1
Thinking through this, wondering if I should just do something like this in my application.rb
class ArcCurrentUser
#current_user_id
def self.id
return #current_user_id
end
def self.id=id_val
#current_user_id=id_val
end
end
and then in my current_user method in application_controller, just update ArcCurrentUser.id to #current_user.id? I will only be using it for this logging functionality.
You're correct in that you can't access current_user from a model.
As for the answer you linked, I'm not entirely sure but I think it's not fully thread-safe. From the same question, I like this answer https://stackoverflow.com/a/12713768/4035338 more.
Say we have a controller with this action
...
def update
#my_object = MyModel.find(params[:id])
#my_object.current_user = current_user
#my_object.assign_attributes params[:my_model]
#my_object.save
end
...
and this model
class MyModel < ActiveRecord::Base
attr_accessor :current_user
before_save :log_who_did_it
private
def log_who_did_it
return unless current_user.present?
puts "It was #{current_user}!"
end
end
Or my favourite
...
def update
#my_object = MyModel.find(params[:id])
#my_object.update_and_log_user(params[:my_model], current_user)
end
...
class MyModel < ActiveRecord::Base
...
def update_and_log_user(params, user)
update_attributes(params)
puts "It was #{user}!" if user.present?
end
end

Rails: force column update for routes

I've got a route constraint that matches column values. Works fine, but it seems to cache the values so that new values don't match. How can I force a reload for this class?
class ClientCodeConstraint
def matches?(request)
#client_code = request.path_parameters[:client_code]
users.each { |u| return true if #client_code == u.client_code }
false
end
private
def users
#users ||= User.all
end
end
I need to force update it somehow.
First of all, your code seems to be very bad. If it possible, you should make just one DB query instead of retrieving all users.
class ClientCodeConstraint
def matches?(request)
User.where(client_code: request.path_parameters[:client_code]).any?
end
end
I think the problem is here #users ||= User.all. You are caching User.all result in instance variable, so it does not updated. You don't need to use instance variable at all. Choose your users method to:
def users
User.all
end
Or if it possible, just use my solution.

Reuse same object in other methods within same model

Using Rails 3.2. I have the following code:
# photo.rb
class Photo < ActiveRecord::Base
before_create :associate_current_user
after_save :increase_user_photos_count
after_destroy :decrease_user_photos_count
private
def associate_current_user
current_user = UserSession.find.user
self.user_id = current_user.id
end
def increase_user_photos_count
current_user = UserSession.find.user
User.increment_counter(:photos_count, current_user.id)
end
def decrease_user_photos_count
current_user = UserSession.find.user
User.decrement_counter(:photos_count, current_user.id)
end
end
Before a new record is created, it searches for the current_user. This is alright if it's just 1 new record at a time. But if there are 100 records to be created, it's gonna search for the same current_user 100 times. There is definitely performance issue.
I don't want it to keep finding the current user every time a record is created/photos_count updated, etc.
After refactoring, does this affect other users who are also uploading their photos using their accounts?
Note: For some reasons, I can't use the counter_cache and photos_controller.rb because I am following this example: http://www.tkalin.com/blog_posts/multiple-file-upload-with-rails-3-2-paperclip-html5-and-no-javascript
Thanks.
Use this
def current_user
#current_user ||= UserSession.find.user
end
This will cache the value in the instance variable #current_user unless it's nil (first time in the request), in which case it will set it.

Resources