Trouble accessing object attributes from cache - ruby-on-rails

I am caching several User objects using Rails.cache.write but when I retrieve it from cache and try to call an attribute method (id, name, email, etc.) on any of the User objects I am getting
You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.include?
I can call inspect on the User objects and I see all the attributes. Any ideas? Thanks
I'm using the standard cache configuration, no modification. Here is some code:
I am writing to the cache in my model inside an instance method that gets called when the user logs in like so:
cached_friends = self.friends
Rails.cache.write(self.id.to_s+"_friends", cached_friends)
Then in my controller I have a before filter:
def get_friends_from_cache
if current_user
friends = Rails.cache.read(current_user.id.to_s+"_friends")
friends.each do |friend|
if !friend.facebook_id.nil?
#other stuff....
end
end
end
end
I get the error on the if !friend.facebook_id.nil? line. facebook_id is a column in the db. Basically there are no User instance methods available for each User object. But I know the objects are there because I can insert raise "#{friends.first.inspect}" and everything spits out as I would expect.

This happens in development mode (i.e. with the setting config.cache_classes = false). The root cause:
ActiveRecord strips the attribute accessor methods from the class objects
MemoryStore (i.e. the default cache store) does not martial/unmartial objects
One workaround is to use MemCacheStore another one is to set config.cache_classes = true. See also Comments on railscasts

Related

Rails - conditional deliver_now based on the content returned by the mailer method

How can I deliver_now conditionally based on the content of the data returned by the mailer method?
I have a Mailer that is sent via a loop through a list of users, like so:
users.each do |u|
AbcMailer.with(user_id: u.user_id).abc_report.deliver_now
end
The list of users that should receive the mailer (users in the loop) lives in ActiveRecord, and all the users' actual data lives in an external MySql DB.
The abc_report method in the AbcMailer class makes some queries to the MySql DB and returns a bunch of info for each user, which is then inserted into an html.erb email template, and delivered now.
My issue is that I need to only deliver to some of those users, because in the DB, one of the pieces of info that comes back is whether the user is active or not. So I would like to only deliver_now if the user has active = 1. But I can't find any examples of unchaining these methods to do what I want.
When I just do AbcMailer.with(user_id: u.user_id).abc_report, what it returns is actually the filled-out html.erb template already. When I do AbcMailer.with(user_id: u.user_id) by itself, it returns #<ActionMailer::Parameterized::Mailer:0x00007fdd43eb5528>.
Things I've Tried
I tried inserting a return if user["Active"] == 0 in the abc_report method but that obviously killed the entire loop rather than skipping to the next item, so I'm working under the assumption that the skip has to happen with a next in the actual loop itself, not in an external method being called.
I also found this which seems like a great solution in a plain Ruby context but because in this case, abc_report is automatically filling out and returning the AbcMailer html.erb template...I'm stumped on how I would get it to just return a boolean without killing the whole loop.
Assuming that you have active field of boolean type in users table and you want to send the emails to all active users.
users.each do |u|
AbcMailer.with(user_id: u.user_id).abc_report.deliver_now if u.active
end
you can add any other condition on this as well.
Add this to user.rb
scope :active, -> {
where(active: 1)
}
Now in your mailer
Users.active.each do |user|
AbcMailer.with(user_id: user.user_id).abc_report.deliver_now
end
This way you have to get less data from the database, resulting in a faster query.

How to use include? in Rails

hopefully a trivial question for the masters at SO. How would I use ruby's include? to see if Post's Comment owner includes the current_user? For example, this code works fine, but I'm trying to find the include? method equivalent to test if the User is included. Many thanks in advance!
# Brief has many Submissions
brief = Brief.first
user = User.first
brief.submissions.where('submissions.user': user) # works as expected and retrieves the correct Submission
brief.submissions.include?(user: user) # false but should return true
The result of where is an ActiveRecord_Relation, where every element inside is an instance of the same class the receiver is defined as, meaning every element in brief.submissions.where('submissions.user': user) is a Submission instance
In order for include? to return true, you must pass an object that's an instance of Submission, what you're passing right now is a Hash.
To solve that, try instead brief.submissions.include?(user).
But, if your query does what it means to, then you're not getting users as results, but submissions, filtering by the user to which they belong to. So, in any case, no matter what user you pass, it's going to return false.
I recommend you to use exists? if you're expecting to receive just a predicate value:
brief.submissions.exists?('submissions.user': user)

Unclear whether values are being saved to db

Part of a create method in my controller is:
if #organization.relationships && #organization.relationships.where('member = ?', true).any?
#organization.users.where('member = ?', true).each do |single|
single.create_digest
debugger
end
end
As you can see I've been testing with the debugger. In the debugger I'm experiencing the following strange behaviour. single and organization.users both display the details/values of the same user. However these values differ between when I examine using single and when I use organization.users in the debugger. For single the user does have values for activation_digest and activation_sent_at, while they are nil when I look at organization.users.
Can anyone explain this behaviour? The nil values are a problem since single isn't available outside the if statement. It's not clear to me whether the value have or have not been saved to the db.
P.S. The model method being used:
def create_digest
create_activation_digest
update_attribute(:activation_digest, self.activation_digest)
update_columns(activation_sent_at: Time.zone.now)
end
When you query for models in Rails (users in your case), each distinct query gives back separate copies of those models.
So for example, the users returned by this query:
#organization.users.where('member = ?', true)
Will be separate copies of the users that are returned by this slightly different query (I assume this is what you run in the debugger):
#organization.users
If you modify one copy of the user and save the modifications to the database, it will not automatically propagate those modifications to the other copy of the user. The other copy still has the (now out of date) data that was returned when you first ran the query.
To verify that the changes were actually persisted to the database, you can force Rails to refresh the user object with the latest data from the database by calling reload. For example:
# These are two different in-memory copies of the same user
user = #organization.users.where('member = ?', true).first
user_copy = User.find(user.id)
user.create_digest
# The copy is now out of date
user_copy.activation_sent_at # => nil
# Refresh the copy from the database
user_copy.reload
user_copy.activation_sent_at # => 2015-08-02 21:00:50 -0700

Instance variables seem to lose their values

My report.rb class contains the following code:
def create
#start_term = ::SchoolTerm.find(1705265)
#end_term = ::SchoolTerm.currently_enrolling
current = 0
total = all_terms.size
#terms = {}
all_terms.each do |t|
#terms[t.id] = Business::Sales::RevenueByWeek::Term.new(t)
Rails.logger.info("#{#terms} is #terms in report#create")
current += 1
self.progress = 100.0 * (Float(current) / Float(total))
end
end
def all_terms
::SchoolTerm.between(#start_term, #end_term) - RevenueGoal.terms_without
end
def each
Rails.logger.info("#{#terms} is #terms in report.each")
all_terms.each do |t|
yield #terms[t.id] if #terms[t.id]
end
end
The logger line inside create shows that #terms has the correct values while looping through different school terms. However, the logger line in the each method shows #terms as empty. When I try to load up the report, I get this error:
You have a nil object when you didn't expect it!
You might have expected an instance of ActiveRecord::Base.
The error occurred while evaluating nil.[]
Extracted source (around line #24):
</tr>
</thead>
<% report.each do |term| %>
<tbody class="term_<%= term.id %>">
<tr class="term">
<th><%= term.term %></th>
I have verified that Business::Sales::RevenueByWeek::Term.new returns the correct data. Any ideas why I'm getting this nil object error? I thought that instance variables retained their values, but somehow this one is getting lost.
Thanks.
I'm assuming report.rb is an active record model. In that framework, #create isn't meant to be a constructor, it only creates rows in the underlying table. I see you set #terms = {} there, however.
If your report instance is queried out of the database, #create is never run, and so your member is never initialized.
I suggest you hide uses of #terms (even internal ones) behind a method named terms (that is, unless you've already declared it as an AR attribute, or similar.) Your method can lazy-initialize the member as needed.
report.rb is not an active record model - no database tables are used. The create method is used to create an instance of the report, with cascading calls to other classes needed to get the values for the report, such as the Term.new call.
After the create method finishes, a view is rendered - the error above comes from one of the partials in that view.
I have placed various logger calls in the create method to verify that it is indeed getting called and that the #terms variable in the create method is what I expect it to be. What I can't figure out is why the filled-in #terms hash isn't available in the each method. The create method logger calls show up before the each method logger calls, and #terms is okay in create but empty or nil in each. Shouldn't #terms retain its value for the whole instance - and do it without hiding things in another method?
I solved the problem. What was happening was that the #terms variable was supposed to be written to a cache. We are using memcached, and the #terms data was more than 1 MB is size, so the Cache.write statement was failing. Then when #terms needed to be pulled from the cache in the 'each' method, there was nothing there. The quick-and-dirty solution was to cut down the size of the #term data so that the Cache.write was successful. A long term solution is to replace memcached with Resque, which has no such limitations on the size of the data corresponding to one key.

Rails Devise: How do I (mem)cache devise's database requests for the user object?

Every time I hit an authenticated page, I notice devise issuing an SQL statement :
User Load (0.2ms) SELECT users.* FROM users WHERE (users.id = 1) LIMIT 1
(I'm using Rails 3 btw .. so cache_money seems out as a solution and despite a lot of searching I've found no substitute).
I tried many overrides in the user model and only find_by_sql seems called. Which gets passed a string of the entire SQL statement. Something intuitive like find_by_id or find doesn't seem to get called. I 'can' override this method and glean the user-id and do a reasonable cache system from that - but that's quite ugly.
I also tried overriding authenticate_user which I can intercept one SQL attempt but then calls to current_user seems to try it again.
Simply, my user objects change rarely and its a sad state to keep hitting the db for this instead of a memcache solution. (assume that I'm willing to accept all responsibility for invalidating said cache with :after_save as part but not all of that solution)
The following code will cache the user by its id and
invalidate the cache after each modification.
class User < ActiveRecord::Base
after_save :invalidate_cache
def self.serialize_from_session(key, salt)
single_key = key.is_a?(Array) ? key.first : key
user = Rails.cache.fetch("user:#{single_key}") do
User.where(:id => single_key).entries.first
end
# validate user against stored salt in the session
return user if user && user.authenticatable_salt == salt
# fallback to devise default method if user is blank or invalid
super
end
private
def invalidate_cache
Rails.cache.delete("user:#{id}")
end
end
WARNING: There's most likely a better/smarter way to do this.
I chased this problem down a few months back. I found -- or at least, I think I found -- where Devise loads the user object here:
https://github.com/plataformatec/devise/blob/master/lib/devise/rails/warden_compat.rb#L31
I created a monkey patch for that deserialized method in /initializers/warden.rb to do a cache fetch instead of get. It felt dirty and wrong, but it worked.
I've been struggling with this, too.
A less convoluted way of doing this is to add this class method to your User model:
def self.serialize_from_session(key, salt)
single_key = key.is_a?(Array) ? key.first : key
Rails.cache.fetch("user:#{single_key}") { User.find(single_key) }
end
Note that I'm prepending the model name to the object ID that is passed in for storing/retrieving the object from the cache; you can use whatever scheme fits your needs.
The only thing to worry about, of course, is invalidating the user in the cache when something changes. It would have been nice instead to store the User in the cache using the session ID as part of the key, but the session is not available in the model class, and is not passed in to this method by Devise.

Resources