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

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])

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 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|

How can I iterate through a model then iterate again in my view?

I want to pull data for each of my users. I grab their person_id from my user table, then use each person's ID to figure out how many days each person has available, and show that in my view.
I'm not sure if I am doing this correctly because I am iterating in my controller then again in my view.
def how_many_days_users_have
#my_group = User.all.pluck(:person_id)
#my_group.each do |v|
#indirect_id_v = Empaccrl.where("person_id = ? and is_active = ?", '#{v]', 'Y').pluck(:a_code).first
#v_range = Empaccrl.where("person_id = ? and is_active = ?", '#{v]', 'Y').pluck(:ac).first
#v_range_taken = Empaccrl.where("person_id = ? and is_active = ?", '#{v]', 'Y').pluck(:taken).first
#total_v_hours = #v_range.to_d - #v_range_taken.to_d
#total_v_days = #total_v_hours / 8
end
Then in my view I use this to show me this data:
%tr.trace-table
-#indirect_id_v.each do |idd|
%tr.trace-table
%td.trace-table{:style => 'border: solid black;'}= idd
-#total_v_days.each do |days|
%tr.trace-table
%td.trace-table{:style => 'border: solid black;'}= days
Okay, first things first, move some of that junk to your model, like so:
class Empaccrl < ActiveRecord::Base
def self.all_people
where(person_id: User.all.pluck(:person_id))
end
def self.active_people
all_people.where(is_active: 'Y')
end
def self.active_vacation_data
active_people.select(:person_id, :ac, :taken)
end
def total_v_hours
ac.to_d - taken.to_d
end
def total_v_days
total_v_hours / 8
end
end
Then you can use:
peoples_vacation_information = Empaccrl.active_vacation_data.all
peoples_vacation_information.map do |person|
p "person #{person.person_id} has #{person.total_v_days} vacation days"
end
Honestly, you don't even need all that, but I'm not sure why you are doing what you are doing, so I figured better be safe and add stuff. Whatever you don't need, just ignore.

Refactoring a complex filter in Rails

I'm trying to deal with a somewhat complicated query. I've read a few methods on how I might approach this but they don't really apply here because this isn't like a complicated search form (like on a vBulletin search post form), but rather a set of routes which filter both by 'category' (unreleased, popular, latest) and by 'time' (all time, last month, last week, today)
I realize the below code is very bad. My goal was only to get it working, and refactor after. Not to mention, it doesn't even truly work because it doesn't take into account BOTH category AND time, just one or the other, but I figured I would deal with that in this thread.
Also, to make this a lot clearer for this SO code paste, I excluded the .page(params[:page]).per(30) from every single line, however it needs to go on all of them.
So, does anyone know how I might go about doing this? I have mulled over it for some time and am kind of stumped
def index
case params[:category]
when "latest"
#books = Book.all.page(params[:page]).per(30)
when "downloads"
#books = Book.order('downloads DESC')
when "top100"
#books = Book.order('downloads DESC').limit(100)
when "unreleased"
#books = Book.unreleased
else
#books = Book.all.page(params[:page]).per(30)
end
case params[:time]
when "today"
#books = Book.days_old(1)
when "week"
#books = Book.days_old(7)
when "month"
#books = Book.days_old(30)
when "all-time"
#books = Book.all
else
#books = Book.all.page(params[:page]).per(30)
end
end
Routes:
# Books
get 'book/:id', to: 'books#show', as: 'book'
resources :books, only: [:index] do
get ':category/:time(/:page)', action: 'index', on: :collection
end
Move all queries to the model as scopes
class Book < ActiveRecord::Base
scope :downloads, -> { order('downloads DESC') }
scope :top100, -> { order('downloads DESC').limit(100) }
scope :unreleased, -> { unreleased }
scope :today, -> { days_old(1) }
scope :week, -> { days_old(7) }
scope :month, -> { days_old(30) }
scope :latest, -> { }
scope :all_time, -> { }
end
Create auxiliary methods to filter the params and avoid unmatching data
class BooksController < ApplicationController
private
def category_params
%w(downloads top100 unreleased).include?(params[:category]) ? params[:category].to_sym : nil
end
def time_params
%w(today week month latest all_time).include?(params[:time]) ? params[:time].to_sym : nil
end
end
Get rid of the case statement by applying the scope with the same name as the params
def index
query = Book.all
query = query.send(category_params) if category_params
query = query.send(time_params) if time_params
#books = query.page(params[:page]).per(30)
end
At four lines we're still within the boundaries of Sandi Metz' guidelines! :)
In rails you can 'chain' queries, for example
Book.where(:released => true).where(:popular => true)
is the same as
Book.where(:released => true, popular => true)
You can use this to help with your refactoring. Here is my take on it:
def index
# Start with all books, we are going to add other filters later
query = Book.scoped
# Lets handle the time filter first
query = query.where(['created_at > ?', start_date] if start_date
case params[:category]
when "latest"
query = query.order('created_at DESC')
when "downloads"
query = query.order('downloads DESC')
when "top100"
query = query.order('downloads DESC').limit(100)
when "unreleased"
query = query.where(:released => false)
end
# Finally, apply the paging
#books = query.page(params[:page]).per(30)
end
private
def start_date
case params[:time]
when "today"
1.day.ago
when "week"
7.days.ago
when "month"
1.month.ago
when "all-time"
nil
else
nil
end
end

Any possible way to add parameters to ON clause on include left joins on rails?

I have a huge complex query like this:
#objects = Object.joins({ x: :y }).includes(
[:s, { x: { y: :z } }, { l: :m },:q, :w,
{ important_thing:
[:h, :v, :c,:l, :b, { :k [:u, :a] }]
}
]).where(conditions).order("x.foo, x.bar")
Then i want to show all Objects and only Important_things that were created at between two dates.
If i put this on there where clause i dont get all Objects, only Objects that has Important_things between informed dates.
A solution using raw sql was this:
select * from objects left join important_things on important_things.object_id = objets.id and important_things.created_at between 'x' and 'y'
Instead of:
select * from objects left join important_things on important_things.object_id = objets.id where important_things.created_at between 'x' and 'y'
I really need all those objects and i don't want to use a raw SQL, any workaround or a possibility to pass parameters to the ON clause on an association?
I do this,
class VendorsRatings < ActiveRecord::Base
def self.ratings(v_ids,sort = "DESC")
joins("RIGHT OUTER JOIN vendors_lists v
ON v.vendor_id = vendors_ratings.vendor_id").where(conditions)
end
end
I did a ugly workaround:
class Parent < ActiveRecord::Base
cattr_accessor :dt_begin, dt_end
has_many :children, conditions: Proc.new { { created_at: (##dt_begin..##dt_end) } }
end
class MetasController < ApplicationController
def index
Parent.dt_begin = Date.parse(param[:dt_begin])
Parent.dt_end = Date.parse(param[:dt_end])
#parents = Parent.includes(:children).where("children.age = ?", params[:age])
end
end
So this way i get all Parents even if i dont have Children created_at between those specified dates.
And the most important part of it i have all objects inside the ActiveRecord.
But be careful because i did messed with cattr_accessor so i have to set everytime before i search for Parents.

Resources