@danshultz • das0118@gmail.com
Beer.find_by_name_and_abv("Two Hearted Ale", 7.0)
Beer.find_all_by_abv(7.0)
Beer.find(:all, conditions: ["abv < ?", 7])
Beer.where("abv between ? and ?", 5.5, 7)
Beer.where(abv: 5.5..7)
record = self.handles.first(
:conditions => {
:value_type => value.class.name,
:value_id => value.id
})
Discussion.find(id).comments.joins(:conversation)
.where("conversations.person_id" => person_id)
Discussion.where("owner" => person_id,
:deleted => false,
"created_on > ?" => 2.weeks.ago)
joins = "INNER JOIN channel_groups cg ON cg.id = discussions.channel_group_id"
joins = "#{joins} INNER JOIN channel_group_people cgp ON cgp.channel_group_id = cg.id" unless as_admin
conditions = "cg.channel_id = #{channel.id}
AND discussions.widget_id = #{for_widget_id ? for_widget_id : 'NULL'}"
conditions = "#{conditions} AND cgp.person_id = #{person.id}" unless as_admin
order = "discussions.updated_at DESC"
find(:all, :joins => joins, :conditions => conditions, :order => order, :limit => limit)
stmt = "SELECT DISTINCT
GROUP_CONCAT(DISTINCT UCASE(CAST(LEFT(AES_DECRYPT(p.last_name, 'xxx'), 1) AS CHAR))) AS letters
FROM channel_groups cg
INNER JOIN channel_group_people cgp ON cgp.channel_group_id = cg.id
INNER JOIN people p ON p.id = cgp.person_id
INNER JOIN channel_membership_attribute_person_values cmapv1 ON p.id = cmapv1.person_id
INNER JOIN channel_membership_attribute_values cmav1 ON cmav1.id = cmapv1.channel_membership_attribute_value_id
INNER JOIN channel_membership_attributes cma1 ON cma1.id = cmav1.channel_membership_attribute_id AND cma1.channel_id = cg.channel_id
INNER JOIN channel_membership_attribute_person_values cmapv2 ON p.id = cmapv2.person_id
INNER JOIN channel_membership_attribute_values cmav2 ON cmav2.id = cmapv2.channel_membership_attribute_value_id
INNER JOIN channel_membership_attributes cma2 ON cma2.id = cmav2.channel_membership_attribute_id AND cma2.channel_id = cg.channel_id
#{if self.show_unregistered_people_in_directory? then
' left join accounts a on a.person_id = p.id'
else
' inner join accounts a on a.person_id = p.id'
end}
WHERE cg.id = #{group_id}"
clause = if (first_attr_id && second_attr_id)
" AND cmav1.id = #{first_attr_id} AND cmav2.id = #{second_attr_id} GROUP BY cmav1.id, cmav2.id"
elsif (first_attr_id)
...
That last query was used to create an address book style initial list.
[A, G, H, K, S, X]
...it's now this
people = Person.in_channel_group(group_id)
if !show_unregistered_people_in_directory?
people = people.registered
end
attribute_ids = [first_attr_id, second_attr_id].compact
if !attribute_ids.empty?
people = people.with_channel_attribute_ids(attribute_ids)
end
people \
.order(Person.column_last_initial)
.uniq \
.pluck(Person.column_last_initial('last_initial').to_sql)
The Person Model has relationships setup properly
class Person < ActiveRecord::Base
has_many :channel_group_people, :dependent => :destroy
has_many :channel_groups,
:through => :channel_group_people
has_many :channels,
:through => :channel_groups
has_many :channel_membership_attribute_person_values
has_many :channel_membership_attribute_values,
:through => :channel_membership_attribute_person_values
has_many :channel_membership_attributes,
:through => :channel_membership_attribute_values
has_one :account
# ...
end
and scopes created
class Person < ActiveRecord::Base
def self.in_channel_group(group_id)
joins(:channel_groups) \
.merge(ChannelGroup.by_id(group_id))
end
def self.with_channel_attribute_ids(*value)
joins(:channel_membership_attribute_values) \
.merge(ChannelMembershipAttributeValue.by_ids(*value))
end
def self.registered
joins(:account)
end
end
including column reference
class << self
alias_method :_, :arel_table
def column_last_initial(as = nil)
decrypt = decrypted_function(:last_name)
left = Arel::Nodes::NamedFunction.new("LEFT",
[decrypt, 1])
Arel::Nodes::NamedFunction.new("UCASE", [left], as)
end
protected
def decrypted_function(column_name, as = nil)
Arel::Nodes::NamedFunction.new("AES_DECRYPT",
[_[column_name.to_sym], 'xxx'],
as)
end
end
And here is the sql
SELECT DISTINCT UCASE(LEFT(AES_DECRYPT(`people`.`last_name`, x'xxx'), 1)) AS last_initial
FROM `people` INNER JOIN `channel_group_people`
ON `channel_group_people`.`person_id` = `people`.`id`
INNER JOIN `channel_groups`
ON `channel_groups`.`id` = `channel_group_people`.`channel_group_id`
INNER JOIN `accounts`
ON `accounts`.`person_id` = `people`.`id`
INNER JOIN `channel_membership_attribute_person_values`
ON `channel_membership_attribute_person_values`.`person_id` = `people`.`id`
INNER JOIN `channel_membership_attribute_values`
ON `channel_membership_attribute_values`.`id` = `channel_membership_attribute_person_values`.`channel_membership_attribute_value_id`
WHERE `channel_groups`.`id` = 21
AND `channel_membership_attribute_values`.`id` = 23
ORDER BY UCASE(LEFT(AES_DECRYPT(`people`.`last_name`, x'xxx'), 1))
class Beer < ActiveRecord::Base
belongs_to :brewery
belongs_to :style
has_and_belongs_to_many :beer_drinkers
end
Prevents hard coding table names
Beer.joins(:brewery).where(brewery: { name: "Bell's" })
# SELECT "beers".*
# FROM "beers" INNER JOIN "breweries"
# ON "breweries"."id" = "beers"."brewery_id"
# WHERE "brewery"."name" = 'Bell''s'
Beer.joins(:brewery).merge(Brewery.where(name: "Bell's"))
# SELECT "beers".*
# FROM "beers" INNER JOIN "breweries"
# ON "breweries"."id" = "beers"."brewery_id"
# WHERE "breweries"."name" = 'Bell''s'
Allows you to not duplicate conditions across models
class Brewery < ActiveRecord::Base
has_many :beers
def self.by_name(name)
where(name: name)
end
end
Beer.joins(:brewery).merge(Brewery.by_name("Bell's"))
class Beer < ActiveRecord::Base
belongs_to :brewery
def self.by_brewery_name(name)
joins(:brewery).merge(Brewery.by_name(name))
end
end
All Relationships
class BeerDrinker < ActiveRecord::Base
has_and_belongs_to_many :beers # many to many
has_many :breweries, through: :beers # has many through
end
class Beer < ActiveRecord::Base
belongs_to :brewery # join from BeerDrinker through
has_and_belongs_to_many :beer_drinkers
end
Inner Joins Through Relationships
BeerDrinker.joins(:breweries) \
.merge(Brewery.by_name("Bell's"))
# SELECT "beer_drinkers".*
# FROM "beer_drinkers" INNER JOIN "beer_drinkers_beers"
# ON "beer_drinkers_beers"."beer_drinker_id"
# = "beer_drinkers"."id"
# INNER JOIN "beers" ON "beers"."id"
# = "beer_drinkers_beers"."beer_id"
# INNER JOIN "breweries" ON "breweries"."id"
# = "beers"."brewery_id"
# WHERE "breweries"."name" = 'Bell''s'
Outer Joins Through Relationship
BeerDrinker.includes(:beers) \
.merge(Beer.where(id: nil))
# SELECT "beer_drinkers"."id" AS t0_r0, "beers"."id" AS ...
# FROM "beer_drinkers" LEFT OUTER JOIN "beer_drinkers_beers"
# ON "beer_drinkers_beers"."beer_drinker_id"
# = "beer_drinkers"."id"
# LEFT OUTER JOIN "beers"
# ON "beers"."id" = "beer_drinkers_beers"."beer_id"
# WHERE "beers"."id" IS NULL
class Beer < ActiveRecord::Base
class << self
def by_name(name)
where(name: name)
end
def by_abv(abv)
where(abv: abv)
end
def by_brewery_name(name)
joins(:brewery).merge(Brewery.by_name(name))
end
end
end
Beer.by_abv(5.5..7).by_brewery_name("Founders Brewing Co.")
# SELECT "beers".*
# FROM "beers" INNER JOIN "breweries"
# ON "breweries"."id" = "beers"."brewery_id"
# WHERE "breweries"."name" = 'Founders Brewing Co.'
# AND ("beers"."abv" BETWEEN 5.5 AND 7)
Limiting to Active Users
Post.includes(:user).where(users: { is_active: true })
Post.includes(:user).where(users: { is_active: true }) \
.where("user.email IS NOT NULL")
Post.includes(:user).where(users: { is_active: true }) \
.where("users.email IS NOT NULL") \
.where(users: { banned: false })
Post.includes(:user).merge(User.active)
Limiting to Active Users
class User < ActiveRecord::Base
has_many :posts
has_many :documents
def self.active
where(is_active: true).with_email.not_banned
end
def self.with_email
where(User.arel_table[:email].not_eq(nil))
end
def self.not_banned
where(banned: false)
end
end
Limiting to Active Users
class Post < ActiveRecord::Base
belongs_to :user
def self.by_active_users
joins(:user).merge(User.active)
end
end
class Document < ActiveRecord::Base
belongs_to :user
def self.by_active_users
joins(:user).merge(User.active)
end
end
Arel::Table
> Beer.arel_table.class
=> Arel::Table
> Beer.arel_table[:name].class
=> Arel::Attributes::Attribute
> Beer.where(Beer.arel_table[:name].matches("%ale%"))
# SELECT "beers".* FROM "beers"
# WHERE ("beers"."name" LIKE '%ale%')
Arel::SelectManager
Beer.scoped.arel.class
=> Arel::SelectManager
query = Beer.where(abv: 5..6)
query.arel.where(Beer.arel_table[:name].matches("%ale%"))
query.all
# SELECT "beers".* FROM "beers"
# WHERE "beers"."abv" between 5 and 6
# AND "beers"."name" LIKE '%ale%'
Starts or Ends with
def starts_or_ends_with(start, ending=nil)
beer = Beer.arel_table
ending = ending || start
where(beer[:name] \
.matches_any(["%#{ending}", "#{start}%"]))
end
Beer.starts_or_ends_with("ale")
# SELECT "beers".*
# FROM "beers"
# WHERE (("beers"."name" LIKE '%ale'
# OR "beers"."name" LIKE 'ale%'))
Or Conditionals
def misclassified_ipas
beer = Beer.arel_table
style = Style.arel_table
joins(:style).where(
beer[:abv].lt(7.5).and(style[:name].eq("Imperial IPA"))
.or(beer[:abv].gt(7.4).and(style[:name].eq("IPA")))
)
end
# SELECT "beers".* FROM "beers"
# INNER JOIN "styles" ON "styles"."id" = "beers"."style_id"
# WHERE (("beers"."abv" < 7.5 AND "styles"."name" = 'Imperial IPA'
# OR "beers"."abv" > 7.4 AND "styles"."name" = 'IPA'))
Or Conditionals
def misclassified_imperials_ipas_and_stouts
beer = Beer.arel_table
style = Style.arel_table
joins(:style).where(
style[:name].eq("Imperial IPA") \
.or(style[:name].eq("Imperial Stout")) \
.and(beer[:abv].lt(7.5))
)
end
# SELECT "beers".* FROM "beers"
# INNER JOIN "styles" ON "styles"."id" = "beers"."style_id"
# WHERE (("styles"."name" = 'Imperial IPA'
# OR "styles"."name" = 'Imperial Stout')
# AND "beers"."abv" < 7.5)
Not Conditions
class Style < ActiveRecord::Base
def not_names(*names)
names = names.flatten
where(Style.arel_table[:name].not_in(names))
end
end
Style.not_names("Stout", "IPA")
# SELECT "styles".* FROM "styles"
# WHERE ("styles"."name" NOT IN ('Stout', 'IPA'))
Inner Join
beer = Beer.arel_table
style = Style.arel_table
join_on = beer.create_on(beer[:style_id].eq(style[:id]))
inner_join = beer.create_join(style, join_on)
Beer.joins(inner_join)
# SELECT "beers".* FROM "beers"
# INNER JOIN "styles" ON "beers"."style_id" = "styles"."id"
Outer Join
beer = Beer.arel_table
style = Style.arel_table
join_on = beer.create_on(beer[:style_id].eq(style[:id]))
outer_join = beer.create_join(style, join_on,
Arel::Nodes::OuterJoin)
Beer.joins(outer_join)
# SELECT "beers".* FROM "beers"
# LEFT OUTER JOIN "styles"
# ON "beers"."style_id" = "styles"."id"
Multiple Joins
beer = Beer.arel_table
beer_drinker = BeerDrinker.arel_table
beer_beer_drinker = Arel::Table.new(:beer_drinkers_beers)
condition = beer.create_on \
beer[:id].eq(beer_beer_drinker[:beer_id])
first_join = beer.create_join(beer_beer_drinker, condition)
condition = beer.create_on \
beer_beer_drinker[:beer_drinker_id].eq(beer_drinker[:id])
second_join = beer.create_join(beer_drinker, condition)
Multiple Joins
Beer.joins(first_join, second_join)
# SELECT "beers".*
# FROM "beers" INNER JOIN "beer_drinkers_beers"
# ON "beers"."id" = "beer_drinkers_beers"."beer_id"
# INNER JOIN "beer_drinkers"
# ON "beer_drinkers_beers"."beer_drinker_id" =
# "beer_drinkers"."id"
Just using normal AR style
Beer.order(:name)
# SELECT "beers".* FROM "beers" ORDER BY name
Beer.joins(:style).order(:name)
# SELECT "beers".* FROM "beers"
# INNER JOIN "styles" ON "styles"."id" = "beers"."style_id"
# ORDER BY name
beer = Beer.arel_table
Beer.order(beer[:name])
# SELECT "beers".* FROM "beers" ORDER BY "beers"."name"
Beer.order(beer[:name].desc)
# SELECT "beers".* FROM "beers" ORDER BY "beers"."name" DESC
Arel::Nodes::NamedFunction
#new(name, expr, aliaz = nil)
Named Function
person = Person.arel_table
decrypt = Arel::Nodes::NamedFunction.new("AES_DECRYPT",
[person[:name], 'xxx'])
decrypt.to_sql
# AES_DECRYPT(`people`.`name`, 'xxx')
Combining Named Functions
person = Person.arel_table
decrypt = Arel::Nodes::NamedFunction.new("AES_DECRYPT",
[person[:name], 'xxx'])
left = Arel::Nodes::NamedFunction.new("LEFT", [decrypt, 1])
ucase = Arel::Nodes::NamedFunction.new("UCASE",
[left], 'last_initial')
ucase.to_sql
# UCASE(LEFT(AES_DECRYPT(`people`.`name`, 'xxx'), 1))
# AS last_initial
Use the named function
Person.order(ucase).to_sql
# SELECT `people`.*
# FROM `people`
# ORDER BY UCASE(LEFT(AES_DECRYPT(`people`.`name`, 'xxx'), 1))
Person.pluck(ucase.to_sql).to_sql
# SELECT UCASE(LEFT(AES_DECRYPT(`people`.`name`, 'xxx'), 1))
# AS last_initial
# FROM `people`
Creating a union
beer = Beer.arel_table
union = Beer.where(name: "Oberon") \
.union(Beer.where(name: "Two Hearted"))
Beer.from(beer.create_table_alias(union, :beers)).all
# SELECT "beers".*
# FROM ( SELECT "beers".*
# FROM "beers"
# WHERE "beers"."name" = 'Oberon'
# UNION
# SELECT "beers".*
# FROM "beers"
# WHERE "beers"."name" = 'Two Hearted' ) "beers"
require 'graphviz'
query = Beer.by_abv(5.5..7).by_brewery_name("Bell's") \
.order("created_at")
graph = GraphViz.parse_string(query.to_dot)
graph.output(:png => "query.png")
Null Object Pattern
Beer.none.where(name: "Pale ale")
Not Operator
Beer.not.where(name: "Pale ale")
All scopes require callable object
class Beer < ActiveRecord::Base
scope :deleted, where(deleted: true)
scope :deleted, lambda { where(deleted: true) }
end
#update_columns to compliment #update_column
Beer.first.update_column(:name, "New Name")
Beer.first.update_columns(name: "New Name", abv: 5.5)
* unsafe, bypasses validations and callbacks
Finder methods are deprecated
Beer.find_all_by_name("Oberon")
Beer.where(name: "Oberon")
#pluck takes multiple columns
> Beer.pluck(:id, :name)
=> [[1, "Hopslam"], [2, "Oberon"], [3, "Two Hearted"]]
Awesome resources
http://benhoskin.gs/2012/07/04/arel-merge-a-hidden-gem
http://danshultz.github.com/talks/mastering_activerecord_arel/
https://github.com/danshultz/mastering_active_record_sample_code