Complex Query with Has many and belongs to for RAILS 3 - sql

I am trying to do a QUERY in my controller to get a list of suppliers with a category ID.
I have my models set up like this.
class Supplier < ActiveRecord::Base
has_and_belongs_to_many :sub_categories
end
class Category < ActiveRecord::Base
has_many :sub_categories
end
class SubCategory < ActiveRecord::Base
belongs_to :category
has_and_belongs_to_many :suppliers
end
A supplier can have Many sub_categories that are under one single category. So i can grab the category of a supplier by doing this.
#supplier.sub_categories.first.category.name
This returns the category that the supplier comes under because they have to have at least 1 sub category which is then linked to a category.
What i am trying to do is by passing a category_id i wish to return all suppliers that come under that category.
I had it written like this but it doesnt seem to be working.
#category = Category.find(params[:category_id])
#suppliers = Supplier.where('sub_category.first.category.id = ?', #category.id)
i get the following sql error
Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '.id = 20)' at line 1: SELECT `suppliers`.* FROM `suppliers` WHERE (sub_category.first.category.id = 20)

Well, that's certainly an sql error. The stuff inside the where() call gets translated directly to SQL, and that's not sq;l. :)
You need to join tables together. I'm assuming there's a sub_category_suppliers table that completes the habtm association. (BTW, I much prefer to use has_many :through exclusively)
I think it would be something like this:
Supplier.joins(:sub_category_suppliers => :sub_categories).
where('sub_categories.category_id =?', #category.id).
group('suppliers.id')
As Caley Woods suggested, this should be placed in the Supplier model as a scope:
scope :by_category, lambda { |category_id|
joins(:sub_category_suppliers => :sub_categories).
where('sub_categories.category_id =?', category_id).
group('suppliers.id')
}
and then called as Supplier.by_category(#category.id)

Related

Get most recent records from deeply nested model

Say I have 3 models:
ModelA has many ModelB
ModelB has many ModelC
I'm querying ModelA, but in ModelC I have multiple ones of the same type, let's say I have 3 but I only need the most recently one.
I tried to do something like this...
records = ModelA.where(some query).includes ModelB includes ModelC
// convert activerecord collection to array
records = records.to_a
records.each do |record|
record.modelBs.each do |modelB|
filter the modelCs i don't need
modelB.modelCs = filteredModelCs
end
end
return records
but instead of merely returning the array of records, an UPDATE sql query is run and the db records are modified. this is a surprise because i never used the .save method and i thought i had converted the collection from an active record collection to an array
How can I filter deeply nested records without modifying the db records? then i can return the filtered result
Assigning a list of instances to a has_many collection with = will immediately persist the changes to the database.
Instead, I would try to solve this with more specific associations like this:
class A
has_many :bs
has_many(:cs, through: :bs)
has_one :recent_c, -> { order(created_at: :desc).limit(1) }, source: :cs
class B
has_many :cs
With those associations, I would expect the following to work:
as = A.where(some query).includes(:recent_c)
as.each do |a|
a.recent_c # returns the most recent c for this a
end
If I got you right, you want to get a collection of latest Cs, which are connected to Bs, which are connected to certain A-relation? If so, you can do something like that (considering you have tables as, bs and cs):
class A < ApplicationRecord
has_many :bs
end
class B < ApplicationRecord
belongs_to :a
has_many :cs
end
class C < ApplicationRecord
belongs_to :b
scope :recent_for_bs, -> { joins(
<<-sql
INNER JOIN (SELECT b_id, MAX(id) AS max_id FROM cs GROUP BY b_id) recent_cs
ON cs.b_id = recent_cs.b_id AND cs.id = recent_cs.max_id
sql
) }
end
And then you would query Cs like that:
C.recent_for_bs.joins(b: :a).merge(A.where(some_query))
You get recent Cs, inner join them with Bs and As and then get records connected to your A-relation by merging it.

Create a scope in rails based on HABTM association count

I'm trying to create a rails scope based on the count of a model's HABTM assocation, but I'm struggling with the SQL.
I want Match.open to return matches with less than two users. I also have Match.upcoming, which returns matches with a 'future_date' in the future, which is working well.
My code:
class Match < ActiveRecord::Base
has_and_belongs_to_many :users
scope :open, joins('matches_users').
select('*').
group('matches.id').
having('count(matches_users.user_id) < 2')
scope :upcoming, lambda {
where("proposed_date between ? and ?", Date.today, Date.today.next_month.beginning_of_month)
}
I'm currently getting the error:
SQLite3::SQLException: no such column: matches_users.user_id: SELECT * FROM "matches" matches_users GROUP BY matches.id HAVING count(matches_users.user_id) < 2
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: matches_users.user_id: SELECT * FROM "matches" matches_users GROUP BY matches.id HAVING count(matches_users.user_id) < 2
I'm currently achieving this with a class method:
def self.open
self.select{|match| match.users.length < 2}
end
Which works, but I'd really like to move this into a scope for speed, and so that I can chain the scopes like Match.open.upcoming.
What am I doing wrong here? What's the correct way to do this? Any help would be appreciated.
Give this a shot - I've used something similar before and it seems to work for me:
class Match < ActiveRecord::Base
has_and_belongs_to_many :users
scope :open, joins(:matches_users)
.select('matches.*')
.group('matches.id')
.having('count(matches_users.id) < 2')
...
end

Filtering Parents by Children

I'm doing a simple blog application - There are posts, which have many tags through a posts_tags table (my models are below). What I have implemented is if a user clicks a tag, it will show just the posts with that tag. What I want is for the user to them be able to select another tag, and it will filter to only the posts that have both of those tags, then a third, then a fourth, etc. I'm having difficulty making the active record query - especially dynamically. The closest I've gotten is listed below - however its in pure SQL and I would like to at least have it in ActiveRecord Rubyland syntax even with the complexity it contains.
Also, the "having count 2" does not work, its saying that "count" does not exist and even if I assign it a name. However, it is outputting in my table (the idea behind count is that if it contains a number that is as much as how many tags we are searching for, then theoretically/ideally it has all the tags)
My current test SQL query
select posts_tags.post_id,count(*) from posts_tags where tag_id=1 or tag_id=3 group by post_id ### having count=2
The output from the test SQL (I know it doesnt contain much but just with some simple seed data).
post_id | count
---------+-------
1 | 2
2 | 1
My Models:
/post.rb
class Post < ActiveRecord::Base
has_many :posts_tags
has_many :tags, :through => :posts_tags
end
/tag.rb
class Tag < ActiveRecord::Base
has_many :posts_tags
has_many :posts, :through => :posts_tags
end
/poststag.rb
class PostsTag < ActiveRecord::Base
belongs_to :tag
belongs_to :post
end
Give a try to:
Post.joins(:tags).where(tags: {id: [1, 3]}).select("posts.id, count(*)").group("posts.id").having("count(*) > 2")
I think "count = 2" is not correct. It should be "count(*) = 2". Your query then will be
select post_id,count(post_id)
from posts_tags
where tag_id = 1 or tag_id = 3
group by post_id
having count(post_id) = 2
In general you want to stay away from writing raw sql when using rails. Active Record has great helper methods to make your sql more readable and maintainable.
If you only have a few tags you can create scopes for each of them (http://guides.rubyonrails.org/active_record_querying.html#scopes)
Since people are clicking on tags one at a time you could just query for each tag and then use the & operator on the arrays. Because you have already requested the exact same set of data from the database the query results should be cached meaning you are only hitting the db for the newest query.

Rails: Many to one ( 0 - n ) , finding records

I've got tables items and cards where a card belongs to a user and a item may or may not have any cards for a given user.
The basic associations are set up as follows:
Class User
has_many :cards
Class Item
has_many :cards
Class Card
belongs_to :user
has_and_belongs_to_many :items
I've also created a join table, items_cards with the columns item_id and card_id. I'd like to make a query that tells me if there's a card for a given user/item. In pure SQL I can accomplish this pretty easily:
SELECT count(id)
FROM cards
JOIN items_cards
ON items_cards.card_id = cards.id
WHERE cards.user_id = ?
AND items_cards.item_id = ?
I'm looking for some guidance as to how I'd go about doing this via ActiveRecord. Thanks!
Assuming you have an Item in #item and a User in #user, this will return 'true' if a card exists for that user and that item:
Card.joins(:items).where('cards.user_id = :user_id and items.id = :item_id', :user_id => #user, :item_id => #item).exists?
Here's what's going on:
Card. - You're making a query about the Card model.
joins(:items) - Rails knows how to put together joins for the association types it supports (usually - at least). You're telling it to do whatever joins are required to allow you to query the associated items as well. This will, in this case, result in JOIN items_cards ON items_cards.card_id = cards.id JOIN items ON items_cards.item_id = items.id.
where('cards.user_id = :user_id and items.id = :item_id', :user_id => #user, :item_id => #item) - Your conditional, pretty much the same as in pure SQL. Rails will interpolate the values you specify with a colon (:user_id) using the values in the hash (:user_id => #user). If you give an ActiveRecord object as the value, Rails will automatically use the id of that object. Here, you're saying you only want results where the card belongs to the user you specify, and there is a row for the item you want.
.exists? - Loading ActiveRecord objects is inefficient, so if you only want to know if something exists, Rails can save some time and use a count based query (much like your SQL version). There's also a .count, which you could use instead if you wanted to have the query return the number of results, rather than true or false.

Active Record find using conditional based on multi level association's attribute

I am getting missing FROM clause error (shown below)
I want to find the entries whose associated reports' city is the same as the users' city, thus only showing relavant entries to users. So, if a user belongs to a city that a report does, the user should be shown the entries from that report.
I tried a few things, one being this scope in the Entry model:
scope :report_in_city, lambda { |user| joins(:report).where("report.city_id = ?", user.city_id) }
And if I call Entry.report_in_city(user)
I get this error:
SELECT "entries".* FROM "entries" INNER JOIN "reports" ON "reports"."id" = "entries"."report_id" WHERE (report.city_id = 1)
ActiveRecord::StatementInvalid: PG::Error: ERROR: missing FROM-clause entry for table "report"
LINE 1: ... ON "reports"."id" = "entries"."report_id" WHERE (report.cit...
^
: SELECT "entries".* FROM "entries" INNER JOIN "reports" ON "reports"."id" = "entries"."report_id" WHERE (report.city_id = 1)
I have a few models set up like this:
class Report
belongs_to user
belongs_to city
has_many entries
class Entry
belongs_to report
class User
has_many reports
belongs_to city
class City
has_many users
has_many reports
I am new to sql and would appreciate any advice on this query!
Since that string condition "report.city_id = ?" goes into the SQL, you have to use the table names. Rails is cute about going back and forth between plural and singular, but SQL only knows the table name reports. You can see that in the generated SQL like ON "reports"."id" = "entries"."report_id". The table name is plural.
Try this:
"reports.city_id = ?"
or the Rails way:
joins(:report).where(:report => { :city_id => user.city_id })