Scoops of scopes
Hegedüs Zoltán
Ordinary scopes
class Foo < ActiveRecord::Base
scope :published, -> { where(published: true) }
scope :published_and_commented, -> { published.where("comments_count > 0") }
end
irb(main):005:0> Foo.published
=> #<ActiveRecord::Relation [#<Foo id: 1>, #<Foo id: 5>]>
irb(main):006:0> Foo.published.joins(bar: :baz).where(id: [1,9])
=> #<ActiveRecord::Relation [#<Foo id: 1>]>
Ordinary scopes cont'd
class Foo < ActiveRecord::Base
scope :published, -> { where(published: true) }
scope :published_and_commented, -> { published.where("comments_count > 0") }
scope :wanted, -> (good_ids = nil) {
join_clause = <<-JOINS
LEFT JOIN (SELECT
sessions.id AS session_id,
articles.person_id AS person_id,
COUNT(articles.id) AS session_activity_count
FROM sessions
INNER JOIN session_inclusions
ON session_inclusions.session_id = sessions.id) tmp1 ON
tmp1.session_id = foos.session_id
JOINS
relation = published
.joins(join_clause)
if good_ids
relation.where(id: good_ids)
else
relation
end
}
end
irb(main):005:0> Foo.wanted([1,9])
=> #<ActiveRecord::Relation [#<Foo id: 1>]>
Ordinary scopes cont'd
SELECT
people.*,
routing_paths.title AS routing_path_title,
teams.title AS team_title
FROM (SELECT
people.id,
people.name,
people.team_id,
scv.routing_path_id,
start_date,
end_date,
session_description,
session_value * session_length_ratio AS session_value,
session_per_capita_value * session_length_ratio AS session_per_capita_value,
COALESCE(session_activity_count, 0) AS session_activity_count,
COALESCE(session_activity_value, 0) AS session_activity_value,
0 AS out_of_contract_activity_value,
0 AS out_of_contract_activity_count
FROM "people"
INNER JOIN "teams" ON "teams"."id" = "people"."team_id"
JOIN (SELECT
sessions.id AS session_id,
sessions.description AS session_description,
sessions.start_date,
sessions.end_date,
sessions.routing_path_id,
(UPPER(daterange(sessions.start_date, sessions.end_date) * '[2019-01-01, 2019-12-31)' :: DATERANGE)
- LOWER(daterange(sessions.start_date, sessions.end_date) *
'[2019-01-01, 2019-12-31)' :: DATERANGE)) :: FLOAT / (364) AS session_length_ratio,
sessions.value_amount / 100 AS session_value,
session_inclusions.subject_id AS person_id,
session_inclusions.weight AS session_per_capita_value
FROM "sessions"
INNER JOIN "session_inclusions" ON "session_inclusions"."session_id" = "sessions"."id" AND
"session_inclusions"."deleted_at" IS NULL
WHERE "sessions"."deleted_at" IS NULL AND "sessions"."id" IN (SELECT "sessions"."id"
FROM "sessions"
WHERE "sessions"."deleted_at" IS NULL AND "sessions"."investor_id" = 851 AND (
(sessions.start_date >= '2019-01-01' AND
'2019-12-31' BETWEEN sessions.start_date AND sessions.end_date)
OR (sessions.end_date <= '2019-12-31' AND
'2019-01-01' BETWEEN sessions.start_date AND sessions.end_date)
OR (sessions.start_date > '2019-01-01' AND sessions.end_date < '2019-12-31')
OR (sessions.start_date < '2019-01-01' AND sessions.end_date > '2019-12-31')
)) AND "session_inclusions"."subject_type" = 'Person') AS scv ON scv.person_id = people.id
LEFT JOIN (SELECT
sessions.id AS session_id,
articles.person_id AS person_id,
sessions.routing_path_id,
SUM(articles.value_amount) / 100 AS session_activity_value,
COUNT(articles.id) AS session_activity_count
FROM "sessions"
INNER JOIN "session_inclusions" ON "session_inclusions"."session_id" = "sessions"."id" AND
"session_inclusions"."deleted_at" IS NULL
JOIN articles
ON session_inclusions.subject_id = articles.person_id AND
articles.status IN ('granted', 'not_attend')
JOIN activities
ON articles.activity_id = activities.id
AND activities.routing_path_id = sessions.routing_path_id
AND activities.deleted_at IS NULL
AND activities.aggregate_status = 'consumed'
AND activities.start_date BETWEEN sessions.start_date AND sessions.end_date
AND activities.classification_id IN (SELECT classification_id
FROM session_classifications
WHERE session_classifications.session_id = sessions.id
AND session_classifications.deleted_at IS NULL)
WHERE "sessions"."deleted_at" IS NULL AND "sessions"."id" IN (SELECT "sessions"."id"
FROM "sessions"
WHERE "sessions"."deleted_at" IS NULL AND "sessions"."investor_id" = 851 AND (
(sessions.start_date >= '2019-01-01' AND
'2019-12-31' BETWEEN sessions.start_date AND sessions.end_date)
OR (sessions.end_date <= '2019-12-31' AND
'2019-01-01' BETWEEN sessions.start_date AND sessions.end_date)
OR (sessions.start_date > '2019-01-01' AND sessions.end_date < '2019-12-31')
OR (sessions.start_date < '2019-01-01' AND sessions.end_date > '2019-12-31')
)) AND "session_inclusions"."subject_type" = 'Person'
LEFT JOIN (SELECT
SUM(session_activity_value) AS session_activity_value,
SUM(session_activity_count) AS session_activity_count,
scv.routing_path_id,
people.id AS person_id
FROM "people"
INNER JOIN "teams" ON "teams"."id" = "people"."team_id"
JOIN (SELECT
sessions.id AS session_id,
sessions.description AS session_description,
sessions.start_date,
sessions.end_date,
sessions.routing_path_id,
(UPPER(
daterange(sessions.start_date, sessions.end_date) * '[2019-01-01, 2019-12-31)' :: DATERANGE) -
LOWER(daterange(sessions.start_date, sessions.end_date) *
'[2019-01-01, 2019-12-31)' :: DATERANGE)) :: FLOAT / (364) AS session_length_ratio,
sessions.value_amount / 100 AS session_value,
session_inclusions.subject_id AS person_id,
session_inclusions.weight AS session_per_capita_value
FROM "sessions"
INNER JOIN "session_inclusions"
ON "session_inclusions"."session_id" = "sessions"."id" AND
"session_inclusions"."deleted_at" IS NULL
WHERE "sessions"."deleted_at" IS NULL AND "sessions"."id" IN (SELECT "sessions"."id"
FROM "sessions"
WHERE "sessions"."deleted_at" IS NULL AND "sessions"."investor_id" = 851 AND (
(sessions.start_date >= '2019-01-01' AND
'2019-12-31' BETWEEN sessions.start_date AND sessions.end_date)
OR (sessions.end_date <= '2019-12-31' AND
'2019-01-01' BETWEEN sessions.start_date AND sessions.end_date)
OR (sessions.start_date > '2019-01-01' AND sessions.end_date < '2019-12-31')
OR (sessions.start_date < '2019-01-01' AND sessions.end_date > '2019-12-31')
)) AND "session_inclusions"."subject_type" = 'Person') AS scv ON scv.person_id = people.id
LEFT JOIN (SELECT
sessions.id AS session_id,
articles.person_id AS person_id,
sessions.routing_path_id,
SUM(articles.value_amount) / 100 AS session_activity_value,
COUNT(articles.id) AS session_activity_count
FROM "sessions"
INNER JOIN "session_inclusions"
ON "session_inclusions"."session_id" = "sessions"."id" AND
"session_inclusions"."deleted_at" IS NULL
JOIN articles
ON session_inclusions.subject_id = articles.person_id AND
articles.status IN ('granted', 'not_attend')
JOIN activities
ON articles.activity_id = activities.id
AND activities.routing_path_id = sessions.routing_path_id
AND activities.deleted_at IS NULL
AND activities.aggregate_status = 'consumed'
AND activities.start_date BETWEEN sessions.start_date AND sessions.end_date
AND activities.classification_id IN (SELECT classification_id
FROM session_classifications
WHERE session_classifications.session_id = sessions.id
AND session_classifications.deleted_at IS NULL)
WHERE "sessions"."deleted_at" IS NULL AND "sessions"."id" IN (SELECT "sessions"."id"
FROM "sessions"
WHERE "sessions"."deleted_at" IS NULL AND "sessions"."investor_id" = 851 AND (
(sessions.start_date >= '2019-01-01' AND
'2019-12-31' BETWEEN sessions.start_date AND sessions.end_date)
OR (sessions.end_date <= '2019-12-31' AND
'2019-01-01' BETWEEN sessions.start_date AND sessions.end_date)
OR (sessions.start_date > '2019-01-01' AND sessions.end_date < '2019-12-31')
OR (sessions.start_date < '2019-01-01' AND sessions.end_date > '2019-12-31')
)) AND "session_inclusions"."subject_type" = 'Person'
GROUP BY sessions.id, articles.person_id) AS sav
ON sav.person_id = people.id AND sav.session_id = scv.session_id
WHERE "people"."id" IN (SELECT "people"."id"
FROM "people"
INNER JOIN "teams" ON "teams"."id" = "people"."team_id"
INNER JOIN "offices" ON "offices"."id" = "people"."office_id"
WHERE "offices"."firm_id" = 851)
GROUP BY people.id, scv.routing_path_id) AS sessions
ON sessions.person_id = people.id AND sessions.routing_path_id = rpav.routing_path_id
Enter Query objects
class Query
class << self
def create(**params)
new(**params).tap(&:save)
end
end
def save
@relation = @scopes.inject(base_relation) { |relation, scope| scope.call(relation) }
end
def results
@relation
end
end
class Query::WantedFoos < Query
def initialize(ids: nil)
@scopes = [bars_scope]
@scopes << id_scope(ids) if ids
end
private
def base_relation
Foo.all
end
def bars_scope
join_clause = <<-JOINS
LEFT JOIN bars ON bars.id < 5
JOINS
->(relation) { relation.joins(join_clause)}
end
def id_scope(ids)
->(relation) { relation.where(id: ids)}
end
end
Scope objects
class BarScope
def call(relation)
join_clause = <<-JOINS
LEFT JOIN bars ON bars.id < 5
JOINS
relation.joins(join_clause)
end
end
class IdScope
def initialize(ids)
@ids = ids
end
def call(relation)
relation.where(id: @ids)
end
end
class Query::WantedFoos < Query
def initialize(ids: nil)
@scopes = [bars]
@scopes << id_scope(ids) if ids
end
private
def base_relation
Foo.all
end
def bars
BarScope.new
end
def id_scope(ids)
IdScope.new(ids)
end
end
Scope objects cont'd
class BarScope
def call(relation)
join_clause = <<-JOINS
LEFT JOIN bars ON bars.id < 5
JOINS
relation.joins(join_clause)
end
end
class IdScope
def initialize(ids)
@ids = ids
end
def call(relation)
relation.where(id: @ids)
end
end
class Query::WantedFoos < Query
def initialize(ids: nil)
@scopes = [bars]
@scopes << id_scope(ids) if ids
end
private
def base_relation
Foo.all
end
def bars
BarScope.new
end
def id_scope(ids)
IdScope.new(ids)
end
end
require 'rails_helper'
describe IdScope do
let(:scope) { described_class.new(params) }
describe "#call" do
subject { scope.call(relation) }
let(:relation) { Foo.all }
context "with existing id" do
let(:params) { [Foo.first.id] }
it "should return that foo" do
subject.should include(Foo.first)
end
end
end
end
class Query::InterestingBar < Query
def initialize(ids: nil)
@scopes = [id_scope]
end
private
def base_relation
Bar.all
end
def id_scope(ids)
IdScope.new(ids)
end
end
Scope objects cont'd
class Scope::Consumption
def initialize(investor:, date_range:, user: )
@user = user
@investor = investor
@date_range = date_range
@scopes = [managed_teams_scope]
@scopes << subscriptions_scope
@scopes << activities_scope
end
def call(relation)
scopes.inject(relation) { |relashun, scope| scope.call(relashun) }
end
private
attr_reader :scopes, :date_range, :investor, :user
def managed_teams_scope
if user.administrator?
scope = Scope::ManagedTeams.new(user.person)
->(relation) { relation.where(people: { id: Person.where(team_id: scope.call(Team.all)) }) }
elsif user.manager?
->(relation) { relation }
else
->(relation) { relation.where(people: { id: user.person.id }) }
end
end
def subscriptions_scope
Scope::Consumption::Subscriptions.new(investor: investor, date_range: date_range)
end
def activities_scope
Scope::Consumption::Activities.new(investor: investor)
end
end
Thank you!
Questions?
scopes
By hegedus-zoltan
scopes
- 531