ActiveModel::Relation
Query a collection of ActiveModel objects like an ActiveRecord::Relation.
Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add active_model-relation
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install active_model-relation
Usage
Initialization
Create a new relation by passing the model class and a collection:
relation = ActiveModel::Relation.new(Project, [
Project.new(id: 1, state: 'draft', priority: 1),
Project.new(id: 2, state: 'running', priority: 2),
Project.new(id: 3, state: 'completed', priority: 3),
Project.new(id: 4, state: 'completed', priority: 1)
])As an alternative, it's also possible to create a collection for a model without explicitly passing a collection.
In this case, the library will attempt to call Project.records to get the default collection. If the method doesn't exist or returns nil, the collection will default to an empty array.
class Project
def self.records
[
Project.new(id: 1, state: 'draft', priority: 1),
Project.new(id: 2, state: 'running', priority: 2),
Project.new(id: 3, state: 'completed', priority: 3),
Project.new(id: 4, state: 'completed', priority: 1)
]
end
end
relation = ActiveModel::Relation.new(Project)Querying
An ActiveModel::Relation can be queried almost exactly like an ActiveRecord::Relation.
#find
You can look up a record by it's primary key, using the find method. If no record is found, it will raise a ActiveModel::RecordNotFound error.
project = relation.find(1)By default, ActiveModel::Relation will assume :id as the primary key. You can customize this behavior by setting a primary_key on the model class.
class Project
def self.primary_key = :identifier
endWhen passed a block, the find method will behave like Enumerable#find.
project = relation.find { |p| p.id == 1 }#find_by
To look up a record based on a set of arbitary attributes, you can use find_by. It accepts the same arguments as #where and will return the first matching record.
project = relation.find_by(state: 'draft')#where
To filter a relation, you can use where and pass a set of attributes and the expected values. This method will return a new ActiveModel::Relation that only returns the matching records, so it's possible to chain multiple calls. The filtering will only happen when actually accessing records.
relation.where(state: 'completed')The following two lines will return the same filtered results:
relation.where(state: 'completed', priority: 1)
relation.where(state: 'completed').where(priority: 1)To allow for more advanced filtering, #where allows filtering using a block. This works similar to Enumerable#select, but will return a new ActiveModel::Relation instead of an already filtered array.
relation.where { |p| p.state == 'completed' && p.priority == 1 }#where.not
Similar to #where, the #where.not chain allows you to filter a relation. It will also return a new ActiveModel::Relation with that returns only the matching records.
relation.where.not(state: 'draft')To allow for more advanced filtering, #where.not allows filtering using a block. This works similar to Enumerable#reject, but will return a new ActiveModel::Relation instead of an already filtered array.
relation.where.not { |p| p.state == 'draft' && p.priority == 1 }Sorting
It is possible to sort an ActiveModel::Relation by a given set of attribute names. Sorting will be applied after filtering, but before limits and offsets.
#order
To sort by a single attribute in ascending order, you can just pass the attribute name to the order method.
relation.order(:priority)To specify the sort direction, you can pass a hash with the attribute name as key and either :asc, or :desc as value.
relation.order(priorty: :desc)To order by multiple attributes, you can pass them in the order of specificity you want.
relation.order(:state, :priority)For multiple attributes, it's also possible to specify the direction.
relation.order(state: :desc, priority: :asc)Limiting and offsets
#limit
To limit the amount of records returned in the collection, you can call limit on the relation. It will return a new ActiveModel::Relation that only returns the given limit of records, allowing you to chain multiple other calls. The limit will only be applied when actually accessing the records later on.
relation.limit(10)#offset
To skip a certain number of records in the collection, you can use offset on the relation. It will return a new ActiveModel::Relation that skips the given number of records at the beginning. The offset will only be applied when actually accessing the records later on.
relation.offset(20)Scopes
After including ActiveModel::Relation::Model, the library also supports calling class methods defined on the model class as part of the relation.
class Project
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Relation::Model
attribute :id, :integer
attribute :state, :string, default: :draft
attribute :priority, :integer, default: 1
def self.completed
where(state: 'completed')
end
endGiven the example above, you can now create relations like you're used to from ActiveRecord::Relation.
projects = Project.all
completed_projects = all_projects.completed
important_projects = all_projects.where(priority: 1)Spawning
It's possilbe to create new versions of a ActiveModel::Relation that only includes certain aspects of the ActiveModel::Relation it is based on. It's currently possible to customize the following aspects: :where, :limit, :offset.
#except
To create a new ActiveModel::Relation without certain aspects, you can use except and pass a list of aspects, you'd like to exclude from the newly created instance. The following example will create a new ActiveModel::Relation without any previously defined limit or offset.
relation.except(:limit, :offset)#only
Similar to except, the only method will return a new instance of the ActiveModel::Relation it is based on but with only the passed list of aspects applied to it.
relation.only(:where)Extending relations
#extending
In order to add additional methods to a relation, you can use extending. You can either pass a list of modules that will be included in this particular instance, or a block defining additional methods.
module Pagination
def page_size = 25
def page(page)
limit(page_size).offset(page.to_i * page_size)
end
def total_count
except(:limit, :offset).count
end
end
relation.extending(Pagination)The following example is equivalent to the example above:
relation.extending do
def page_size = 25
def page(page)
limit(page_size).offset(page.to_i * page_size)
end
def total_count
except(:limit, :offset).count
end
endDevelopment
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/userlist/active_model-relation. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
Acknowledgements
This library is heavily inspired by ActiveRecord::Relation and uses similar patterns and implementations in various parts.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the ActiveModel::Relation project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
What is Userlist?
Userlist allows you to onboard and engage your SaaS users with targeted behavior-based campaigns using email or in-app messages.
Userlist was started in 2017 as an alternative to bulky enterprise messaging tools. We believe that running SaaS products should be more enjoyable. Learn more about us.