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

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

Related

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

How to make a simple join with query in 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)

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.

ActiveRecord: how to do nested queries on foreign keys?

I have 3 models, "Coach", "Team", and "Event"
An event has attributes away_team and home_team, both of which belong to Team. A team belongs to a coach, and a coach can have many teams.
What I'd like to do is find all events where the home_team is not coached by a particular coach. So, something along the lines of:
Event.where("home_team.team.coach_id NOT ?", coach.id)
The problem is figuring out how to write up that syntax. I'm guessing I need to include the "team" model, but I'm not sure how to write an activerecord include that joins through a specific foreign key.
In short, any ideas on how to do this without getting SQL errors (which I've had countless of tonight) would be very appreciated.
Cheers.
If I understand you correctly you need this kind of sql:
SELECT events.* FROM events JOIN team on events.home_team_id = teams.id WHERE teams.coach_id != coach_id
so in rails it should looks something like that:
Event.joins("join teams on events.home_team_id = teams.id").where("teams.coach_id != ?", coach_id)
Of course I don't know your exact table namings, so you should fit this solution to your problem.
If you have in Event model association: belogns_to: home_team, you can use only joins(:home_team).where(....)

Rails Eager Loading Question Find(:all, :include => [:model])

I have a Topic and a Project model. I have a Many-to-many ass between them (HABTM one).
In the Topic's Index Page, I want to display the number of projects that each topic have. So I have
#topics = Topic.all(:include => [:projects])
In my controller, and so far so good. The problem is that the Project Model is so big that the query is still really slow
Topic Load (1.5ms) SELECT * FROM "topics"
Project Load (109.2ms) SELECT "projects".*, t0.topic_id as the_parent_record_id FROM "projects" INNER JOIN "projects_topics" t0 ON "projects".id = t0.project_id WHERE (t0.topic_id IN (1,2,3,4,5,6,7,8,9,10,11))
Is there a way to make the second query not to select * but just the name or the ID? Because the counter_cache is not supported by the HABTM Ass, and I don't really want to implement it by myself... so is there a way to make this second query faster?
I just need to pull the count without loading the whole project object...
Thanks in advance,
Nicolás Hock Isaza
counter_cache is very easy to implement
you can convert habtm to double has_many, i.e. has_many :projects_topics in both project and topic model (and belongs_to in projects_topics) and then use counter_cache or do eager loading only on projects_topics
you can do :select => "count(projects_topics.id)", :group => "topics.id" but this won't work well with postgresql if you care about it...
The second option is the best IMO, I usually don't use habtm at all, only double has_many :)
To expand on Devenv's answer counter cache is what you would typically use for this kind of scenario.
From the api docs:
Caches the number of belonging objects
on the associate class through the use
of increment_counter and
decrement_counter. The counter cache
is incremented when an object of this
class is created and decremented when
it‘s destroyed. This requires that a
column named #{table_name}_count (such
as comments_count for a belonging
Comment class) is used on the
associate class (such as a Post
class). You can also specify a custom
counter cache column by providing a
column name instead of a true/false
value to this option (e.g.,
:counter_cache => :my_custom_counter.)
Note: Specifying a counter cache will
add it to that model‘s list of
readonly attributes using
attr_readonly.
Here is a screen cast from ryan bates' railscasts on counter_cache.
Here is an answer to a question I asked half a year ago where the solution was an easily implemented home-brew counter cache.

Resources