Skip to content

cveneziani/query-objects-example

Repository files navigation

Query Objects - Example

This is an example of a rails application that defines and uses Query Objects.

Getting started

Have a look at app/queries/ folder.

3 implementations are provided:

  • Delegating to ActiveRecord::Relation (default one)
  • Delegating to ActiveRecord::Relation with chaining ActiveRecord conditions in between
  • Extending ActiveRecord::Relation

Usage

Defining a Query Object

class ArtistQuery < BaseQuery
  # defines the default model on which queries will be made
  def self.relation(base_relation=nil)
    super(base_relation, Artist)
  end

  # a first scope
  def available
    where(available: true)
  end

  # another scope
  def by_genre(genre)
    where(genre: genre)
  end
end

Making queries

ArtistQuery.relation
# =>  Returns all artists.
#     Based on `all` relation provided by `ActiveRecord`).

ArtistQuery.relation.available
# =>  Returns all available artists.
#     Based on `available` scope method provided by `ArtistQuery`.

ArtistQuery.relation.available.by_genre('Metal')
# =>  Returns all available Metal artists.
#     Based on `available` and `by_genre(name)` scope methods provided by `ArtistQuery`.

ArtistQuery.relation.available.by_genre('Metal').order(:name)
# =>  Returns all available metal artists ordered by name.
#     Based on `available` and `by_genre(name)` scope methods provided by `ArtistQuery`
#     and based on `order` method provided by `ActiveRecord`.

awesome_label = Label.first
ArtistQuery.relation(awesome_label.artists).available
# =>  Returns all available artists for awesome label.
#     Based on the following association: `label` has many `artists`.

Chaining with ActiveRecord conditions in between

By default, this feature is not provided.

# PROVIDED
ArtistQuery.relation.available.order(:name)

# NOT PROVIDED
ArtistQuery.relation.order(:name).available
# =>  NoMethodError:
#     undefined method `available' for #<Artist::ActiveRecord_Relation:0x000000033208b8>

To enable this feature - which isn't recommended - switch to the second implementation.

Why isn't it recommended?

The purpose of query objects is to extract scopes from models and to have relevant queries. Thus introducing ActiveRecord conditions between relevant queries is an anti-pattern. Try to avoid this behaviour.

Tests

There are tests for the 3 implementations. To run the tests:

$ rspec

Benchmark

A benchmark is provided between 2 implementations: delegator (default one) and extend. Just run the following command:

$ rake benchmark

Results

Warming up --------------------------------------
delegator -- without model 12.047k i/100ms
delegator -- with model    15.510k i/100ms
extend -- without model     8.431k i/100ms
extend -- with model        8.397k i/100ms
Calculating -------------------------------------
delegator -- without model 170.047k (± 3.8%) i/s -    855.337k in   5.037890s
delegator -- with model    169.862k (± 3.5%) i/s -    853.050k in   5.029212s
extend -- without model     77.498k (±12.7%) i/s -    379.395k in   5.009203s
extend -- with model        81.004k (±14.7%) i/s -    394.659k in   5.005465s

Comparison:
delegator -- without model: 170047.0 i/s
delegator -- with model:    169862.0 i/s - same-ish: difference falls within error
extend -- with model:        81004.4 i/s - 2.10x slower
extend -- without model:     77497.9 i/s - 2.19x slower

Running the app

Requirements

  • Ruby 2.4+
  • SQLite

Installation

$ bundle install
$ rake db:create db:migrate db:seed

Credits

Thanks to Bert Goethals for his help in optimizing the Query Objects.

License

This project is released under the WTFPL License.