I found a bug in my code today that stems from my not remembering that ActiveRecord::Base will try to coerce values set on its attributes into whatever type is defined in the DB. Specifically I got burned by #id= coercing a String into a Fixnum. Check this out:
1 2 3 4 5 6 |
[10] pry(main)> record = ActiveRecordSubclass.new => #<ActiveRecordSubclass:0x00555d2e588a28 ... > [11] pry(main)> record.id = "09378476376282" => "09378476376282" [12] pry(main)> record.id => 9378476376282 |
Not only is the type of object changed by setting the attribute but the leading zero is lost. Luckily my specs caught this.
I know, I know it’s a bad idea to set #id yourself. You should let ActiveRecord do that for you. In my case I was trying to cleverly avoid manipulating incoming attributes before assignment. The API I’m working with returns JSON records that have their own id field (probably a common case). I was mass assigning these attributes to a new ActiveRecord object and letting a callback, before validation, move the API id from #id into #api_id.
1 2 3 4 5 6 7 8 9 |
# attribute 'api_id' is keyed as 'id' in the API responses # if 'id' attribute is changed during mass assignment # reset to its original value def convert_id_to_api_id change_set = changes return unless change_set.has_key? 'id' self.id = change_set['id'].first self.api_id = change_set['id'].last end |
I thought it was safe because A) the only time #id would appear in #changes would be after an API mass assignment and B) I just assumed that because these are brand new Ruby objects that they would behave as such and not care about the type of object that is set on an attribute (at least while it is still a #new_record?) . Obviously I was wrong and this was a bad idea. I double checked the ActiveRecord docs and there is no mention of type coercion. While doing so I came across #id_before_type_cast and discovered its useful behavior:
1 2 3 4 5 6 |
[10] pry(main)> record = ActiveRecordSubclass.new => #<ActiveRecordSubclass:0x00555d2e588a28 ... > [11] pry(main)> record.id = "09378476376282" => "09378476376282" [12] pry(main)> record.id_before_type_cast => "09378476376282" |
Good to know, but still better to just avoid using #id=.
Got questions or feedback? I want it. Drop your thoughts in the comments below or hit me @ccstump. You can also follow me on Twitter to be aware of future articles.
Thanks for reading!
Comments