#355 Hacking with Arel pro
Here I show a variety of ways to rewrite a long SQL query using only Active Record and Arel. This includes generating scopes dynamically, adding an "or" operator, and adding a powerful "match" method.
- Download:
- source codeProject Files in Zip (80.4 KB)
- mp4Full Size H.264 Video (38.7 MB)
- m4vSmaller H.264 Video (18.5 MB)
- webmFull Size VP8 Video (22.1 MB)
- ogvFull Size Theora Video (43 MB)
Resources
rails console
t = Product.arel_table t[:price].lt(10) Product.where(_) Product.where(t[:name].matches("%catan%")) Product.where(stock: 2).arel.class Product.where(stock: 2).arel.constraints _.first.or(Product.where(id: 1).arel.constraints.first) Product.where(_) Product.where(stock: 1) | Product.where(id: 1) Product.where(id: 1..5) - Product.where(id: 2) Product.match(stock: {lt: 3}) Product.match(stock: [nil, {lt: 3}]) Product.search("catan")
models/product.rb
# 1. Hash Arguments def self.search(query) where("released_at <= :now and (discontinued_at is null or discontinued_at > :now) and stock >= :stock and name like :query", now: Time.zone.now, stock: 2, query: "%#{query}%") end # 2. Named Scopes scope :released, -> { where("released_at > ?", Time.zone.now) } scope :not_discontinued, -> { where("discontinued_at is null or discontinued_at > ?", Time.zone.now) } scope :stock, -> { where("stock >= ?", 2) } scope :available, -> { released.not_discontinued.available } scope :search, ->(query) { available.where("name like ?", "%#{query}%") } # 3. Arel Match def self.search(query) available.where(arel_table[:name].matches("%#{query}%")) end # 4. Dynamic Scope Generation generate_scopes scope :not_discontinued, -> { discontinued_at_eq(nil) | discontinued_at_gt(Time.zone.now) } scope :available, -> { not_discontinued.released_at_lteq(Time.zone.now).stock_gteq(2) } def self.search(query) available.name_matches("%#{query}%") end # 5. Match Method def self.search(query) match( released_at: {lteq: Time.zone.now}, discontinued_at: [nil, {gt: Time.zone.now}], stock: {gteq: 2}, name: {matches: "%#{query}%"}, ) end
config/initializers/scope_generator.rb
module ScopeGenerator def generate_scopes columns.each do |column| predicates_for(column).each do |predicate| scope "#{column.name}_#{predicate}", ->(value) do where(arel_table[column.name].send(predicate, value)) end end end end def predicates_for(column) base = [:eq, :not_eq, :in, :not_in] case column.type when :integer, :float, :decimal, :date, :datetime, :timestamp, :time base + [:lt, :lteq, :gt, :gteq] when :string, :text base + [:matches, :does_not_match] else base end end end ActiveSupport.on_load :active_record do extend ScopeGenerator end
config/initializers/scope_operators.rb
module ScopeOperators def or(other) left = arel.constraints.reduce(:and) right = other.arel.constraints.reduce(:and) scope = merge(other) scope.where_values = [left.or(right)] scope end alias_method :|, :or def not(other) left = arel.constraints.reduce(:and) right = other.arel.constraints.reduce(:and) scope = merge(other) scope.where_values = [left, right.not] scope end alias_method :-, :not def and(*args) merge(*args) end alias_method :&, :and end ActiveSupport.on_load :active_record do ActiveRecord::Relation.send(:include, ScopeOperators) end
config/initializers/match_scope.rb
module MatchScope def match(matches) clause = matches.map do |attribute, conditions| Array.wrap(conditions).map do |predicates| predicates = predicates.kind_of?(Hash) ? predicates : {eq: predicates} predicates.map do |predicate, value| arel_table[attribute].send(predicate, value) end.reduce(:and) end.reduce(:or) end.reduce(:and) where(clause) end end ActiveSupport.on_load :active_record do extend MatchScope end