has_many through => find not matching records - ruby-on-rails

I want to be able to find unpopulated hives, but don't find any solution.
Can you help me please ?
The goal is to be able to do Hive.unpopulated
The main problem is the most_recent, butins ok for me to work with a raw SQL, but I don't find the right query.
Here are my classes :
class Hive < ApplicationRecord
has_many :moves, dependent: :destroy
has_many :yards, through: :moves
has_many :populations, -> { where(:most_recent => true) }
has_many :colonies, through: :populations
validates :name, uniqueness: true
def hive_with_colony
"#{name} (colony #{if self.colonies.count > 0 then self.colonies.last.id end})"
end
def self.populated
Hive.joins(:populations)
end
def self.unpopulated
end
end
class Population < ApplicationRecord
belongs_to :hive
belongs_to :colony
after_create :mark_most_recent
before_create :mark_end
class Colony < ApplicationRecord
has_many :populations, -> { where(:most_recent => true) }
has_many :hives, through: :populations
has_many :visits
has_many :varroas
has_many :most_recents_populations, -> { where(:most_recent => true) }, :class_name => 'Population'
scope :last_population_completed, -> { joins(:populations).where('populations.most_recent=?', true)}

I think you can do a simple query to select Hives which are not in populated list, so:
def self.unpopulated
where.not(id: populated.select(:id))
end

Another option is a LEFT OUTER JOIN and picking the lines that have no population id set on the right side.
def self.unpopulated
left_outer_joins(:populations).where(populations: { id: nil })
end
It depends on your data if Thanh's version (which compares a potentially huge list of ids) or this version (which makes a sightly more complex join but doesn't need to compare against a list of ids) is more performant.

Related

How to remove N+1 queries

I have a rails API that currently has quite a few N+1 queries that I'd like to reduce.
As you can see it's going through quite a few loops before returning the data.
The relationships are as follows:
Company Model
class Company < ApplicationRecord
has_many :jobs, dependent: :destroy
has_many :contacts, dependent: :destroy
has_many :listings
end
Job Model
class Job < ApplicationRecord
belongs_to :company
has_many :listings
has_and_belongs_to_many :technologies
has_and_belongs_to_many :tools
scope :category, -> ( category ) { where category: category }
end
Listing Modal
class Listing < ApplicationRecord
belongs_to :job, dependent: :destroy
belongs_to :company, dependent: :destroy
scope :is_active, -> ( active ) { where is_active: active }
end
Job Serializer
class SimpleJobSerializer < ActiveModel::Serializer
attributes :id,
:title,
:company_name,
attribute :technology_list, if: :technologies_exist
attribute :tool_list, if: :tools_exist
def technology_list
custom_technologies = []
object.technologies.each do |technology|
custom_technology = { label: technology.label, icon: technology.icon }
custom_technologies.push(custom_technology)
end
return custom_technologies
end
def tool_list
custom_tools = []
object.tools.each do |tool|
custom_tool = { label: tool.label, icon: tool.icon }
custom_tools.push(custom_tool)
end
return custom_tools
end
def tools_exist
return object.tools.any?
end
def technologies_exist
return object.technologies.any?
end
def company_name
object.company.name
end
end
Current query in controller
Job.eager_load(:listings).order("listings.live_date DESC").where(category: "developer", listings: { is_active: true }).first(90)
I've tried to use eager_load to join the listings to the Jobs to make the request more efficient but i'm unsure how to handle this when some of the n+1 queries are coming from inside the serializer as it tries to look at tools and technologies.
Any help would be much appreciated!
You might was well eager load tools and technologies since you know that the serializer is going to use them:
Job.eager_load(:listings, :tools, :technologies)
.order("listings.live_date DESC")
.where(category: "developer", listings: { is_active: true })
.first(90)
After that you really need to refactor that serializer. #each should only be used when you are only interested in the side effects of the iteration and not the return value. Use #map, #each_with_object, #inject etc. These calls can be optimized. return is implicit in ruby so you only explicitly return if you are bailing early.
class SimpleJobSerializer < ActiveModel::Serializer
# ...
def tool_list
object.tools.map { |t| { label: tool.label, icon: tool.icon } }
end
# ...
end
Try nested preload:
Job.preload(:technologies, :tools, company: :listings).order(...).where(...)

Rails 5: How to allow model create through when polymorphic reference also carries distinct association

I have model with polymorhphic reference to two other models. I've also included distinct references per this article eager load polymorphic so I can still do model-specific queries as part of my .where clause. My queries work so I can search for scores doing Score.where(athlete: {foo}), however, when I try to do a .create, I get an error because the distinct reference alias seems to be blinding Rails of my polymorphic reference during validation.
Given that athletes can compete individually and as part of a team:
class Athlete < ApplicationRecord
has_many :scores, as: :scoreable, dependent: :destroy
end
class Team < ApplicationRecord
has_many :scores, as: :scoreable, dependent: :destroy
end
class Score < ApplicationRecord
belongs_to :scoreable, polymorphic: true
belongs_to :athlete, -> { where(scores: {scoreable_type: 'Athlete'}) }, foreign_key: 'scoreable_id'
belongs_to :team, -> { where(scores: {scoreable_type: 'Team'}) }, foreign_key: 'scoreable_id'
def athlete
return unless scoreable_type == "Athlete"
super
end
def team
return unless scoreable_type == "Team"
super
end
end
When I try to do:
Athlete.first.scores.create(score: 5)
...or...
Score.create(score: 5, scoreable_id: Athlete.first.id, scoreable_type: "Athlete")
I get the error:
ActiveRecord::StatementInvalid (SQLite3::SQLException: no such column: scores.scoreable_type
Thanks!
#blazpie, using your scoping suggestion worked for me.
"those scoped belongs_to can be easily substituted by scopes in Score: scope :for_teams, -> { where(scorable_type: 'Team') }

Ruby apply association on each item of collection

I am trying to access specific objects through associations applied one after the other on a collection. For example, one of my database request would be :
get_current_user.readable_projects.cards.find(params[:card_id]).tasks
get_current_user returns a unique User, readable_projects a collection of projects that this user can read. So far, so good. However, each project has many cards, and I'd like to retrieve all the cards from each project I have in the collection, so that in this result I can do a find for a specific card, and then retrieve its tasks, the association between Card and Task being also a has_many.
I could iterate through the projects with a each, but the problem is I want to use the default behavior of a find, so that if in all the cards from all the projects I don't find the one I was looking for, the default RecordNotFound routine is triggered. I could also use find_by and raise the exception manually, doing something like that :
#projects = get_current_user.readable_projects
#projects.each do |p|
#found = p.cards.find_by(id: params[:card_id])
break if #found.present?
end
if #found.present?
#tasks = #found.tasks
else
raise ActiveRecord::RecordNotFound
end
However my main objective is to get this card in a way anyone reading the code could easily understand what I am doing here.
All my model relationships are what follow :
User.rb :
has_many :reader_projects, -> { where "memberships.status = #{Membership::ACTIVE} AND memberships.role_id >= #{Membership::READER} " },
through: :memberships, :class_name => 'Project', :source => :project
has_many :contributor_projects, -> { where "memberships.status = #{Membership::ACTIVE} AND memberships.role_id >= #{Membership::CONTRIBUTOR} " },
through: :memberships, :class_name => 'Project', :source => :project
has_many :admin_projects, -> { where "memberships.status = #{Membership::ACTIVE} AND memberships.role_id >= #{Membership::ADMIN} " },
through: :memberships, :class_name => 'Project', :source => :project
def readable_projects
self.reader_projects + self.contributable_projects
end
def contributable_projects
self.contributor_projects + self.administrable_projects
end
def administrable_projects
self.admin_projects
end
Project.rb :
has_many :cards, inverse_of: :project, dependent: :destroy
Card.rb :
has_many :tasks, inverse_of: :card, dependent: :destroy
My question is : is there a way to do such kind of request in one very understandable line ?
Thank you in advance for your help.

can I add an includes extension to a belongs_to association?

I'm having trouble getting a belongs_to association to eager load it's children. I have:
class User < ActiveRecord::Base
has_many :campaigns, -> { includes :campaign_shirts, :arts, :selected_campaign_shirt }
belongs_to :selected_campaign, {class_name: "Campaign", inverse_of: :user}, -> { includes :campaign_shirts, :arts, :selected_campaign_shirt }
end
which results in:
// GOOD
u.campaigns.first.campaign_shirts.first.to_s
=> "#<CampaignShirt:0x007fc023a9abb0>"
u.campaigns.first.campaign_shirts.first.to_s
=> "#<CampaignShirt:0x007fc023a9abb0>"
// NOT GOOD
u.selected_campaign.campaign_shirts.first.to_s
(queries db)
=> "#<CampaignShirt:0x007fc023d7c630>"
(queries db)
u.selected_campaign.campaign_shirts.first.to_s
=> "#<CampaignShirt:0x007fc01af528a0>"
Am I running afoul of this issue? Is there a way to achieve what I want, which is to be able to refer to current_user.selected_campaign and have eager-loaded/frozen current_user.selected_campaign.campaign_shirts.first etc.?
Try moving the lambda scope before other association options like follows:
# app/models/users.rb
belongs_to :selected_campaign, -> { includes :campaign_shirts, :arts, :selected_campaign_shirt }, {class_name: "Campaign", inverse_of: :user},

(Rails 3) Combine two queries into one

I have these models simplified:
class Game::Champ < ActiveRecord::Base
has_one :contract, :class_name => "Game::ChampTeamContract", :dependent => :destroy
has_one :team, :through => :contract
# Attributes: :avg => integer
end
#
class Game::Team < ActiveRecord::Base
has_many :contracts, :class_name => "Game::ChampTeamContract", :dependent => :destroy
has_many :champs, :through => :contracts
end
#
class Game::ChampTeamContract < ActiveRecord::Base
belongs_to :champ
belongs_to :team
# Attributes: :expired => bool, :under_negotiation => bool
end
#
So what I want to do here is to find all Game::Champs that have no Game::ChampTeamContract whatsoever OR has, but (is not :under_negociation OR is :expired ), sorted by Champ.avg ASC
I am kinda stuck at using two queries, concating the result and sorting it. I wish there were a better way to to it more "Railish"
UPDATE: Just added a constraint about :expired
Try something like:
Game::Champs.
joins("left outer join game_champ_team_contracts on game_champ_team_contracts.champ_id = game_champs.id").
where("game_champ_team_contracts.id is null or (game_champ_team_contracts.state != ? or game_champ_team_contracts.state = ?)", :under_negotiation, :expired).
order("game_champs.avg ASC")
This is a fairly nasty line if left as-is, so if you use this, it needs tidying up. Use scopes or methods to split it up as much as possible!
I just tested with a super simple query:
#bars1 = Bar.where(:something => 1)
#bars2 = Bar.where(:something => 2)
#bars = #bars1 + #bars2
Not sure if it's right, but it works...

Resources