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

ActiveRecord can't do it, Arel can!

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Prathamesh Sonpatki Prathamesh Sonpatki
January 10, 2015
3.2k

ActiveRecord can't do it, Arel can!

Talk given at GCRC 2015 with @vipulnsward

Avatar for Prathamesh Sonpatki

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')