Rails 3.0.3 Rails.cache.read can't write to DB - ruby-on-rails

I am playing around with the Rails 3 Rails.cache feature and when I write an ActiveRecord entry to cache, I cannot read it back, change the attributes then write to the database. I get a "TypeError: can't modify frozen hash". I used to use another memcache plugin, but I'm trying to switch over to Heroku, and it's incredibly annoying that I cannot save ActiveRecord entries I throw into Memcache. This will result in a lot of unnecessary DB reads to change small bits of information.
For example, I might do this in the database. Assuming the User model is:
User -> login:string and typing the following into rails c
user = User.new
user.login = 'test'
user.save
Rails.cache.write('user:login:test', user)
user2 = Rails.cache.read('user:login:test')
user2.login = 'test2'
TypeError: can't modify frozen hash
/app/.bundle/gems/ruby/1.8/gems/activerecord-3.0.3/lib/active_record/attribute_methods/write.rb:26:in `[]='
/app/.bundle/gems/ruby/1.8/gems/activerecord-3.0.3/lib/active_record/attribute_methods/write.rb:26:in `write_attribute'
/app/.bundle/gems/ruby/1.8/gems/activerecord-3.0.3/lib/active_record/attribute_methods/dirty.rb:61:in `write_attribute'
/app/.bundle/gems/ruby/1.8/gems/activerecord-3.0.3/lib/active_record/attribute_methods/write.rb:13:in `login='
Does anyone know how to solve this problem?

After getting the user2 object value from this line:
user2 = Rails.cache.read('user:login:test')
do the following:
dup_user = user2.dup # this will make a clone of the user2 object
dup_user.login = test_2
Then the "frozen hash" exception should not appear again.

When you store an object in cache, the object is frozen. This is one of the reasons why you should never store complex objects, like ActiveRecord models.
When you read from cache, the object is loaded, but it's still frozen. ActiveRecord tries to restore the object state, but it needs to update some internal attributes. The update will fail because the object is frozen.
Instead of storing the entire object, simply store its id and retrieve the user on-the-fly.
user = User.new
user.login = 'test'
user.save
Rails.cache.write('user:login:test', user.id)
user2 = User.find(Rails.cache.read('user:login:test'))
user2.login = 'test2'
This is a little bit less more efficient in terms of caching, but it's the way to go. In fact, you don't consider that in the meanwhile other attributes of the user might have been changed and your object no longer represents the current status.
Also, in this way your cache store won't act as a database and you'll be able to store more cache entries. IDs take less memory than an entire ActiveRecord instance.
Of course, you can use the #dup workaround, but it's just a trick. It's not the solution. You will still have problems with cache integrity as I explained above.
user = User.new
user.login = 'test'
user.save
Rails.cache.write('user:login:test', user)
user2 = Rails.cache.read('user:login:test').dup
user2.login = 'test2'

Related

Rails console update

Hello I was trying the update the data in the table using the rails console.
Box.where("code = 'learning'").update(duration: 10)
I ran this command.
The data is temporarily changing.
Box.where("code = 'learning'")
When I run this the precious data is being displayed.
Could anyone let me the issue.
Thank you in advance.
#update updates a single record.
user = User.find_by(name: 'David')
user.update(name: 'Dave')
It will return true/false depending on if the record was actually updated. You can see the validation errors by inspecting the errors object:
user.errors.full_messages
In non user-interactions situations like seed files and the console it can be helpful to use the bang methods such as #update!, #save! and #create! which will raise an exception if the record is invalid.
If you want to update multiple records at once you need to use #update_all:
Box.where("code = 'learning'")
.update_all(duration: 10)
This creates a single SQL update statement and is by far the most performant option.
You can also iterate through the records:
Box.where("code = 'learning'").find_each do |box|
box.update(duration: 10)
end
This is sometimes necissary if the value you are updating must be calculated in the application. But it is much slower as it creates N+1 database queries.

Ruby on Rails / ActiveRecord: Updating records with first_or_initialize causes RecordNotUnique

I'm currently getting user data from a SAML assertion and creating users in a local DB based on that info:
mapped_role = map_role user_role
user = User.where(email: auth_attrs.single('Email')).first_or_initialize do |u|
u.firstname = auth_attrs.single('First Name')
u.uid = auth_attrs.single('UID')
u.provider = resp.provider
u.role = mapped_role
end
This works well enough, but when the user's details change (for instance, their role changes), that data doesn't get updated in the DB. What I've tried doing is moving the role assignment out of the do block (on the user object returned by first_or_initialize) and then calling a follow-up user.save, but this results in a pretty red screen informing me that the column 'email' isn't unique. I don't want to be creating a new record here, just updating an existing one. Is there a better pattern to be using here?
Edit: I've tried the various approaches laid out here, but they result in the same SQLite3 error. It seems like I'm missing something there.
Edit2: It looks like this might be due to Devise trying to do something behind the scenes with an email field of its own(?).
I think I would go about it like so
mapped_role = map_role user_role
# find the user or initatiate an new un-persisted user
user = User.find_or_initialize_by(email: auth_attrs.single('Email'))
attributes = {firstname: auth_attrs.single('First Name'),
uid: auth_attrs.single('UID'),
provider: resp.provider,
role: mapped_role}
# attempt to update the above record with the appropriate attributes
# this will set the attributes and fire #save
if user.update(attributes)
# successful
else
# handle validation errors
end
This way there is no need for logical handling of users that are already persisted and new users.

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

How to store find_by_sql_results in session variable

Here is my controller code to check login details of a user
def validateLogin
#email = params[:userEmail1]
#pass = params[:userPassword1]
if params[:userEmail1] != nil
valid_user = Userprofile.find_by_sql(["select * from userprofiles where userEmail=? and userPassword=?", #email, #pass])
if valid_user.count > 0
session[:email] = #email
session[:uid] = valid_user.id
session[:userType] = valid_user.userType # usertype is a column in userprofiles table
# But here i am not receiving the usertype it gives error that undefined variable usertype.
redirect_to "/userhomes/"
else
flash[:message] = "Either email or password is incorrect"
redirect_to '/'
end
else
flash[:message]="Fields can not be blank"
render :action=>'defaults'
end
Please help
session[:userType] = valid_user.userType
# Error: (usertype is a column in userprofiles table)
But here i am not receiving the usertype it gives error that undefined variable usertype.
You are seeing this error because you receive an array of objects from find_by_sql. You even check the size of the array in your if clause.
From your code I think you expect only one returned object. But you still need to get it from the array like so:
profiles = Userprofile.find_by_sql(["select * from userprofiles where userEmail=? and userPassword=?", #email, #pass])
if profiles.count > 0
user_profile = profiles[0]
#... your other stuff
end
Another variant which also much better uses Rails idioms and especially ActiveRecord as is was inteded to be used is to let it construct the SQL by itself which is generally safer, less prone to errors and cacheble.
You didn't write which version of Rails you are using, but for Rails 2.3.x, it looks like this
user_profile = Userprofile.first(:conditions => {:userEmail => #email, :userPassword => #pass})
For Rails 3.x, it looks like this:
user_profile = Userprofile.where(:userEmail => #email, :userPassword => #pass).first
Both variants expect that you have a model called Userprofile, which you generally require to effectively work with database objects in Rails. What both queries do is to create a new model instance from the first row returned from your query (that's what the first does).
Generally, you should get a book or some guide on the internet and learn how to properly use ActivRecord. Note that the API has seriously changed between Rails 2.3 and Rails 3 so make sure to use a guide for your actual Rails version.
And as a final advice, you shouldn't store actual ActiveRecord objects in the session. They would need to be serialized on store and de-serialized on access. What makes it hard (or impossible to track object references.
Also, Rails uses the cookie session store by default, which means that the whole session data is stored in a cookie on the client. The data therein in fully readyabkle to anyone with access to the cookie as it is only signed to restrict tampering with the data, but it is not encrypted. Thus, in your case anyone would be able to ready the (unecrypted) password.
Instead of storing the model object, you should store it's id instead and get the actual (and up-to-date) object from the database instead on each request. This is much easier, saves you from cache inconsistencies (what happens if the user changes her password) and is probably faster than to transfer a huge session cookie from the client on each request.

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