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
end
When 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
end
Given 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
end
Development
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.