Skip to content

Allows you to compare Active Record objects based on their *attributes* (with same_attributes_as? and has_attribute_values?), to exclude some attributes from being used in comparison, and adds improved inspect method

vinett-de/active_record_ignored_attributes

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ActiveRecord Ignored Attributes

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 the id 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

Example

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

Installing

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

Configuring which attributes are ignored

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

inspect_without_ignored_attributes

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 idid 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

Customizable inspect method

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.

RSpec

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"}

Motivation

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

Questions and Ideas

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

Contributing

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

License

Copyright 2011, Tyler Rick

This is free software, distributed under the terms of the MIT License.

About

Allows you to compare Active Record objects based on their *attributes* (with same_attributes_as? and has_attribute_values?), to exclude some attributes from being used in comparison, and adds improved inspect method

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Ruby 100.0%