Rails HABTM query only returning matching associations - ruby-on-rails

i'm trying create a record filter on their associations. So I have creatives and they have multiple talents. I want to have av view that filters the creatives with a specific talent. But still display each creatives multiple talents in the view.
class Creative
has_and_belongs_to_many :talents
end
Creative -> HABTM -> Talents
#creatives = Creative.includes(:talents, user: [:profile_image_attachment])
#creatives = #creatives.where(talents: { id: searched_talent_id })
The problem is that when displaying each creative it only returns the matching talent.
So rendering:
<% #creatives.each do |creative| %>
<% creative.talents.each do |talent| %>
<%= talent.name %>
<% end %>
<% end %>
Only shows the talent matched by the query, not all of them. I.e. the creative has multiple talents.
If I change the code to include a call to .all.
<% #creatives.each do |creative| %>
<% creative.talents.all.each do |talent| %>
<%= talent.name %>
<% end %>
<% end %>
Then I do get all talents BUT the database is hit with query for each creative.
Can I avoid this? I.e. eager loading all talents in creative and not getting limited by the one i search on!?

You can solve this by using a subquery:
#creatives = Creative.includes(:talents, user: [:profile_image_attachment])
.where(
id: Creative.joins(:talents)
.where(talents: { id: searched_talent_id })
)
This creates the following SQL:
SELECT
"creatives".* FROM "creatives"
WHERE
"creatives"."id" IN (
SELECT "creatives"."id"
FROM "creatives"
INNER JOIN "creatives_talents" ON "creatives_talents"."creative_id" = "creatives"."id"
INNER JOIN "talents" ON "talents"."id" = "creatives_talents"."talent_id"
WHERE "talents"."id" = $1
)
LIMIT $2
This only applies the WHERE clause to the subquery instead of the rows fetched by .includes.

Related

ruby - Order a group_by collection desc?

I have a collection of products users have purchased, grouped by their name, so I can count the number of unique products and how many of each has been purchased:
Controller:
#line_items = Spree::LineItem.joins(:order).where(spree_orders: {state: "complete"})
#products = #line_items.group_by(&:name)
View:
<% #products.each do |name, line_items| %>
<%= name %> - <%= line_items.count %><br>
<% end %>
Is there a way to order the .each loop so that it descends by line_items.count?
Thanks
It will perform better getting the correct data directly from the db:
#products = #line_items.group(:name).order("count_all DESC").count
That will give you the names and counts directly, e.g.
# => { "line_1" => 3, "line_2" => 2, "line_3" => 8 }
There's a bit of Rails magic at work here: the SQL generated using group, order and count will look like:
SELECT COUNT(*) AS count_all, name AS name FROM spree_line_items GROUP BY name ORDER BY count_all DESC
That's where count_all comes from: Rails attaches it to the count column automatically.
Then you can plug this directly into your view:
<% #products.each do |name, line_item_count| %>
<%= name %> - <%= line_item_count %><br>
<% end %>
Alternatively, if you're using the instance variable elsewhere, here's a simple Ruby solution:
#products = #line_items.group_by(&:name).sort_by { |_k, line_items| line_items.count }.reverse
This simply uses sort_by to get records ordered by the relevant count, then reverses to get decending order. There's a good benchmark on doing this here.
Hope that helps - let me know how you get on / if you've any questions.

ActiveRecord select all columns from multiple tables after joins

I have two models Article - :id, :name, :handle Comment - :id, :name, :article_id
My query looks like data = Article.select("articles.*, comments.*").joins("INNER JOIN comments on articles.id = comments.article_id")
Now both the models have conflicting fields. Ideally I would want to be able to do something like data.first.comments.name or data.first.articles.name.
Note I am aware of option of doming something like articles.name as article_name but I have some tables with around 20 columns. So don't want to do that.
The example you are showing is barely utilising the Rails framework at all. It seems you are thinking to much of the database structure instead of thinking of the result as Ruby Objects.
Here is what I would suggest you do to get access to the data (since you are using Rails, I assume it is for a webpage so my example is for rendering an html.erb template):
# In controller
#articles = Article.includes(:comments)
# In views
<% #articles.each do |article| %>
<h1><%= article.name %></h1>
<% article.comments.each do |comment| %>
<p><%= comment.name %></p>
<% end %>
<% end %>
But if you just want all the column data into a big array or arrays (database style), you could do it like this
rows = Article.includes(:comments).inject([]) { |arr, article|
article.comments.inject(arr) { |arr2, comment|
arr2 << (article.attributes.values + comment.attributes.values)
}
}
If you don't know how the inject method works, I really recommend to read up on it because it is a very useful tool.

Joins + group query works but throws error in view

I'm working on an app with applicants and answers. Association:
applicant has_many :answers
I want to return only the applicants whose answers are not archived (in the answers table, archived: false). Currently, this is my query:
def index
#super_admin = current_admin.super_admin
#applicants = Applicant.joins(:answers)
.where(answers: {archived: false})
.group("applicant_id")
end
The query runs fine, but I run into trouble in the view. I want to list the ids of all of the applicants returned by the query and link to that applicant's answers (as found by their answers.applicant_id). I added the .group in the index action because without it, I was getting every answer that wasn't archived (as opposed to every applicant whose answers weren't archived). So for applicant 4, I want to see one link '4' to lead to all of their answers.
Here's the link in the view:
<% #applicants.each do |a| %>
<%= link_to "#{a.applicant_id}", "/admins/view/#{a.applicant_id}" %><br>
<% end %>
It throws this error:
PG::GroupingError: ERROR: column "applicants.id" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: SELECT "applicants".* FROM "applicants" INNER JOIN "answers"...
^
: SELECT "applicants".* FROM "applicants" INNER JOIN "answers" ON "answers"."applicant_id" = "applicants"."id" WHERE "answers"."archived" = $1 GROUP BY applicant_id
Any thoughts? Thank you!!!
I found a workaround (it's not as sleek as it could be, but it'll get me through a presentation tomorrow):
def index
#super_admin = current_admin.super_admin
#ids = Applicant.all
#applicants = Applicant.joins(:answers)
.where(answers: {archived: false})
.group("applicant_id")
end
view:
<% #ids.each do |a| %>
<%= link_to "#{a.id}", "/admins/view/#{a.id}" %><br>
<% end %>

Rails Single Results from Multiple Tables

Using Rails 4.2
I have two models, suppliers and clients. Both models contain a name (string) and email (string). They do not have any relationship between them.
I would like to generate a list of all the names and emails from both suppliers and clients. In this list I would also like to know if the partner is a supplier or client.
Controller
#suppliers = Supplier.all
#clients = Client.all
#all_partners = (#suppliers + #clients).sort { |x, y| x.name <=> y.name }
View
<% #all_partners.each do |partner| %>
<%= partner.name %>, <%= partner.email %>, <%= partner.type %>
<!-- I need some way to know if the partner type is a supplier or client -->
<% end %>
How can I put in which type of partner it is? Is there a way to do this with one single AR call or query? This is basically how to use an SQL Union statement in Rails.
You could get the class name of the object I believe <%= partner.class.model_name.human %>
Thanks for the help all.
I ended up using the same controller as in the question, with some additional information in the view.
View
<% #all_partners.each do |partner| %>
<%= partner.name %>, <%= partner.email %>, <%= partner.try(:client_type) %>, <%= partner.class.model_name.human %>
<% end %>
Union in ActiveRecord works only within a single model. You could use union for two different tables using raw SQL, something like this:
Supplier.connection.execute("(SELECT id, ..., 'suppliers' as table FROM suppliers WHERE...) UNION (SELECT id,... 'clients' as table FROM clientsWHERE...)")
but the result would be of type PG::Result.
So the best way, unfortunately, is to use two ActiveRecord queries.
OR if clients and suppliers have similar fields, you could put them in one table
class Partner < ActiveRecord::Base
default_scope where(is_supplier: true)
scope :clients, -> { where(is_supplier: false) }
end
so Partner.all will output only suppliers, Partner.unscoped - all partners

Rails 3.1 adding an object to an array returned by find_by_sql / can't show in erb file

I'm porting a php app to rails so I have a set of sql statements that I'm converting to find_by_sql's. I see that it returns a collection of objects of the type that I called on it. What I'd like to do is then iterate through this collection (presumably an array) and add an instance of a specific object like this:
#class is GlobalList
#sql is simplified - really joining across 3 tables
def self.common_items user_ids
items=find_by_sql(["select gl.global_id, count(gl.global_id) as global_count from main_table gl group by global_id"])
#each of these items is a GlobalList and want to add a location to the Array
items.each_with_index do |value,index|
tmp_item=Location.find_by_global_id(value['global_id'])
#not sure if this is possible
items[index]['location']=tmp_item
end
return items
end
controller
#controller
#common_items=GlobalList.common_items user_ids
view
#view code - the third line doesn't work
<% #common_items.each_with_index do |value,key| %>
<%=debug(value.location) %> <!-- works -->
global_id:<%=value.location.global_id %> <!-- ### doesn't work but this is an attribute of this object-->
<% end %>
So I have 3 questions:
1. is items an Array? It says it is via a call to .class but not sure
2. I am able to add location these GlobalList items. However in the view code, I cannot access the attributes of location. How would I access this?
3. I know that this is pretty ugly - is there a better pattern to implement this?
I would get any data you need from locations in the sql query, that way you avoid the n+1 problem
#items=find_by_sql(["select locations.foo as location_foo,
gl.global_id as global_id,
count(gl.global_id) as global_count
from main_table gl
inner join locations on locations.global_id = gl.global_id
group by global_id"])
Then in the view:
<% #items.each do |gl| %>
<%= gl.location_foo %> <-- select any fields you want in controller -->
<%= gl.global_count %>
<%= gl.global_id %>
<% end %>
If you want to keep it close to as is, I would:
def self.common_items user_ids
items=find_by_sql(["select gl.global_id, count(gl.global_id) as global_count
from main_table gl group by global_id"])
items.map do |value|
[value, Location.find_by_global_id(value.global_id)]
end
end
Then in the view:
<% #common_items.each do |value,location| %>
<%=debug(location) %>
global_id:<%=value.global_id %>
<% end %>

Resources