Combining a 'where' clause with a method - ruby-on-rails

In my app, a Team belongs_to :hunt. Once a Hunt is confirmed, then all associated teams are ready.
This a sample from my team.rb file, where I use the method ready? to check if team.hunt is confirmed.
#team.rb
def ready?
hunt.confirmed? ? true : false
end
I would love to have a scope in team.rb file so I can call Teams.all.ready.count to show the number of teams that are ready.
How can I write a method or a scope to achieve the behaviour above without adding anything to my DB or iterating through an array etc?

Updated:
Thanks to #TomLord's insight, you'd rather do Solution 1 below instead of Solution 2. Also, added sample SQL to show comparison.
Solution 1
class Team < ApplicationRecord
belongs_to :hunt
scope :ready, -> { joins(:hunt).where(hunts: { confirmed: true }) }
end
Usage:
Team.ready # or: Team.all.ready
# SELECT "teams".* FROM "teams" INNER JOIN "hunts" ON "hunts"."id" = "teams"."hunt_id" WHERE "hunts"."confirmed" = ? LIMIT ? [["confirmed", "t"], ["LIMIT", 11]]
Or, Solution 2
class Team < ApplicationRecord
belongs_to :hunt
end
class Hunt < ApplicationRecord
scope :confirmed, -> { where(confirmed: true) }
end
Usage:
# you can also move the logic below as a method/scope inside `Team` model (i.e. as `ready` method/scope)
# Example 1 (using JOINS):
Team.joins(:hunt).where(hunts: { id: Hunt.confirmed })
# SELECT "teams".* FROM "teams" INNER JOIN "hunts" ON "hunts"."id" = "teams"."hunt_id" WHERE "hunts"."id" IN (SELECT "hunts"."id" FROM "hunts" WHERE "hunts"."confirmed" = ?) LIMIT ? [["confirmed", "t"], ["LIMIT", 11]]
# Example 2 (same as Example 1 above but faster and more efficient):
Team.where(hunt_id: Hunt.confirmed)
# SELECT "teams".* FROM "teams" WHERE "teams"."hunt_id" IN (SELECT "hunts"."id" FROM "hunts" WHERE "hunts"."confirmed" = ?) LIMIT ? [["confirmed", "t"], ["LIMIT", 11]]

Solution: if Hunts#confirmed is a database column:
class Team < ApplicationRecord
belongs_to :hunt
scope :ready, -> { joins(:hunt).where(hunts: { confirmed: true }) }
end
In this case, Team.ready will return ActiveRecord::Relation.
Solution: If Hunts#confirmed? is a NOT a database column:
class Team < ApplicationRecord
belongs_to :hunt
scope :ready, -> { includes(:hunts).select(&:ready?) }
end
In this case, Team.ready will return an Array
You need to be aware that the second solution is looping over the Team records calling ready? on each of them while the first is performing a database query. First is more efficient.

Related

activerecord joins multiple time the same table

I am doing a simple search looking like this (with 8 different params, but I will copy 2 just for the exemple)
if params[:old] == "on"
#events = #events.joins(:services).where(services: { name: "old" })
end
if params[:big] == "on"
#events = #events.joins(:services).where(services: { name: "big" })
end
The query works fine when I have only one params "on" and returns my events having the service in the param.
BUT if I have two params "on", even if my event has both services, it's not working anymore.
I've tested it in the console, and I can see that if I do a .joins(:services).where(services: { name: "big" }) on the element that was already joined before, it returns nothing.
I don't understand why.
The first #events (when one param) returns an active record relation with a few events inside.
Why can't I do another .joins on it?
I really don't understand what's wrong in this query and why it becomes empty as soon as it is joined twice.
Thanks a lot
The code you're using will translate to:
SELECT "events".* FROM "events" INNER JOIN "services" ON "services"."event_id" = "events"."id" WHERE "services"."name" = ? AND "services"."name" = ? LIMIT ? [["name", "big"], ["name", "old"], ["LIMIT", 11]]
This is why it returns zero record.
Here is the solution I can think of at the moment, not sure if it's the ideal, but yes it works and has been tested.
# event.rb
class Event < ApplicationRecord
has_many :services
has_many :old_services, -> { where(name: 'old') }, class_name: 'Service'
has_many :big_services, -> { where(name: 'big') }, class_name: 'Service'
end
# service.rb
class Service < ApplicationRecord
belongs_to :event
end
And your search method can be written this way:
if params[:old] == "on"
#events = #events.joins(:old_services)
end
if params[:big] == "on"
#events = #events.joins(:big_services)
end
#events = #events.distinct
# SELECT DISTINCT "events".* FROM "events" INNER JOIN "services" ON "services"."event_id" = "events"."id" AND "services"."name" = ? INNER JOIN "services" "old_services_events" ON "old_services_events"."event_id" = "events"."id" AND "old_services_events"."name" = ? LIMIT ? [["name", "big"], ["name", "old"], ["LIMIT", 11]]

How do you return intermediary model while joining through several models in Rails?

I am trying to figure out how to query activerecord and get back a list of objects, while joining through other objects.
Here is a simplified example.
class Person < ApplicationRecord
has_many :wearings
has_many :shirts, through :wearings
end
class Wearing < ApplicationRecord
belongs_to :shirt
belongs_to :person
#has wear date + other attributes
end
class Shirt < ApplicationRecord
scope :white, -> {where(white:true)}
scope :large, -> {where(large:true)}
scope :sleeves, -> {where(sleeves:true)}
end
People own shirts, and may even own several of the same color. In this case, I want to get back the wearings records for all shirts that match my criteria.
I can get the shirts back like:
shirts = person.shirts.white.large
I'd like to be able to do the same single query using scopes, but instead get back the subset of person.wearings that involved large white shirts.
This is in Rails 5.
I know 2 ways to implement this. Both of them produce same SQL request. We can add one of following scopes to Wearing:
scope :by_white_shirts, -> { joins(:shirt).where(shirts: { white: true }) }
or using merge
scope :by_white_shirts, -> { joins(:shirt).merge(Shirt.white) }
the result will be like:
person.wearings.by_white_shirts
>> Wearing Load (0.6ms) SELECT "wearings".* FROM "wearings" INNER JOIN
"shirts" ON "shirts"."id" = "wearings"."shirt_id" WHERE
"wearings"."person_id" = ? AND "shirts"."white" = ? LIMIT ?
[["person_id", 1], ["white", 1], ["LIMIT", 11]]

how to write a scope for a model that belongs_to two other models and excludes all records with matching foreign keys

I want to write a scope for a model that belongs_to two other models. This scope should select all records where the value of one model is not equal to the value of its parent. What I have below seems like it should work:
Class Blob
belongs_to :user
belongs_to :item
scope: :not_owned_by_item_owner, -> { where.not(user_id: item.owner.id) }
end
But then calling blob.not_owned_by_item_owner anywhere in a controller results in an undefined local variable or method error for item. Why is item not recognized?
Solution 1
scope :not_owned_by_item_owner, -> {
joins(:item).where.not(
Blob.arel_table[:user_id].eq(Item.arel_table[:owner_id])
)
}
# SELECT "blobs".* FROM "blobs" INNER JOIN "items" ON "items"."id" = "blobs"."item_id" WHERE "blobs"."user_id" != "items"."owner_id"
Solution 2
or if you prefer raw SQL:
scope :not_owned_by_item_owner, -> {
joins(:item).where('blobs.user_id != items.owner_id')
}
# SELECT "blobs".* FROM "blobs" INNER JOIN "items" ON "items"."id" = "blobs"."item_id" WHERE (blobs.user_id != items.owner_id) LIMIT $1
P.S. I don't think there's an ActiveRecord native solution for this, but I am also interested if anyone finds one. The above Solution 1 uses Arel directly.

Rails ActiveRecord - get belongs_to association with lock

I would like to retrieve a belongs_to association and acquire a database lock on its object.
> character.team.lock!
ActiveRecord::Base -- Team Load -- { :sql => "SELECT `teams`.* FROM `teams` WHERE `teams`.`id` = 1 LIMIT 1" }
ActiveRecord::Base -- Team Load -- { :sql => "SELECT `teams`.* FROM `teams` WHERE `teams`.`id` = 1 LIMIT 1 FOR UPDATE" }
Above runs two queries, which technically makes sense - character.team loads the team, then team.lock! selects once more with FOR UPDATE.
The question is - how can I make it issue only one query?
You can carry on a scope to the association using ActiveRecord::Relation#scoping:
Team.lock.scoping { character.team }
Apparently you can't, because the .lock method will always reload the instance (issuing a second SQL load). From the docs:
.lock: Obtain a row lock on this record. Reloads the record to obtain the requested lock. Pass an SQL locking clause to append the end of the SELECT statement or pass true for “FOR UPDATE” (the default, an exclusive row lock). Returns the locked record.
Thankfully there is a solution:
character.association(:team).scope.lock.to_a[0]
# SELECT "teams".* FROM "teams" WHERE "teams"."id" = '...' LIMIT 1 FOR UPDATE
The only difference to the SQL query is the added FOR UPDATE.
May be abstracted in ApplicationRecord for all belongs_to associations:
class ApplicationRecord < ActiveRecord::Base
# ...
def self.belongs_to(name, *)
class_eval <<~RUBY
def locked_#{name}
association(:#{name}).scope.lock.to_a[0]
end
RUBY
super
end
end
Then in app code:
character.locked_team

Writing scope to complex multiple associations - Rails 4

I am having challenges writing a scope that displays live_socials created by users belonging to a category_managementgroup called client_group.
Social.rb
belongs_to :user
scope :live_socials, -> {where(['date >= ?', Date.current])}
CategoryManagementgroup.rb
has_many :users
User.rb
has_many :socials
belongs_to :category_managementgroup
the below scope displays all the socials for users in the category_managementgroup called client_group:
users.joins(:socials, :category_managementgroup).client_group.flat_map(&:socials)
i am unsure how to extend the scope to display the live_socials
(socials that have not expired). i tried the below but no success:
users.joins(:socials, :category_managementgroup).client_group.flat_map(&:socials).live_socials
i get the below error:
2.3.0 :264 > ap users.joins(:socials, :category_managementgroup).client_group.flat_map(&:socials).live_socials
User Load (0.5ms) SELECT "users".* FROM "users" INNER JOIN "socials" ON "socials"."user_id" = "users"."id" INNER JOIN "category_managementgroups" ON "category_managementgroups"."id" = "users"."category_managementgroup_id" WHERE "category_managementgroups"."name" = 'Client Group'
Social Load (0.1ms) SELECT "socials".* FROM "socials" WHERE "socials"."user_id" = ? [["user_id", 10]]
NoMethodError: undefined method `live_socials' for #<Array:0x007ff2f9834570>
Try applying live_socials scope before flat_map and add below scope to User model
scope :live_socials, -> { joins(:socials).where(['socials.date >= ?', Date.current])}

Resources