Rails ActiveRecord intersect query with has_many association - ruby-on-rails

I have the following models:
class Piece < ActiveRecord::Base
has_many :instrument_pieces
has_many :instruments, through: :instrument_pieces
end
class Instrument < ActiveRecord::Base
has_many :pieces, through: :instrument_pieces
has_many :instrument_pieces
end
class InstrumentPiece < ActiveRecord::Base
belongs_to :instrument
belongs_to :piece
end
And I have the following query:
Piece
.joins(:instrument_pieces)
.where(instrument_pieces: { instrument_id: search_params[:instruments] } )
.find_each(batch_size: 20) do |p|
Where search_params[:instruments] is an array. The problem with this query is that it will retrieve all pieces that have any of the instruments, so if search_params[:instruments] = ["1","3"], the query will return pieces with an instrument association of either 1 or 3 or of both. I'd like the query to only return pieces whose instrument associations include both instruments 1 and 3. I've read through the docs, but I'm still not sure how this can be done...

It seems like what I wanted was an intersection between the two queries, so what i ended up doing was:
queries = []
query = Piece.joins(:instruments)
search_params[:instruments].each do |instrument|
queries << query.where(instruments: {id: instrument})
end
sql_str = ""
queries.each_with_index do |query, i|
sql_str += "#{query.to_sql}"
sql_str += " INTERSECT " if i != queries.length - 1
end
Piece.find_by_sql(sql_str).each do |p|
Very ugly, but ActiveRecord doesn't support INTERSECT yet. Time to wait for ActiveRecord 5, I suppose.

You can use where clause chaining to achieve this. Try:
query = Piece.joins(:instrument_pieces)
search_params[:instruments].each do |instrument|
query = query.where(instrument_pieces: { instrument_id: instrument } )
end
query.find_each(batch_size: 20) do |p|
or another version
query = Piece.joins(:instruments)
search_params[:instruments].each do |instrument|
query = query.where(instrument_id: instrument)
end
query.find_each(batch_size: 20) do |p|

Related

Display Similar Items With Having Distinct Count Rails 5.1

I'm trying to display a list of gins that have a similar minimum number of botanicals on my show page. I feel I'm close, but the current output is not right. It's actually just printing the name of the gin a number of times.
Gin Load (1.6ms) SELECT "gins".* FROM "gins" INNER JOIN
"gins_botanicals" ON "gins_botanicals"."gin_id" = "gins"."id" INNER
JOIN "botanicals" ON "botanicals"."id" =
"gins_botanicals"."botanical_id" WHERE "botanicals"."id" IN (4, 10, 3)
AND ("gins"."id" != $1) GROUP BY gins.id HAVING (COUNT(distinct
botanicals.id) >= 3) [["id", 2]]
I have three models; two resources with a joins table:
gin.rb
class Gin < ApplicationRecord
belongs_to :distillery, inverse_of: :gins
accepts_nested_attributes_for :distillery, reject_if: lambda {|attributes| attributes['name'].blank?}
acts_as_punchable
has_many :gins_botanical
has_many :botanicals, through: :gins_botanical
botanical.rb
class Botanical < ApplicationRecord
has_many :gins_botanical
has_many :gins, through: :gins_botanical
gins_botanical.rb
class GinsBotanical < ApplicationRecord
belongs_to :gin
belongs_to :botanical
gins_controller
def show
#gin = Gin.friendly.find(params[:id])
#gin.punch(request)
#meta_title = meta_title #gin.name
#similiar_gins = Gin.joins(:botanicals).where("botanicals.id" => #gin.botanical_ids).where.not('gins.id' => #gin.id).having("COUNT(distinct botanicals.id) >= 3").group("gins.id")
end
so in #similar_gins i am trying to count how many matching botanicals does the current #gin have compared to all the other #gins and if >= 3 return the values.
And in my view:
show.html.erb
<% #similiar_gins.each do |gin| %>
<%= #gin.name %>
<% end %>
I'm suspecting my where is not correct...
Yes, I have the similar feature but I have implemented like below
#gin = Gin.find(params[:id])
if #gin.botanicals.count > 1
#botanicals = #gin.botanical_ids
#gin_ids = Botanical.select('distinct gin_id').where('gin_id IN (?)', #botanicals).limit(10)
#ids = #gin_ids.map(&:gin_id)
#similiar_gins = Gin.where('id IN (?)', #ids).where.not(id: #gin) #=> similar all without current gin
end
This code is converted from my code which is relation is category and jobs, if you need to see my code for showing the similar jobs then it is
def show
#job = Job.find(params[:id])
if #job.categories.count > 1
#category = #job.category_ids
#jobs = JobCategory.select('distinct job_id').where('category_id IN (?)', #category).limit(10)
ids = #jobs.map(&:job_id)
#releted_jobs = Job.where('id IN (?)', ids).where.not(id: #job)
end
end
Hope it helps

Rails association scope by method with aggregate

I'm trying to retrieve association records that are dependent on their association records' attributes. Below are the (abridged) models.
class Holding
belongs_to :user
has_many :transactions
def amount
transactions.reduce(0) { |m, t| t.buy? ? m + t.amount : m - t.amount }
end
class << self
def without_empty
includes(:transactions).select { |h| h.amount.positive? }
end
end
class Transaction
belongs_to :holding
attributes :action, :amount
def buy?
action == ACTION_BUY
end
end
The problem is my without_empty method returns an array, which prevents me from using my pagination.
Is there a way to rewrite Holding#amount and Holding#without_empty to function more efficiently with ActiveRecord/SQL?
Here's what I ended up using:
def amount
transactions.sum("CASE WHEN action = '#{Transaction::ACTION_BUY}' THEN amount ELSE (amount * -1) END")END")
end
def without_empty
joins(:transactions).group(:id).having("SUM(CASE WHEN transactions.action = '#{Transaction::ACTION_BUY}' THEN transactions.amount ELSE (transactions.amount * -1) END) > 0")
end

rails 4 - sort by number of votes

So... I have images. and those images have votes.
I currently have image.rb
class Image < ActiveRecord::Base
belongs_to :user
belongs_to :event
has_many :image_votes, dependent: :destroy
default_scope { order(ci_lower_bound) }
def taken_by? (photographer)
self.user == photographer
end
def self.ci_lower_bound
pos = image_votes.where(value: 1).size
n = image_votes.size
if n == 0
return 0
end
z = 1.96
phat = 1.0*pos/n
(phat + z*z/(2*n) - z * Math.sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n)
end
end
I've been playing around with this... the only way to get default scope to work is to use a method with self. I found that formula at http://www.evanmiller.org/how-not-to-sort-by-average-rating.html - how would I call this to make it work??
I'd create a new scope called by_votes, include sum() and order by this new column:
scope :by_votes, -> { select("#{Image.table_name}.*, sum(#{ImageVote.table_name}.votes) AS votes").order("votes DESC") }

How can I refactor this Rails controller?

I have the following in my controller:
#custom_exercises = #user.exercises.all
#all_exercises = Exercise.not_the_placeholder_exercise.public.order("name").all
if #user.trainers.present?
trainer_exercises = []
#user.trainers.each do |trainer|
trainer_exercises << trainer.exercises.all
end
#my_trainer_custom_exercises = trainer_exercises
end
#exercises = #custom_exercises + #all_exercises
if #my_trainer_custom_exercises.present?
#exercises << #my_trainer_custom_exercises
#exercises.flatten!
end
This feels really messy. How could I refactor this?
First step: set up an AR relationship between users and exercises, probably along the lines of:
class User < ActiveRecord::Base
has_many :trainer_exercises,
:through => :trainers,
:foreign_key => :client_id,
:source => :exercises
end
Second step: move #all_exercises to a class method in Exercise.
class Exercise < ActiveRecord::Base
def self.all_exercises
not_the_placeholder_exercise.public.order("name").all
end
end
This way, the whole controller gets a whole lot simpler:
#custom_exercises = #user.exercises.all
#trainer_exercises = #user.trainer_exercises.all
#exercises = Exercise.all_exercises + #custom_exercises + #trainer_exercises
From a purely less lines of code perspective, you could start with this ( more or less / not tested but should work:
if #user.trainers.present?
#my_trainer_custom_exercises = #user.trainers.each.inject([]){ |trainer, trainer_exercises|
trainer_exercises << trainer.exercises.all
}
end

N+1 while enumerating self-referencing records

I'm doing a pretty basic thing - displaying a tree of categories in topological order and ActiveRecord issues extra query for enumerating each category's children.
class Category < ActiveRecord::Base
attr_accessible :name, :parent_id
belongs_to :parent, :class_name => 'Category'
has_many :children, :class_name => 'Category', :foreign_key => 'parent_id'
def self.in_order
all = Category.includes(:parent, :children).all # Three queries as it should be
root = all.find{|c| c.parent_id == nil}
queue = [root]
result = []
while queue.any?
current = queue.shift
result << current
current.children.each do |child| # SELECT * FROM categories WHERE parent_id = ?
queue << child
end
end
result
end
end
UPD. As far as I understand what's going here is that when a category is referred as a children of some category it's not the same object as the one in the initial list and so it hasn't it's children loaded. Is there a way to implement desired behavior without resorting to creating extra adjacency list?
UPD2: Here's the manual adjacency list solution. It uses only one query but I'd really like to use something more idiomatic
def self.in_order_manual
cache = {}
adj = {}
root = nil
all.each do |c|
cache[c.id] = c
if c.parent_id != nil
(adj[c.parent_id] ||= []) << c.id
else
root = c.id
end
end
queue = [root]
result = []
while queue.any?
current = queue.shift
result << current
(adj[current] || []).each{|child| queue << child}
end
result.map{|id| cache[id]}
end

Resources