gjb
gjb

Reputation: 6317

Rails: Ignoring non-existant attributes passed to create()

I have the following Rails model:

class CreateFoo < ActiveRecord::Migration
  def self.up
    create_table :foo do |t|
      t.string :a
      t.string :b
      t.string :c
      t.timestamps
    end
  end

  def self.down
    drop_table :foo
  end
end

If I try and create a new record with an additional non-existent attribute, this produces an error:

Foo.create(a: 'some', b: 'string', c: 'foo', d: 'bar')
ActiveRecord::UnknownAttributeError: unknown attribute: d

Is there a way I can get create() to ignore attributes that don't exist in the model? Alternatively, what is the best way to remove non-existent attributes prior to creating the new record?

Many thanks

Upvotes: 31

Views: 20613

Answers (8)

ironsand
ironsand

Reputation: 15141

You can use Hash#slice and column_names method exists also as class method.

hash = {a: 'some', b: 'string', c: 'foo', d: 'bar'}
Foo.create(hash.slice(*Foo.column_names.map(&:to_sym)))

Upvotes: 10

originalhat
originalhat

Reputation: 1716

I found a solution that works fairly well, and is ultimately a combination of the above options. It allows for invalid params to be passed (and ignored), while valid ones are mapped correctly to the object.

def self.initialize(params={})
  User.new(params.reject { |k| !User.attribute_method?(k) })
end

Now rather than calling User.new(), call User.initialize(). This will "filter" the correct params fairly elegantly.

Upvotes: 0

CambridgeMike
CambridgeMike

Reputation: 4622

I came up with a solution that looks like this, you might find it helpful:

def self.create_from_hash(hash)
  hash.select! {|k, v| self.column_names.include? k }
  self.create(hash)
end

This was an ideal solution for me, because in my case hash was coming from an ideal data source which mirrored my schema (except there were additional fields).

Upvotes: 4

user1919149
user1919149

Reputation: 111

I use this frequently (simplified):

params.select!{|x| Model.attribute_names.index(x)}
Model.update_attributes(params)

Upvotes: 11

Amir Rubin
Amir Rubin

Reputation: 860

I just had this exact problem upgrading to Rails 3.2, when I set:

config.active_record.mass_assignment_sanitizer = :strict

It caused some of my create! calls to fail, since fields that were previously ignored are now causing mass assignment errors. I worked around it by faking the fields in the model as follows:

attr_accessor   :field_to_exclude
attr_accessible :field_to_exclude

Upvotes: 7

afournier
afournier

Reputation: 323

I think using the attr_accessible method in the model class for Foo would achieve what you want, e.g.,:

class Foo < ActiveRecord::Base

  attr_accessible :a, :b, :c

  ...
end

This would allow the setting/updating of only those attributes listed with attr_accessible.

Upvotes: 2

jenjenut233
jenjenut233

Reputation: 1938

Trying to think of a potentially more efficient way, but for now:

hash = { :a => 'some', :b => 'string', :c => 'foo', :d => 'bar' }
@something = Something.new
@something.attributes = hash.reject{|k,v| [email protected]?(k.to_s) }
@something.save

Upvotes: 42

Larry K
Larry K

Reputation: 49104

Re: Is there a way I can get create() to ignore attributes that don't exist in the model? -- No, and this is by design.

You can create an attr_setter that will be used by create --

attr_setter :a # will silently absorb additional parameter 'a' from the form.

Re: Alternatively, what is the best way to remove non-existent attributes prior to creating the new record?

You can remove them explicitly:

params[:Foo].delete(:a) # delete the extra param :a

But the best is to not put them there in the first place. Modify your form to omit them.

Added:

Given the updated info (incoming data), I think I'd create a new hash:

incoming_data_array.each{|rec|
  Foo.create {:a => rec['a'], :b => rec['b'], :c => rec['c']} # create new
                                                              # rec from specific
                                                              # fields
}

Added more

# Another way:
keepers = ['a', 'b', 'c'] # fields used by the Foo class.

incoming_data_array.each{|rec|
  Foo.create rec.delete_if{|key, value| !keepers.include?(key)} # create new rec
}                                                               # from kept
                                                                # fields

Upvotes: 6

Related Questions