Display Similar Items With Having Distinct Count Rails 5.1 - ruby-on-rails

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

Related

Rails query where at least 1 of the associated records contains an attribute

I have a GroupMeetings table and a GroupMeetingsUser table which is a join table between User and GroupMeetings. I want to find all the GroupMeetings where at least 1 of the GroupMeetingsUser has an attribute.
Right now, this works:
#group_meetings = GroupMeeting.where('lang_one_id IN (?) AND lang_two_id IN (?) AND meeting_time >= ?', #user.languages.pluck(:id), #user.languages.pluck(:id), Date.today)
#new_group_meetings_id = []
#group_meetings.each do |meeting|
meeting.group_meetings_user.each do |user|
if(user.user.location === #user.location)
#new_group_meetings_id.push(meeting.id)
end
end
end
#group_meetings = GroupMeeting.where('id IN (?)', #new_group_meetings_id)
But how can I include the .each loop in original GroupMeetings query instead? Like using .joins(:group_meetings_user) to find all the records where at least 1 of the users has an attribute?
class GroupMeeting < ApplicationRecord
has_many :group_meetings_users
has_many :users, through: :group_meetings_users
end
class GroupMeetingsUser < ApplicationRecord
belongs_to :user
belongs_to :group_meeting
validates_presence_of :user_id, :group_meeting_id
validates :user_id, :uniqueness => {:scope => :group_meeting_id, :message => 'can only join each group once.'}
end
UPDATE 1:
GroupMeeting.joins(:users).where(group_meeting: { lang_one_id: #user.languages.pluck(:id), lang_two_id: #user.languages.pluck(:id), meeting_time: DateTime.now..DateTime::Infinity.new}, user: { location: #user.location })
gives the error:
no such column: group_meeting.lang_one_id: SELECT "group_meetings".* FROM "group_meetings" INNER JOIN "group_meetings_users" ON "group_meetings_users"."group_meeting_id" = "group_meetings"."id" INNER JOIN "users" ON "users"."id" = "group_meetings_users"."user_id" WHERE "group_meeting"."lang_one_id" IN (29, 30, 31, 22) AND "group_meeting"."lang_two_id" IN (29, 30, 31, 22) AND ("group_meeting"."meeting_time" >= ?) AND "user"."location" = ?
I suggest the following:
GroupMeeting
.joins('INNER JOIN languages l1 ON languages l1.id = group_meetings.lang_one_id')
.joins('INNER JOIN languages l2 ON languages l2.id = group_meetings.lang_two_id')
.joins(:group_meeting_users => :users)
.where('meeting_time >= ?', Date.today)
.select('group_meetings.*')
.group('group_meetings.id')
.having('users.location = ?', #user.location)
It should be faster than doing sub-selects (using languages.id IN (?) and pluck)
Let me know if that works and/or if you have questions.

Active Record: find records by ceratin condition

My goal is to find three doctors with more than 1 review and with average rating >= 4
At the moment I'm using this service
class RatingCounterService
def get_three_best_doctors
doctors = find_doctors_with_reviews
sorted_doctors = sort_doctors(doctors)
reversed_hash = reverse_hash_with_sorted_doctors(sorted_doctors)
three_doctors = get_first_three_doctors(reversed_hash)
end
private
def find_doctors_with_reviews
doctors_with_reviews = {}
Doctor.all.each do |doctor|
if doctor.reviews.count > 0 && doctor.average_rating >= 4
doctors_with_reviews[doctor] = doctor.average_rating
end
end
doctors_with_reviews
end
def sort_doctors(doctors)
doctors.sort_by { |doctor, rating| rating }
end
def reverse_hash_with_sorted_doctors(sorted_doctors)
reversed = sorted_doctors.reverse_each.to_h
end
def get_first_three_doctors(reversed_hash)
reversed_hash.first(3).to_h.keys
end
end
Which is very slow.
My Doctor model:
class Doctor < ApplicationRecord
has_many :reviews, dependent: :destroy
def average_rating
reviews.count == 0 ? 0 : reviews.average(:rating).round(2)
end
end
Review model:
class Review < ApplicationRecord
belongs_to :doctor
validates :rating, presence: true
end
I can find all doctors with more than 1 review with this request
doctors_with_reviews = Doctor.joins(:reviews).group('doctors.id').having('count(doctors.id) > 0')
But how can I find doctors with an average rating >= 4 and order them by the highest rating if the "average rating" is an instance method?
Thanks to this answer :highest_rated scope to order by average rating
My final solution is
Doctor.joins(:reviews).group('doctors.id').order('AVG(reviews.rating) DESC').limit(3)

Rails ActiveRecord intersect query with has_many association

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|

Rails - how to make a condition with "has_many" association?

I have these two models:
car
class Car < ActiveRecord::Base
has_many :colors
end
Color
class Color < ActiveRecord::Base
belongs_to :car
end
Every car can have many colors.
What I am trying to do - to fetch all cars that don't have black colors.
Is there any way to do it in one query with using ActiveRecord? I can do it this way:
#cars = Car.where('brand = ?', params[:car])
#cars.each do |car|
car.colors.each do |color|
...test that the color is not black...
end
end
But this method is a bit slow... is there a faster approach of doing this with one query?
Thank you
We don't know the attributes on your Color model, but something like this is what you're looking for:
#cars = Car.where('brand = ?', params[:car])
#cars = #cars.joins(:colors)
.where("colors.name <> 'black'").each do |car|
Or, if you're using Rails 4:
#cars = Car.where('brand = ?', params[:car])
#cars = #cars.joins(:colors)
.where.not(colors: { name: :black }).each do |car|
Try this
Car.joins(:colors).where("colors.name <> 'black'").group('cars.id')
UPDATE:
You can probably do this in two queries
black_car_ids = Color.where(:name => 'black').pluck('DISTINCT car_id')
#cars = Car.where("id not in (?)", black_car_ids).where('brand = ?', params[:car])

How do I return all non deleted messages of a current thread in ruby on rails?

Having a little trouble figuring out a way to display messages for users message threads depending on which ones have been deleted and not. Deleted messages "sender_status or recipient_status" will change from 0 to 1 on deletion of a message. Currently to display a list of users message threads in their inbox I use:
e.g.
current_user = User.find(2)
current_user.message_threads
This grabs all their message_threads with 0 sender or recipient statuses.
class User < ActiveRecord::Base
has_many :messages
has_many :message_threads
Nice and easy but what I would like to do now is some how set up something that enables me to be able to grab messages with 0 sender or recipient statuses from a current thread. So if I was to type:
one_thread = current_user.message_threads.first
From this thread I'd want to be able to easily grab the only messages I needed. Would like to put something in the message_thread model so I could eventually type out:
current_user.message_threads.first.messages
#or
one_thread.messages
and have only the messages I needed loaded and the ones with "1" statuses ignored.
User Model:
class User < ActiveRecord::Base
has_many :messages
has_many :message_threads
def message_threads
MessageThread.where([ '(sender_id = ? AND sender_status = 0) OR (recipient_id = ? AND recipient_status = 0)', self.id, self.id ])
end
def messages
Message.where([ '(sender_id = ? AND sender_status = 0) OR (recipient_id = ? AND recipient_status = 0)', self.id, self.id ])
end
MessageThread model:
class MessageThread < ActiveRecord::Base
has_many :messages
Message model:
class Message < ActiveRecord::Base
acts_as_tree
has_one :message_thread
I tried experimenting with joins but this didn't work out well for me.
def messages
joins(:messages).merge( Message.where([ '(parent_id = ? AND sender_status = 0) OR (parent_id = ? AND recipient_status = 0)',self.message_id, self.message_id ]) )
end
Help would be much appreciate.
Kind regards
I'm guessing the problem is that one_thread is returning a MessageThread object, and then you're trying to get the user id by calling self.id, but it's being called on the wrong model.
I haven't tried this code but it might work:
class User < ActiveRecord::Base
def self.threads_in_inbox
message_threads.where('(sender_id = ? AND sender_status = 0) OR (recipient_id = ? AND recipient_status = 0)', self.id, self.id)
end
end
class MessageThread < ActiveRecord::Base
def self.inbox_messages
messages.where('(sender_id = ? AND sender_status = 0) OR (recipient_id = ? AND recipient_status = 0)', self.user.id, self.user.id)
# this hits the database 1 extra time however due to self.user.id
# you could change it to inbox_messages(user_id) and pass in the id manually
end
end
first_thread = current_user.threads_in_inbox.first
first_thread.inbox_messages
In your question it seems like you're overriding the association queries generated by rails (message_threads and messages) - not sure if it's on purpose but I changed it above.
Also, I think you can remove the duplication and do the following:
where('(sender_id = ?) AND (sender_status = 0 OR recipient_status = 0)', self.id)

Resources