W4rQCC1yGh
W4rQCC1yGh

Reputation: 2219

Adding element to postgres array field fails while replacing the whole array works

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

Answers (2)

Laas
Laas

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

Frederick Cheung
Frederick Cheung

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

Related Questions