# Query Objects ## How I use Query objects in Rails applications -- ## Who am I? [![Ruby on Rails freelance](/assets/images/avatar-dccc6af7.png)](http://cecilitse.org/) Cecile Rails dev / coach
Teacher at [Le Wagon](https://www.lewagon.com/)
Remote worker --- ## The Rails Way (aka everything in the Model) ``` class Artist < ActiveRecord::Base scope :available, -> { where(available: true) } scope :by_genre, -> (genre) { where(genre: genre) } end ``` -- ## Why Query Objects? * Keep skinny models * Extract queries from models * Possibility to have a query per model, context, etc. --- ## Query Objects I'm not using any new/fancy gem. -- ## Initializing Query Object ``` # app/queries/artist_query.rb class ArtistQuery attr_reader :relation def initialize(base_relation=nil) base_relation = Artist.all unless base_relation @relation = base_relation.extending(Scopes) end module Scopes # where your "scopes" will go end end ``` -- ## Defining Scopes ``` # app/queries/artist_query.rb class ArtistQuery # here lies initialization code module Scopes def available where(available: true) end def by_genre(genre) where(genre: genre) end end end ``` -- ## Using Query ``` # All artists ArtistQuery.new.relation # Available artists ArtistQuery.new.relation.available # Metal artists ArtistQuery.new.relation.by_genre("Metal") ``` --- ## Refactoring Making it generic -- ## Base Query Object ``` class BaseQuery @model = nil attr_reader :relation def initialize(base_relation=nil) base_relation = self.class.model.all unless base_relation @relation = base_relation.extending(self.class::Scopes) end def self.model @model end def self.relation(base_relation=nil) query = new(base_relation) query.relation end module Scopes # any new scope should be implemented there. end end ``` -- ## Inherit from base Query ``` class ArtistQuery < BaseQuery @model = Artist module Scopes def available where(available: true) end def by_genre(genre) where(genre: genre) end end end ``` -- ## Usage ``` # All artists ArtistQuery.relation # Available artists ArtistQuery.relation.available # Metal artists ArtistQuery.relation.by_genre("Metal") ``` -- ## Icing on the cake You can still **chain queries** with any kind of ActiveRecord relation. ``` ArtistQuery.relation.available.limit(20) ``` --- ## Associations Let's say we have a Label model. ``` class Label < ActiveRecord::Base has_many :artists end ``` -- ## Using Query Objects ``` label = Label.find(params[:id]) artists = label.artists # Available artists for a label ArtistQuery.relation(artists).available ``` --- ## Performance But **extending** an ActiveRecord::Relation is **not performant, at all**. Each time we extend, i.e. each time we instantiate a Query Object, the Ruby global method cache is invalidated. [More info here](http://dev.mensfeld.pl/2015/04/ruby-global-method-cache-invalidation-impact-on-a-single-and-multithreaded-applications/) -- ## Optimization Let's delegate ActiveRecord queries to ActiveRecord::Relation by using **SimpleDelegator**. -- ## Base Query ``` # app/queries/base_query.rb require 'delegate' class BaseQuery < SimpleDelegator def self.relation(base_relation, base_model) base_relation ||= base_model.all new(base_relation) end def self.method_added(method_name) return if @redefining @redefining = true alias_method :"_#{method_name}", method_name define_method method_name do |*args, &block| result = send(:"_#{method_name}", *args, &block) if result.class == __getobj__.class self.class.new(result) else result end end @redefining = false end end ``` -- ## Artist Query ``` class ArtistQuery < BaseQuery def self.relation(base_relation=nil) super(base_relation, Artist) end def available where(available: true) end def by_genre(genre) where(genre: genre) end end ``` -- ## Benchmark It's even twice faster than the extend implementation. ``` 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 ``` --- ## Pros & Cons
Pros
Cons
Model scopes
The Rails Way
Scopes lost in the middle of your model class/instance/DSL methods
Query objects
Separation of concerns
More overhead
--- ## Happy Hacking! [Query Objects example](https://github.com/cveneziani/query-objects-example/) [@cecilitse](http://twitter.com/cecilitse)