How to make a simple join with query in rails - ruby-on-rails

I am really new to Rails, but I had some experience with sql, so right now I am really struggling with doing a simple thing in rails syntax.
So, there are two tables:
class WorkshopMetadata < ActiveRecord::Base
attr_accessible :uuid, :action
belongs_to :workshop
end
class Workshop < ActiveRecord::Base
has_many :workshop_metadatas
end
And the query I want to do is:
SELECT workshops.*
FROM workshops LEFT JOIN
(SELECT workshop_metadatas.workshop_id as id, workshop_metadatas.uuid
FROM workshop_metadatas WHERE uuid = 'smth') as metadatas
WHERE uuid IS NULL
I know that to do left join you have to use includes, but how do I include query, not the table? I am completely baffled by this.
Thank you!
P.S. And while we are at it, are there any good and comprehensive docs for rails? The one that are listing all the available arguments for includes method, for example.

I'd recommend reading through the ActiveRecord query interface guide - it's a bit verbose, but it's got a lot of great pointers! For the exhaustive API reference I'd normally point you to http://api.rubyonrails.org/, but it's pretty bare for ActiveRecord::QueryMethods :( The guide is the best bet...
Let me rephrase your query to make sure I have it correct: You want to select all workshops that do not have a metadata row with a uuid of smth?
Thankfully, rails lets you drop down to query fragments, so you should be able to do it via:
Workshop.joins(
'LEFT JOIN workshop_metadatas ON workshop_metadatas.workshop_id = workshops.id'
).where('uuid IS NULL')
(Isn't that expressing the same thing w/o the subquery? If not: you should be able to pass your subquery into the joins call)

Related

In 1-to-many Rails ActiveRecord relation, what is the best practice to get parent records that they only have children records?

I have a Post model that has many comments
class Post < ApplicationRecord
has_many :comments
end
How do I get distinct posts that have comments excluding the posts that do not have comments?
I believe that Post.joins(:comments).distinct("posts.*") does the job.
Is there any better alternative?
I emphasize that the result should not have duplicate entries of posts, which is what happens when we join with comments (one-to-many relationship).
Looking at this again, and considering Mark's comment to my original solution, the simplest solution may just be to drop the "posts.*" from your original.
Post.joins(:comments).distinct
This works, though obviously a matter of taste as to whether it is better:
Post.left_outer_joins(:comments).where.not(comments: {id: nil})
And left_outer_joins came in with Rails 5, so with earlier versions a more verbose solution is needed. See Finding nil has_one associations in where query
The nicest thing about it, is it pairs nicely with the opposite query:
Post.left_outer_joins(:comments).where(comments: {id: nil})
Which is all the posts without comments
You don't have to really use distinct at the end, it's not at all effective as you are selecting everything by default (posts.*) which has a unique primary key (id) for each record.
Post.joins(:comments)
# SELECT `posts`.* FROM `posts` INNER JOIN `comments` ON `comments`.`post_id` = `posts`.`id`
Note:
To see the difference yourself please try to run below queries.
Post.joins(:comments).count
Post.joins(:comments).distinct.count
# Both should be giving you the same numbers.
You can also achieve this using subquery:
Post.where(id: Comment.select(:post_id).distinct)
Load time depends on the number of comments in the database.

Fun Rails ActiveRecord Join with attributes

I'm feeling rusty on my basic rails and working on a simple music stats service.
class User
has_many :monthly_top_artists # this model also has play_count and month attrs- not just a join
has_many :top_artists, through: monthly_top_artists, source: :artist
I've basically written my own #monthly_top_artists query method which queries for a specific month's artists. right now i'm working with a line like this:
current_top_artists = MonthlyTopArtist.where(user_id: id, month: time_range)
.joins(:artist).select('month,name,play_count,user_id,artist_id,monthly_top_artists.updated_at')
The purpose of this is to load up my MonthlyTopArtist model with all the artist data as well- but am I just forgetting some basic rails/active record methods that can handle this for me? I could also use user.top_artists with the code above, but this would give me the same problem- the monthly statistics are in another table. Am I overthinking this in trying to limit my db queries? I realize that writing my own getter method is a little bit more than vanilla rails but this join + selecting attributes not defined on my MTA model is making me think there is something bigger wrong with my schema (or i'm just forgetting something).

Active records complex relationship query

I have the following scenario:
User->HABTM->businesses
Suppliers->HABTM->businesses
Suppliers->HAS_MANY->Payments
I am having real trouble working out how to get all the payments for a user through the HABTM relationships that describe the business->supplier and User->business relationship.
I am after all payments that belong to the user through the supplier business relationship.
I can do this with SQL very easily but am having trouble doing it the rails way.
It's similar to a post on getting all the comments that belong to a user via a post model.
Any help would be appreciated.
Is it even possible?
I am doing this at the moment:
has_many :payments,:finder_sql => Proc.new {
%Q{
SELECT DISTINCT *
FROM payments
INNER JOIN businesses_users ON businesses_users.user_id
INNER JOIN businesses_suppliers ON businesses_suppliers.business_id
WHERE payments.supplier_id = businesses_suppliers.supplier_id AND businesses_users.user_id = #{id}
ORDER BY payments.created_at
}}
This lets me do user.payments
Kinda pressed for time, but wanted to chime in. First, your models seem to suggest that there might be an IS A relationship between User and Supplier, in which case you could employ polymorphic associations. If that's not the case, I'd look into the includes option within the activerecord query interface. In that manner, you can basically force AR to eager load the relationships down the chain. An example might look like:
all_users_and_their_pmts = User.includes(:businesses => {:suppliers => :payments })
An alternative, but highly inefficient, way to do this would be:
user_record.businesses.map {|b| b.suppliers.map {|s| s.payments}}.flatten
which would give you an array of payments. Using raw sql like you have above will be far more efficient than this, since activerecord can't chain the calls within map{}. I think :include would be a more idiomatic way for you to go, but your solution isn't horrible.

Rails One-to-Many Query Question

I feel like I'm missing something obvious here, so hopefully someone can help me out.
Lets say I have 3 models: Buildings, Shelves, and Books.
The problem I am trying to solve is to remove all the shelves which have no books out of a particular group of buildings.
The basic structure would look like this:
class Building
has_many :shelves
end
class Shelf
has_many :books
belongs_to :building
end
class Book
belongs_to :shelf
end
I don't need this to be extremely efficient, but I do want to keep it from being horribly inefficient. More specifically, If I have a building whose empty shelves need to be deleted, Here is what I've been trying to do to find the shelves which are empty:
Shelf.joins(:books).where("books.id is NULL").where(:building_id => building_id)<other_conditions...>
However, this query isn't giving me the results I expect (its not returning any shelves when I know some are empty). I was hoping that the join would fill in NULL for the books.id column for a shelf with no books, but it appears I am mistaken.
Is there another way to accomplish this, or is there something simple that I'm missing?
Thanks for the help!
Try the same query with includes instead of joins.
books.id can never be NULL, because you asked for the books which belong to shelves. joins will only give you the records for which the association is set, so you're asking for the books whose ids are both NULL and NOT NULL.
This answer might shed some light on the differences between the two methods.

How to find all items not related to another model - Rails 3

I have a fairly complicated lookup i'm trying to do in Rails and I'm not entirely sure how hoping someone can help.
I have two models, User and Place.
A user is related to Place twice. Once for visited_places and once for planned_places. Its a many to many relationship but using has_many :through. Here's the relationship from User.
has_many :visited_places
has_many :visited, :class_name=>"Place", :through=>:visited_places, :source=>:place
has_many :planned_places
has_many :planned, :class_name=>"Place", :through=>:planned_places, :source=>:place
In place the relationship is also defined. Here's the definition there
has_many :visited_users, :class_name=>"User", :through=>:visited_places
has_many :planned_users, :class_name=>"User", :through=>:planned_places
I'm trying to write a find on Place that returns all places in the database that aren't related to a User through either visited or planned. Right now I'm accomplishing this by simply querying all Places and then subtracting visited and planned from the results but I want to add in pagination and I'm worried this could complicate that. Here's my current code.
all_places = Place.find(:all)
all_places = all_places - user.visited - user.planned
Anyone know how i can accomplish this in just a call to Place.find. Also this is a Rails 3 app so if any of the active record improvements make this easier they are an option.
How about something like:
unvisited_places = Place.find(:all, :conditions => "id NOT IN(#{visited_places.map(&:place_id)})")
That's the general idea -- it can be made more efficient and convenient depending on your final needs.
You don't show it but if I am right in assuming that the VisitedPlace and PlannedPlace models have a belongs_to :user relationships then those tables have a user_id secondary key, right?
So in that case I would think it would be most efficient to do this in the database in which case you are looking for a select across a table join of places, visited_places and planned_places where users.id is not in either of visited_places or planned_places
in sql:
select * from places where id not in
(
(select place_id from visited_places where user_id = ?)
union
(select place_id from planned_places where user_id=?)
)
If that query works, you can use as follows:
Places.find_by_sql(...the complete sql query ...)
I would not know how to write such a query, with an exclusion, in Rails 3 otherwise.
I ran into a similar desire recently... I wanted to get all Model1s that weren't associated with a Model2. Using Rails 4.1, here's what I did:
Model1.where.not(id: Model2.select(:user_id).uniq)
This creates a nested SELECT, like #nathanvda suggested, effectively letting the database do all the work. Example SQL produced is:
SELECT "model1s".* FROM "model1s" WHERE ("model1s"."id" NOT IN (SELECT DISTINCT "model2s"."model1_id" FROM "model2s"))

Resources