Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ActiveRecord can't do it, Arel can!

Prathamesh Sonpatki
January 10, 2015
3.2k

ActiveRecord can't do it, Arel can!

Talk given at GCRC 2015 with @vipulnsward

Prathamesh Sonpatki

January 10, 2015
Tweet

Transcript

  1. class Location < ActiveRecord::Base has_many :nearby_locations, class_name: 'Location', foreign_key: 'parent_location_id'

    has_many :reviews has_many :bookings end class Booking < ActiveRecord::Base belongs_to :location belongs_to :traveler end
  2. 30+ active Users ➜ ~ heroku run rails console --app

    backpackerz Running rails console attached to terminal... up, run.6589 Loading production environment (Rails 4.1.1) irb(main):001:0> User.pluck(:email).uniq.count => 31
  3. Searching for Indian food! class Restaurant < ActiveRecord::Base def self.food_search_raw

    search_term where("cuisine ILIKE ? OR cuisine ILIKE ?", '%indian%', "%#{search_term}%" ) end end
  4. Searching for Indian food! Restaurant.food_search_raw(“india”) #=> # SELECT restaurants.* FROM

    restaurants # WHERE (cuisine ILIKE '%indian%' OR cuisine ILIKE '%india%')
  5. Arel.is_a? • SQL Generator for Ruby • Based on Relational

    Algebra • Used by ActiveRecord under the hood(Rails 3+) • Lots of features not exposed through ActiveRecord API
  6. Arel.is_a? • SQL Generator for Ruby • Based on Relational

    Algebra • Used by ActiveRecord under the hood(Rails 3+) • Lots of features not exposed through ActiveRecord API • Largely Undocumented
  7. Arel != ActiveRecord • Job of Arel is to just

    generate SQL. • Arel does not know about database. • Arel does not execute the generated SQL.
  8. How to use Arel? Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY

    s.name = 'activerecord' .... s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version s.add_dependency 'arel', '7.0.0.alpha' end
  9. Arel table def arel_table @arel_table ||= Arel::Table.new(table_name) end Contact.arel_table =>

    #<Arel::Table:0x007fad4bd15fb0 @name="contacts", @engine=Contact(id: integer, user_id: integer, contact_type: string, data: string, created_at: datetime, updated_at: datetime), @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>
  10. Searching for Indian food! class Restaurant < ActiveRecord::Base def self.food_search

    search_term where(table[:cuisine].matches_any(['%indian%', "%#{search_term}%"])) end end # SELECT restaurants.* FROM restaurants # WHERE ((restaurants.cuisine ILIKE '%indian%' OR # restaurants.cuisine ILIKE '%india%'))
  11. Searching for Indian food class Restaurant < ActiveRecord::Base def self.food_search

    search_term where(table[:cuisine].matches_any(['%indian%',"%#{search_term}%"])) end end # SELECT restaurants.* FROM restaurants # WHERE ((restaurants.cuisine ILIKE '%indian%' OR restaurants.cuisine # ILIKE '%india%')) where(a: something) where(“a like ?”, something) where(AST)
  12. Searching for Indian food class Restaurant < ActiveRecord::Base def self.food_search

    search_term where(table[:cuisine].matches_any(['%indian%',"%#{search_term}%"])) end end # SELECT restaurants.* FROM restaurants # WHERE ((restaurants.cuisine LIKE '%indian%' OR restaurants.cuisine # LIKE '%india%')) Same query on MySQL database
  13. I don’t want non-vegetarian food! class Restaurant < ActiveRecord::Base def

    self.food_search_not search_term where("cuisine_type NOT ILIKE '%nonveg%'") end end # SELECT restaurants.* FROM restaurants WHERE # (restaurants.cuisine_type NOT ILIKE '%nonveg%')
  14. I don’t want non-vegetarian food class Restaurant < ActiveRecord::Base def

    self.food_search_not search_term where(table[:cuisine_type].does_not_match('%nonveg%')) end end # SELECT restaurants.* FROM restaurants WHERE # (restaurants.cuisine_type NOT ILIKE '%nonveg%')
  15. List of all predicates • eq • eq_any • eq_all

    • in • in_any • in_all • matches • matches_all • matches_any • not_eq • not_eq_any • not_eq_all • gt/lt • gteq/lteq • gteq_any/lteq_any • gteq_all/lteq_all • gt_any/lt_any • gt_all/lt_all • not_in • not_in_any • not_in_all • does_not_match • does_not_match_any • does_not_match_all
  16. Locations with bookings && rating > 3 [ Location.joins("INNER JOIN

    reviews ON locations.id = reviews.location_id AND reviews.rating > ?", rating), Location.joins(:bookings) ].map(&:to_sql).join(" INTERSECT ")
  17. Locations with bookings && rating > 3 # SELECT *

    FROM locations # INNER JOIN reviews ON locations.id = reviews.location_id # AND reviews.rating > 3 INTERSECT # SELECT locations.* FROM locations INNER JOIN bookings ON # bookings.location_id = locations.id
  18. Reviews with rating > 3 # All reviews having rating

    greater than 3 class Review def self.having_rating_more_than rating table[:rating].gt(rating) end end #=> "\"reviews\".\"rating\" > 3"
  19. Locations with bookings && rating > 3 Location.table.join(Review.table) .on(Location.table[:id].eq(Review.table[:location_id]) .and(Review.having_rating_more_than(3)))

    .project(Arel.star) #=> “*” #=> "SELECT * FROM \"locations\" INNER JOIN \"reviews\" ON \" locations\".\"id\" = \"reviews\".\"location_id\" AND \" reviews\".\"rating\" > 3"
  20. Locations with bookings && rating > 3 # All locations

    with reviews having rating more than 3 Location.table.join(Review.table) .on(Location.table[:id].eq(Review.table[:location_id]) .and(Review.having_rating_more_than(3))) .project(Arel.star) # All locations with bookings Location.joins(:bookings)
  21. Locations with bookings && rating > 3 # Combine them

    using intersect Location.table.join(Review.table) .on(Location.table[:id].eq(Review.table[:location_id]) .and(Review.having_rating_more_than(3))) .project(Arel.star) .intersect(Location.joins(:bookings))
  22. Locations with bookings && rating > 3 # SELECT *

    FROM locations INNER JOIN reviews # ON locations.id = reviews.location_id # AND reviews.rating > 3 # INTERSECT # SELECT locations.* FROM locations INNER JOIN bookings # ON bookings.location_id = locations.id
  23. Unions # SELECT * FROM locations INNER JOIN reviews #

    ON locations.id = reviews.location_id # AND reviews.rating > 3 # UNION # SELECT locations.* FROM locations INNER JOIN bookings # ON bookings.location_id = locations.id
  24. All hotels within 5 km of malls class Location <

    ActiveRecord::Base has_many :nearby_locations, class_name: 'Location', foreign_key: 'parent_location_id' ... end
  25. All hotels within 5 km of malls def self.all_nearby_locations_within_distance_from_parent(distance =

    5) joins("INNER JOIN locations nearby_locations ON locations.id = nearby_locations.parent_location_id AND nearby_locations.distance_from_parent <= #{distance}" ) end # SELECT "locations".* FROM "locations" INNER JOIN locations # nearby_locations ON locations.id = nearby_locations.parent_location_id # AND nearby_locations.distance_from_parent <= 5
  26. All hotels within 5 km of malls class Location <

    ActiveRecord::Base has_many :nearby_locations, class_name: 'Location', foreign_key: 'parent_location_id' ... end
  27. All hotels within 5 km of malls class Location <

    ActiveRecord::Base … nearby_locations = table.alias(“nearby_locations”) table.join(nearby_locations) .on(table[:id].eq(nearby_locations[:parent_location_id]) .join_sources #=> [Arel::Nodes::Join] end
  28. All hotels within 5 km of malls class Location <

    ActiveRecord::Base ... table.join(nearby_locations) .on(table[:id].eq(nearby_locations[:parent_location_id]) .and(nearby_locations[:distance_from_parent].lteq(5)) .join_sources end
  29. All hotels within 5 km of malls nearby_locations = table.alias

    Location.joins(table.join(nearby_locations) .on(table[:id].eq(nearby_locations[:parent_location_id]) .and(nearby_locations[:distance_from_parent].lteq(5)) .join_sources ) #=> SELECT "locations".* FROM "locations" INNER JOIN "locations" "nearby_locations" ON "locations"."id" = "nearby_locations"."parent_location_id" AND "nearby_locations"."distance_from_parent" <= 5
  30. Super Securez def set_sekret(value = 'lol', key = 'sekret') escape_and_execute(["SELECT

    pgp_sym_encrypt(?, ?)", value.to_s, key])['pgp_sym_encrypt'] end def get_sekret(value = 'lol', key = 'somesekret') escape_and_execute(["SELECT pgp_sym_decrypt(?, ?)", value, key])['pgp_sym_decrypt'] rescue raise 'lol; get it right bro' end def escape_and_execute(query) query = ::ActiveRecord::Base.send :sanitize_sql_array, query ::ActiveRecord::Base.connection.execute(query).first end
  31. Named functions def pgp_sym_encrypt(value = 'lol', key = 'sekret') Arel::Nodes::NamedFunction.new('pgp_sym_encrypt',

    [value, key]) end #=> pgp_sym_encrypt('lol', 'sekret') def arel_sekret(value = 'lol', key = 'sekret') User.select(pgp_sym_encrypt(value, key)) end #=> SELECT pgp_sym_encrypt('lol', 'sekret') FROM \"users\"
  32. Named functions def pgp_sym_encrypt(value = 'lol', key = 'sekret') Arel::Nodes::NamedFunction.new('pgp_sym_encrypt',

    [value, key], ‘pgp_encrypted’) end #=> pgp_sym_encrypt('lol', 'sekret') AS pgp_encrypted" def arel_sekret(value = 'lol', key = 'sekret') User.select(pgp_sym_encrypt(value, key)) end #=> SELECT pgp_sym_encrypt('lol', 'sekret') AS pgp_encrypted FROM \"users\"
  33. Other example of Named functions class Task < ActiveRecord::Base def

    self.order_criteria_raw "coalesce('completed_at', 'created_at')" end end Task.order(Task.order_criteria_raw) #=> SELECT "tasks".* FROM "tasks" ORDER BY coalesce('completed_at', 'created_at')
  34. Other example of Named functions class Task < ActiveRecord::Base def

    self.order_criteria Arel::Nodes::NamedFunction.new('coalesce', ['completed_at', 'created_at']) end end Task.order(Task.order_criteria_raw) #=> SELECT "tasks".* FROM "tasks" ORDER BY coalesce('completed_at', 'created_at')