1

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.

2 Answers 2

5

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.

Sign up to request clarification or add additional context in comments.

Comments

1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.