Reputation: 2219
I have an User model which has an array of roles.
From my schema.db:
create_table "users", force: true do |t|
t.string "roles", array: true
My model looks like this:
class User < ActiveRecord::Base
ROLES = %w(superadmin sysadmin secretary)
validate :allowed_roles
after_initialize :initialize_roles, if: :new_record?
private
def allowed_roles
roles.each do |role|
errors.add(:roles, :invalid) unless ROLES.include?(role)
end
end
def initialize_roles
write_attribute(:roles, []) if read_attribute(:roles).blank?
end
Problem is when I try to add another role from console like user.roles << "new_role" then user.save! says true and asking user.roles gives me my wanted output. But when I ask User.find(user_id).roles then I get the previous state without "new_role" in it.
For ex.
user.roles
=> ["superadmin"]
user.roles << "secretary"
=> ["superadmin", "secretary"]
user.save!
=> true
user.roles
=> ["superadmin", "secretary"]
User.find(<user_id>).roles
=> ["superadmin"]
When replacing the whole array, it works as I want:
user.roles
=> ["superadmin"]
user.roles = ["superadmin", "secretary"]
user.save!
=> true
user.roles
=> ["superadmin", "secretary"]
User.find(<user_id>).roles
=> ["superadmin", "secretary"]
I'm using rails 4 and postgresql, roles are for cancancan gem.
Changing other fields like user.name for ex works like expected. I made quite a lot of digging in google, but no help.
Upvotes: 1
Views: 227
Reputation: 6068
Frederick's answer got me thingking and I wrote a simple gem deep_dirty that provides deep dirty checking by comparing current attribute values to those recast from *_before_type_cast
. To automate this on ActiveRecord models, the gem sets up a before_validation
callback.
Usage
gem 'deep_dirty'
class User < ActiveRecord::Base
include DeepDirty
end
user.roles << 'secretary'
user.changed? # => false
user.valid? # => true
user.changed? # => true
Also, deep checking can be initiated without validations:
user.changed? # => false
user.deep_changed? # => true
user.changed? # => true
Check out the source code at github: borgand/deep_dirty
Upvotes: 1
Reputation: 84132
Active Record tracks which columns have changed and only saves these to the database. This change tracking works by hooking onto the setter methods - mutating an object inplace isn't detected. For example
user.roles << "superuser"
wouldn't be detected as a change.
There are 2 ways around this. One is never to change any Active Record object attribute in place. In your case this would mean the slight clumsier
user.roles += ["superuser"]
If you can't/won't do this then you must tell Active Record what you have done, for example
user.roles.gsub!(...)
user.roles_will_change!
lets Active Record know that the roles attribute has changed and needs to be updated.
It would be nicer if Active Record dealt better with this - when change tracking came in array columns weren't supported (mysql had the lion's share of the attention at the time)
Yet another approach would be to mark such columns as always needing saving (much like what happens with serialised attributes) but you'd need to monkey patch activerecord for that.
Upvotes: 5