Adds various behavior to Active Record models relating to the model's attributes:
- Allows you to compare Active Record objects based on their attributes, which often makes more sense than the built-in
==
operator (which does its comparisons based on theid
attribute alone! — not always what you want!) - You can configure which attributes, if any, should be excluded from the comparison
- Provides a customizable inspect method, which by default excludes the same attributes that are excluded when doing a
same_attributes_as?
comparison
Consider a User model that holds the notion of users that have a name.
ActiveRecord::Schema.define do
create_table :addresses, :force => true do |t|
t.string :name
t.text :address
t.string :city
t.string :state
t.string :postal_code
t.string :country
t.timestamps
end
end
class Address < ActiveRecord::Base
end
Default ActiveRecord ==
behavior:
a = Address.new(address: 'B St.')
b = Address.new(address: 'B St.')
a == b # => false
Using same_as?
:
a = Address.new(address: 'B St.')
b = Address.new(address: 'B St.')
a.same_as?(b) # => true
a = Address.new(address: 'B St.')
b = Address.new(address: 'Nowhere Road')
a.same_as?(b) # => false
Using has_attribute_values?
:
a = Address.new(address: 'A St.', city: "Don't care")
a.has_attribute_values?(address: 'A St.') # => true
a.has_attribute_values?(address: 'A St.', city: 'Different') # => false
b = Address.new(address: 'B St.', city: "Don't care")
b.has_attribute_values?(address: 'A St.') # => false
b.has_attribute_values?({}) # => true
Add to your Gemfile
:
gem "active_record_ignored_attributes"
If you want to replace the default ActiveRecord ==
operator with the same_as?
behavior, you should be able to just override it, like this:
class ActiveRecord::Base
alias_method :==, :same_as?
end
By default, id
, created_at
, and updated_at
will be ignored (id
is not ignored by inspect_without_ignored_attributes
though).
If you want to add some ignored attributes to the default array ([:id, :created_at, :updated_at]
), you can override self.ignored_attributes
like so, referencing super
:
class Address < ActiveRecord::Base
def self.ignored_attributes
super + [:name]
end
end
If you want to override the defaults instead of appending to them, just don't reference super
:
class Address < ActiveRecord::Base
def self.ignored_attributes
[:id, :name]
end
end
Now that you've declared which attributes you don't really care about, how about making it so you don't have to see them in your inspect
output too? (The output from inspect
is verbose enough as it is!!)
object.inspect_without_ignored_attributes
will give you the same output as the default inspect
but without all those ignored attributes (except for id
— id
is always included, even if it's listed in ignored_attributes
)):
address.inspect_without_ignored_attributes # => "#<Address id: 1, address: nil, city: nil, country: nil, postal_code: nil, state: nil>"
# Compared to:
address.inspect # => "#<Address id: 1, name: nil, address: nil, city: nil, state: nil, postal_code: nil, country: nil, created_at: \"2011-08-19 18:07:39\", updated_at: \"2011-08-19 18:07:39\">"
But that is a lot to type every time. If you want inspect to always be more readable, you can override the ActiveRecord default like this:
class Address < ActiveRecord::Base
alias_method :inspect, :inspect_without_ignored_attributes
end
or even:
class ActiveRecord::Base
alias_method :inspect, :inspect_without_ignored_attributes
end
If you want to customize inspect further and specify exactly which attributes to show (and, optionally which delimiters to bracket the string with), you can use inspect_with
:
class Address < ActiveRecord::Base
def inspect
inspect_with([:city, :state, :country])
end
end
or:
class Address < ActiveRecord::Base
def inspect
inspect_with([:id, :name, :address, :city, :state, :postal_code, :country], ['{', '}'])
end
end
If you want to inspect with the same attributes as inspect_without_ignored_attributes plus some
additional attributes (or, more likely, some virtual attributes), you can just use the
attributes_for_inspect
method that inspect_without_ignored_attributes
uses, which automatically
excludes any attributes listed in ignored_attributes
:
class Address < ActiveRecord::Base
def inspect
inspect_with(attributes_for_inspect + [:virtual_attr_1, :virtual_attr_2])
end
end
This is useful because virtual attributes (methods in your model that aren't part of the "attributes" returned by record.attributes) won't be included by inspect_without_ignored_attributes by default.
This gem comes with a be_same_as
and have_attribute_values
matcher for RSpec.
Add this to your spec_helper.rb:
require 'active_record_ignored_attributes/matchers'
Then in your specs you can write such nicely readable expectations as:
expected = Address.new({city: 'City', country: 'USA'})
Address.last.should be_same_as(expected)
a = Address.new(address: 'B St.')
b = Address.new(address: 'B St.')
a.should be_same_as?(b) # passes
a = Address.new(address: 'B St.')
b = Address.new(address: 'Nowhere Road')
a.should be_same_as?(b) # fails
and it will lovingly do a diff for you and only show you the attributes in each object that actually differed:
expected: #<Address address: "Nowhere Road">
got: #<Address address: "B St.">
Or use should have_attribute_values
whenever it's more convenient to specify the expected attributes with a hash instead of building a new model instance:
a = Address.new( name: 'A', address: 'The Same Address', city: "Don't care")
a.should have_attribute_values name: 'A', address: 'A Slightly Different Address'
will fail with:
expected: {:name=>"A", :address=>"A Slightly Different Address"}
got: {:name=>"A", :address=>"The Same Address"}
The default ActiveRecord ==
behavior isn't always adequate, as you can probably tell from the example at the top, or by the fact that you are looking at this gem right now.
This is the implementation of ==
in ActiveRecord:
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
# is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
#
# Note that new records are different from any other record by definition, unless the
# other record is the receiver itself. Besides, if you fetch existing records with
# +select+ and leave the ID out, you're on your own, this predicate will return false.
#
# Note also that destroying a record preserves its ID in the model instance, so deleted
# models are still comparable.
def ==(comparison_object)
comparison_object.equal?(self) ||
(comparison_object.instance_of?(self.class) &&
comparison_object.id == id && !comparison_object.new_record?)
end
That implementation is often fine when you are dealing with saved records, but isn't helpful at all when one or both of the objects being compared is not-yet-saved.
If you want to compare two model instances based on their attributes, you will probably want to exclude certain irrelevant attributes from your comparison, such as: id
, created_at
, and updated_at
. (I would consider those to be more metadata about the record than part of the record's data itself.)
This might not matter when you are comparing two new (unsaved) records (since id
, created_at
, and updated_at
will all be nil
until saved), but I sometimes find it necessary to compare a saved object with an unsaved one (in which case == would give you false since nil != 5). Or I want to compare two saved objects to find out if they contain the same data (so the ActiveRecord ==
operator doesn't work, because it returns false if they have different id
's, even if they are otherwise identical).
See also: http://stackoverflow.com/questions/4738439/how-to-test-for-activerecord-object-equality
- Does such a gem already exist?
- All I've found so far is https://github.com/GnomesLab/active_record_attributes_equality
- What should the method be called? I don't think overriding the existing
==
operator is a good idea (as done in active_record_attributes_equality), so for now I'm calling itsame_attributes_as?
(aliased assame_as?
since that's easier to type). Other runners up werepractically_same_as?
andattributes_eql?
.
Possible improvements:
- Allow the default to be overridden with a class macro like
ignore_for_attributes_eql :last_signed_in_at, :updated_at
Also, perhaps you want to set the default ignored attributes for a model but still wish to be able to override these defaults as needed...
address.same_as?(other_address, :ignore => [:addressable_type, :addressable_id])
address.same_as?(other_address, :only => [:city, :state, :country])
Comments and contributions are welcome.
Please feel free to fork the project at http://github.com/TylerRick/active_record_ignored_attributes and to send pull requests.
Bugs can be reported at https://github.com/TylerRick/active_record_ignored_attributes/issues
Copyright 2011, Tyler Rick
This is free software, distributed under the terms of the MIT License.