Rails scope - how to return instances where their children match a certain value exclusively? - sql

How can I write a rails scope that returns all instances of Company where its Employees status are ALL 'active' ?
class Employee < ApplicationRecord
STATUS = ['active', 'busy', 'inactive']
belongs_to: :company
end
class Company < ApplicationRecord
has_many: :employees
end
I tried the following scope :
scope :with_active_employees_only, -> {
select("DISTINCT ON (companies.id) companies.*")
.joins(:employees)
.where("employees.status NOT IN (?)", Employee::STATUS - ['active'])
}
But it still returns me Companies where some of the employees are 'busy' or 'inactive', even though I only want companies where its employees are exclusively 'active'. How can I achieve that ?

If you are on the latest 6.1.x version of activerecord, you can use where.missing with an aptly built association:
class Company
has_many :inactive_employees, -> { where(status: 'inactive') },
class_name: 'Employee', inverse_of: nil
# includes companies with no employee at all
scope :without_inactive_employees, -> {
where.missing(:inactive_employees)
}
end
If you are not there, you can still backport it from https://github.com/rails/rails/blob/6-1-stable/activerecord/lib/active_record/relation/query_methods.rb#L71-L80

you could group employees by company then count all status in which active is 1 otherwise 0, so that the company which has all employees active is the one has the status-count is 1.
scope :with_active_employees_only, -> {
Company.joins(:employees)
.select("companies.*")
.group("companies.id")
.having("SUM(CASE WHEN employees.status = 'active' THEN 1 ELSE 0 END) = 1")
}

Related

Filtering an association by missing records in Rails

In a Rails application I'm working on, I've got a few different models associated thusly (condensed for clarity):
group.rb
class Group < ApplicationRecord
has_many :members, class_name: 'GroupMember'
has_many :newsletters
end
group_member.rb
class GroupMember < ApplicationRecord
belongs_to :group,
has_many :subscriptions, inverse_of: :group_member, class_name: 'Newsletter::Subscriber'
scope :subscribed_to, ->(newsletter_id) { joins(:subscriptions).merge(Newsletter::Subscriber.where(["newsletter_id = ?", newsletter_id])) }
scope :not_subscribed_to, ->(newsletter_id) { where.missing(:subscriptions) }
end
newsletter.rb
class Newsletter < ApplicationRecord
acts_as_tenant :group
has_many :subscribers, inverse_of: :newsletter
end
newsletter/subscriber.rb
class Newsletter::Subscriber < ApplicationRecord
acts_as_tenant :group
belongs_to :newsletter, inverse_of: :subscribers
belongs_to :group_member, class_name: 'GroupMember', inverse_of: :subscriptions
end
Given the above associated models, here's the framework I'm working within:
Each Group has n Group Members and n Newsletters.
Each Group Member can have multiple Newsletter Subscriptions (one per newsletter in the group)
What I'm trying to do (unsuccessfully, so far), is find out which members in a group are NOT subscribed to a specific newsletter that is associated with the group.
I can find out the members that DO have a subscription using the following scope on the GroupMember object:
scope :subscribed_to, ->(newsletter_id) { joins(:subscriptions).merge(Newsletter::Subscriber.where(["newsletter_id = ?", newsletter_id])) }
That allows me to query, for instance, current_group.members.subscribed_to(current_group.newsletters.first.id).
However, I'm not sure how to negate that and get the the opposite of that set of members. That is, members NOT subscribed to that specific newsletter. The :not_subscribed_to scope I currently have defined isn't cutting it because it doesn't take into account which newsletter I'm referring to.
Given the variable newsletter_id.
One alternative is to use WHERE NOT EXISTS(...) with a subquery:
Member
.where(
'NOT EXISTS(
SELECT 1 FROM "subscriptions"
WHERE "subscriptions"."member_id" = "members"."id"
AND "subscriptions"."newsletter_id" = ?
)', newsletter_id
)
Translated into Arel:
Member.where(
Subscription.select('1')
.where(
Subscription.arel_table[:member_id].eq(Member.arel_table[:id])
).where(
newsletter_id: newsletter_id
).arel.exists.not
)
Or group, count and having:
Member.group(:id)
.left_joins(:subscriptions)
.where(subscriptions: { newsletter_id: newsletter_id })
.having(Subscription.arel_table[:id].count.eq(0))

Finding parent of parent in a where clause

I have a restaurant that belongs to a city, which belongs to a state:
class Restaurant < ApplicationRecord
belongs_to :city
end
class City < ApplicationRecord
belongs_to :state
end
class State < ApplicationRecord
has_many :cities
end
I'd like to create a scope in the restaurant model to find the given state of the city it resides in. So far I have this:
scope :by_state, -> state { joins(city: :state).includes(city: [:state]).where(state: state) }
This unfortunately gives me the error:
ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR: column restaurants.state does not exist)
What am I doing wrong here?
.where(state: state) still queries state on Restaurant.
scope :by_state, -> state { joins(city: :state).includes(city: [:state]).where('states.name = ?', state) } is probably what you want.

How do I write an SQL query for a has_many through relationship where a foreign_key_id is "1" or NULL in Rails?

I have project where you own a fantasy team (Roster model) and you pick teams (SportsTeam model), similar to a college basketball pool. These two models are joined by a "has many through" relationship on a RosterSpots model. I have different leagues (FantasyLeague model) identified on my Roster model by fantasy_league_id.
I'm trying to set up a view that takes parameter[:fantasy_league_id]. The view will show all of the sports_teams (whether they are owned in that fantasy_league or not) and which sports_teams are currently on a roster in that fantasy_league.
My current code will show all of the teams owned in a fantasy_league, i.e. fantasy_league_id = 1, and all sports_teams where fantasy_league_id is NULL, but I can't figure out how to show a team that is not owned in fantasy_league_id = 1 but is owned in fantasy_league_id = 2. I need another OR but I can't figure out how to write it.
Here is short example:
team (fantasy_league_id):
Seattle (NULL),
Atlanta (1),
Atlanta (2),
Arizona (1),
New York (2)
With params[:fantasy_league_id] = 1, I correctly show Seattle, Atlanta, and Arizona but I need to show New York as not being on a roster in that league.
Here are my models:
class SportsTeam < ActiveRecord::Base
has_many :roster_spots
has_many :rosters, through: :roster_spots
belongs_to :sports_league
scope :left_joins_rosters, -> { joins("LEFT OUTER JOIN roster_spots
ON roster_spots.sports_team_id = sports_teams.id
LEFT OUTER JOIN rosters ON rosters.id = roster_spots.roster_id" ) }
scope :select_rosters, -> { select("sports_teams.*, rosters.*") }
scope :joins_sports_league, -> { joins(:sports_league)
.select("sports_teams.*, sports_leagues.*") }
class RosterSpot < ActiveRecord::Base
belongs_to :sports_team
belongs_to :roster
class Roster < ActiveRecord::Base
has_many :roster_spots, dependent: :destroy
has_many :sports_teams, through: :roster_spots
belongs_to :fantasy_league
class FantasyLeague < ActiveRecord::Base
has_many :rosters
has_many :sports_teams, through: :rosters
Here is my controller:
class StandingsController < ApplicationController
def list_teams
#sports_teams = SportsTeam.left_joins_rosters.joins_sports_league
.select_rosters
.where("fantasy_league_id is NULL OR fantasy_league_id = ?",
params_fantasy_league)
end
private
def params_fantasy_league
params_fantasy_league = params[:fantasy_league_id] || 1
end
end
I tried writing the WHERE statement directly in the left_joins_rosters scope but got the same result.
scope :left_joins_rosters_league, -> { joins("LEFT OUTER JOIN roster_spots
ON roster_spots.sports_team_id = sports_teams.id
LEFT OUTER JOIN rosters ON rosters.id = roster_spots.roster_id
WHERE fantasy_league_id is NULL OR fantasy_league_id = 1") }
I also considered starting with the FantasyLeague model with a .where(fantasy_league_id: params[:fantasy_league_id], but I think I would need to use a RIGHT OUTER JOIN to get all of the sports teams which isn't supported in SQLite.
Thanks!
Axel

ActiveRecord query based on multiple objects via has_many relationship

I have a Product class that has_many Gender through Connection class instances. I want to query to find products that have both end_a and end_b present. The current class method works with 2 caveats:
Fails to return correctly if searching where end_a and end_b are the same. Instead should search if product has 2 instances, not just one of object.
Returns an Array when I want an ActiveRecord_Relation.
The class method .query is below, any feedback or ideas are appreciated.
class Product < ActiveRecord::Base
has_many :connections, dependent: :destroy, as: :connectionable
has_many :genders, through: :connections
def self.query(end_a, end_b)
search_base = active.joins(:connections)
end_a_search = search_base.where(connections: { gender_id: end_a } )
end_a_search & search_base.where(connections: { gender_id: end_b } )
end
end
ps: Once this is figured out will likely move this to a scope for Product
class Product < ActiveRecord::Base
has_many :connections, dependent: :destroy, as: :connectionable
has_many :genders, through: :connections
scope :with_genders, -> (end_a, end_b) {
relation = joins('INNER JOIN connections c1 ON c1.connectionable_id = products.id AND c1.connectionable_type = \'Product\'')
.joins('INNER JOIN connections c2 ON c1.connectionable_id = c2.connectionable_id AND c2.connectionable_type = \'Product\'')
.where(c1: {gender_id: end_a}, c2: {gender_id: end_b})
.group('products.id')
end_a == end_b ? relation.having('COUNT(products.id) > 1') : relation
}
end

Many to many relationship with ability to set a state (active)

I have a fully working (for some time now) many-to-many relationship in my Rails application.
Instructors has many Schools (through SchoolsToInstructorsAssociations)
Schools has many Instructors (through SchoolsToInstructorsAssociations)
At this time, I would like the ability to have an "active state" in addition to simply adding or removing an Instructor from a School or a School from an Instructor.
I want an Instructor to be set as inactive before being removed completely at a later point (or reactivated).
My first thought was to add an 'active' boolean to the relationship model (SchoolsToInstructorsAssociations), but there's no simple way to access this attribute to update or query it).
My second thought was to simply create another relationship model with the 'active' attribute, but it's redundant and something extra I have to track.
Maybe a custom many-to-many module? Create a SchoolsToInstructorsAssociations controller?
class Instructor < ActiveRecord::Base
has_many :schools_to_instructors_association
has_many :schools, :through => :schools_to_instructors_association
end
class School < ActiveRecord::Base
has_many :schools_to_instructors_association
has_many :instructors, :through => :schools_to_instructors_association
end
class SchoolsToInstructorsAssociation < ActiveRecord::Base
belongs_to :user
belongs_to :school
end
I also plan to create a history record each time an instructors 'active' state changes or an instructor is removed or added to a school. Not asking how to do this, but wondering if it could be used to track an instructors 'active' state.
class SchoolsController < ApplicationController
def instructors_index
#school = School.find(params[:id])
instructors = find_instructors
#active_instructors = instructors[0]
#inactive_instructors = instructors[1]
respond_to do |format|
format.html # index.html.erb
format.json { render json: #schools }
end
end
private
def find_instructors
active = []; inactive = []
#school.instructors.each do |s|
if SchoolsToInstructorsAssociationRecord.where(user_id: s, school_id: #school)[0].active?
active << s
else
inactive << s
end
return [active, inactive]
end
end
end
class SchoolsToInstructorsAssociationRecord < ActiveRecord::Base
default_scope order('created_at DESC')
attr_accessor :user_id, :school_id, schools_to_instructors_association_id, :active
end
Sounds like you can accomplish what you're trying to do with scopes. Add a boolean column for 'active' as you described for the 'Instructor' class, then you can add scopes for it:
class Instructor < ActiveRecord::Base
...
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
...
end
Then for a given School, you can get the active (or inactive) instructors for that school:
#school.instructors.active
=> SELECT "instructors".* FROM "instructors" WHERE "instructors"."school_id" = <id> AND "instructors"."active" = 't'
If you wanted to do some operations on all the inactive instructors (like destroy them, as an example), you could do:
Instructor.inactive.map(&:destroy)
And you can of course write whatever custom methods you want for the Instructor or School classes.