30

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

pguardiario
  • 53,827
  • 19
  • 119
  • 159
gjb
  • 6,237
  • 7
  • 43
  • 71
  • 3
    I am querying a 3rd party web service which returns XML that I am parsing to a hash. There will be a field in the model for each field returned by the service, but I need to be sure that if they decide to return any additional fields, this doesn't prevent the record from being created. There are over 100 fields, so mapping each field individually isn't an option. – gjb Nov 08 '10 at 21:42
  • so, you are reading in a XML file from someone and you are trying to inject that into a model? And you are also saying that you could have a large number of options, right? – cbrulak Nov 08 '10 at 21:53
  • 2
    Ok,so the answers provided are good technically. But, from a maintenance p.o.v. you should consider this carefully. Dependency injection is tricky. There are tons of that in Java and none of it is really that amazing. It adds complexity and imho makes your code more bittle and error-prone. I would consider adding in an object or functions(s) that parse/sort the needed fields and then create the models. This would be similar to a factory pattern or an abstract factory pattern. – cbrulak Nov 08 '10 at 22:34

8 Answers8

42

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| !@something.attributes.keys.member?(k.to_s) }
@something.save
jenjenut233
  • 1,938
  • 16
  • 13
  • I think you have an error, hash.select returns an array, not a hash. – Larry K Nov 08 '10 at 22:03
  • Yep you're right, should just use the reject option. I had that originally, was typing before thinking :P – jenjenut233 Nov 08 '10 at 22:04
  • 1
    Just to add, it seems `delete_if` is more efficient than `reject` as it works on the original copy of the hash rather than duplicating it. – gjb Nov 08 '10 at 23:07
  • You're correct, however you're modifying the in-memory object.. if that works for you, by all means use it and you don't need to create a new hash. It's explained very well in the Ruby docs. You can also use #reject! to do the same thing, however avoid setting a variable for this because if it doesn't reject anything it will return nil. I was just taking the long approach because some people are not aware of the effects of #delete_if and #reject! (i.e. modifying the original) but if you understand the outcome, then definitely use it where necessary. :) – jenjenut233 Nov 09 '10 at 00:40
  • How about: `hash.each{|k,v| @something[k] = v if @something.has_attribute?(k)}` – pguardiario Jun 06 '15 at 23:51
11

I use this frequently (simplified):

params.select!{|x| Model.attribute_names.index(x)}
Model.update_attributes(params)
user1919149
  • 111
  • 1
  • 2
  • 1
    You might want to use `x.to_s` because `attribute_names` is an Array of strings. In any case, +1 =) – Abdo Oct 28 '14 at 13:57
8

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)))
ironsand
  • 14,329
  • 17
  • 83
  • 176
  • That is a super helpful one-liner and it save me a ton of time. Thanks! – BeeZee Feb 28 '20 at 23:23
  • 2
    I don't understand why this answer doesn't have more votes. It's much simpler and railsy. Are you sure about the ```.map(&:to_sym)``` part though? I managed to make it work with just ```Foo.create(hash.slice(*Foo.column_names))``` – Alberto T. Mar 13 '20 at 12:00
  • @AlbertoT. I guess `.map(&:to_sym)` is needed if your hash keys are symbols `{a: ´some', ...` but is not needed if keys are strings `{'a' => 'some', ...` – Paulo Belo Mar 22 '23 at 09:37
7

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
Amir Rubin
  • 850
  • 7
  • 11
6

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
Larry K
  • 47,808
  • 15
  • 87
  • 140
  • Thanks for your answer. I added additional details above while you were replying. Is it possible to do the reverse of this and delete "all attributes except for ..." from a hash? – gjb Nov 08 '10 at 21:50
  • 1
    if you downvoted this one, please add a comment why. This solution seems pretty good. – cbrulak Nov 08 '10 at 22:02
  • 1
    Because in the OP's comments, he mentions "I am querying a 3rd party web service which returns XML that I am parsing to a hash." ... so there is no incoming data array, he also mentions he has over 100 fields so manually remapping attributes is not an option. (Downvoted prior to your delete_if edit) Regardless, with over 100 fields, he should just pull the valid attribute keys off of the record, not create an array of "keepers" (since Rails already does this logic for us). – jenjenut233 Nov 08 '10 at 22:07
  • 2
    @jenjenut233: A bit harsh. Typically, we down vote for wrong answers, answers that recommend something dangerous, etc. If an answer does not answer the question to the OP's satisfaction, then the Op does not give it a check mark. Remember that an answer can be useful or helpful (and thus worthy of being up voted), even if it does not completely address the OP. Finally, remember that XML of a set of data will usually be parsed and used within your Ruby/Rails program as an array of records. – Larry K Nov 08 '10 at 22:13
  • The answer seemed to completely ignore the OP's request for a solution. Don't take it personal, but your answer goes so far out of the way to solve the problem that it actually causes more convolution and an upvote would simply be misleading in this scenario. – jenjenut233 Nov 08 '10 at 22:16
  • 2
    I disagree that the answer provides a "far out of the way" solution. The only difference between this and the answer I have accepted is that the other solution utilises the attributes from the model rather than requiring a separate array of keepers. This answer is useful however, so I have voted it up. – gjb Nov 08 '10 at 22:44
4

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).

CambridgeMike
  • 4,562
  • 1
  • 28
  • 37
  • I like this solution. Sometimes, it might be useful to delete an item from the incoming hash (i.e. the id) as such: `FooModel.create_from_hash( hash.except('id') )` – Abdo Oct 28 '14 at 14:07
2

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.

afournier
  • 323
  • 2
  • 6
0

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.

originalhat
  • 1,676
  • 16
  • 18