Query a collection of ActiveModel objects like an ActiveRecord::Relation.
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
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)
An ActiveModel::Relation
can be queried almost exactly like an ActiveRecord::Relation
.
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 }
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')
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 }
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 }
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.
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)
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)
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)
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)
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
.
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)
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)
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
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.
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.
This library is heavily inspired by ActiveRecord::Relation
and uses similar patterns and implementations in various parts.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the ActiveModel::Relation project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
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.