Is a double-level joins table possible? - ruby-on-rails

I have three tables, Show, Episode, and Character. Each Show has_many Episodes and Characters.
class Show < ActiveRecord::Base
has_many :episodes
has_many :characters
class Episode < ActiveRecord::Base
belongs_to :show
class Character < ActiveRecord::Base
belongs_to :show
I have a list of Characters. What I want to do is order them by the date that their Show's second Episode aired on. Episode has a uniquely named attribute called :air_date
So I would have to do a two-level joins table, something like:
#characters = Character.joins(:show)
.joins(:episodes)
.where(episodes: {number: 2})
.order(:air_date)
But ActiveRecord thinks I'm looking for Episodes associated with Characters, and of course that association doesn't exist.
How can I return the Episodes associated with the Shows associated with a list of Characters? Or is that even possible?

You should use this syntax (I assume air_date is an attribute of episodes):
Character.joins(show: :episodes)
.where(episodes: {number: 2})
.order('episodes.air_date ASC')
You should also be able to use AREL for that:
Character.joins(show: :episodes)
.where(episodes: {number: 2})
.order(Episode.arel_table[:air_date].asc)
If you want to load everything to memory you can use includes, preload or eager_load instead of joins
A complete explanation and examples can be found here.

#character = Character.find x
#shows = #character.show.where(episodes: {number: 2}).order(:air_date).joins(:episodes)
Completely untested - I remembered an ActiveRecord query I saw some time back which put the joins at the end. It was a similar use case (get only the x records which met particular conditions).

Related

How to access has_many through relationship possible in rails?

How can I access my related records?
class Post < ActiveRecord::Base
has_many :post_categories
has_many :categories, through: :post_categories
class Categories < ActiveRecord::Base
has_many :post_categories
has_many :post, through: :post_categories
class PostCategories < ActiveRecord::Base
belongs_to :post
belongs_to :category
PostCategories table has id, posts_id, and categories_id columns.
id | posts_id | categories_id
1. | 2 | 3
2. | 2 | 4
What I want is: to get posts related to a category. like: all Posts where in x category.
Yep, this is an easy one.
one_or_more_categories = # Category.find... or Category.where...
posts = Post.joins(:categories).where(category: one_or_more_categories)
Rails is clever enough to take either a model or a query that would find some data and turn that into an efficient appropriate query, that might be a subquery. Trying things out in the Rails console (bundle exec rails c) is a good way to see the generated SQL and better understand what's going on.
(EDIT: As another answer points out, if you've already retrieved a specific Category instance then you can just reference category.posts and work with that relationship directly, including chaining in .order, .limit and so-on).
Another way to write it 'lower level' would be:
Post.joins(:categories).where(category: {id: one_or_more_category_ids})
...which is in essence what Rails will be doing under the hood when given an ActiveRecord model instance or an ActiveRecord::Relation. If you already knew the e.g. category "name", or some other indexed text column that you could search on, then you'd adjust the above accordingly:
Post.joins(:categories).where(category: {name: name_of_category})
The pattern of joins and where taking a Hash where the join table name is used as a key with values nested under there can be taken as deep as you like (e.g. if categories had-many subcategories) and you can find more about that in Rails Guides or appropriate web searches. The only gotcha is the tortuous singular/plural stuff, which Rails uses to try and make things more "English-y" but sometimes - as in this case - just creates an additional cognitive burden of needing to remember which parts should be singular and which plural.
Not sure if this answers it but in ActiveRecord your Post will have direct access to your Category model and vice versa. So you could identify the category you want the posts from in a variable or an instance variable, and query #specific_category.posts. If you are doing this in your controller, you could even do it in before_action filter. If you are using it in serializers its not much different.
You could also create a scope in your Post model and use either active record or raw SQL to query specific parameters.
You also have an error in your Category model. Has many is always plural so it would be has_many :posts, through: :post_categories
Get the category object and you can directly fetch the related posts. Please see the following
category = Category.find(id)
posts = category.posts
Since you have already configured the has_many_through relation, rails will fetch post records related the category.

Get last comment of all posts in one category

Comment belongs to Post.
Post belongs to Category.
How would I get a collection of every lastly updated comment for each post, all belonging to one single category?
I've tried this but it just gives me one post:
category.posts.joins(:comments).order('updated_at DESC').first
Update
What I want is to fetch one commment per post, the last updated comment for each post.
Rails doesn't do this particularly well, especially with Postgres which forbids the obvious solution (as given by #Jon and #Deefour).
Here's the solution I've used, translated to your example domain:
class Comment < ActiveRecord::Base
scope :most_recent, -> { joins(
"INNER JOIN (
SELECT DISTINCT ON (post_id) post_id,id FROM comments ORDER BY post_id,updated_at DESC,id
) most_recent ON (most_recent.id=comments.id)"
)}
...
(DISTINCT ON is a Postgres extension to the SQL standard so it won't work on other databases.)
Brief explanation: the DISTINCT ON gets rid of all the rows except the first one for each post_id. It decides which row the first one is by using the ORDER BY, which has to start with post_id and then orders by updated at DESC to get the most recent, and then id as a tie-breaker (usually not necessary).
Then you would use it like this:
Comment.most_recent.joins(:post).where("posts.category_id" => category.id)
The query it generates is something like:
SELECT *
FROM comments
INNER JOIN posts ON (posts.id=comments.post_id)
INNER JOIN (
SELECT DISTINCT ON (post_id) post_id,id FROM comments ORDER BY post_id,updated_at DESC,id
) most_recent ON (most_recent.id=comments.id)
WHERE
posts.category_id=#{category.id}
Single query, pretty efficient. I'd be ecstatic if someone could give me a less complex solution though!
If you want a collection of every last updated Comment, you need to base your query on Comment, not Category.
Comment.joins(:post).
where("posts.category_id = ?", category.id).
group("posts.id").
order("comments.updated_at desc")
What you're basically asking for is a has_many :through association.
Try setting up your Category model something like this:
class Category < ActiveRecord::Base
has_many :posts
has_many :comments, through: :posts
end
Then you can simply do this to get the last 10 updated comments:
category.comments.order('updated_at DESC').limit(10)
You could make this more readable with a named scope on your Comment model:
class Comment < ActiveRecord::Base
scope :recently_updated, -> { order('updated_at DESC').limit(10) }
end
Giving you this query to use to get the same 10 comments:
category.comments.recently_updated
EDIT
So, a similar solution for what you actually wanted to ask for, however it requires you to approach your associations from the Comment end of things.
First of all, set up an association on Comment so that it has knowledge of its Category:
class Comment < ActiveRecord::Base
belongs_to :post
has_one :category, through: :post
end
Now you can query your comments like so:
Comment.order('updated_at desc').joins(:post).where('posts.category' => category).group(:post_id)
Somewhat long-winded, but it works.
.first is grabbing only one for you. The first one to be exact. So drop the .first. So instead do:
category.posts.joins(:comments).order('updated_at DESC')

Condition for association Rails 4

There's a way to condition something to an associative table of ActiveRecord?
I retrieve segments this way:
#segments = Segment.all
But, a Segment has_many products. See:
models/product.rb:
class Product < ActiveRecord::Base
belongs_to :segment, dependent: :destroy
end
models/segment.rb:
class Segment < ActiveRecord::Base
has_many :products
end
The problem is: I just want to retrieve products whose its status is equals to 1. I can condition something like this using where on Segment model, but how can I achieve this for products?
What I already tried
I found a solution. Take a look:
#segments = Segment.find(:all, include: :products, conditions: {products: {status: 1}})
It worked, but I think the code can be better.
Why I think the code can be better
Well, why should I use include: :products if the association is already live within the models? We're associating things through the model and I'm sure that is something near to enough.
Ideas?
Segment.joins(:products).where("products.status = 1")
You can also use includes instead of joins. But rails will convert it into a join internally since you are using the products table attribute in the query
A few tips, that might help you.
For easy naming purposes, I am considering the status==1 as being active. Of course I have no idea what it means in your specific case.
class Product
ACTIVE=1
def self.active
where(status: ACTIVE)
end
end
Now you write something like:
segment.products.active
and this will return only the active products for the given segment.
The solution you found, which will retrieve all segments with (active) products, could be written differently as follows:
Segment.includes(:products).where(products: {status: 1})
Now, why so elaborate: this actually translates to a sql query, so you have to be a little more explicit about it.
If you only ever want those with a status of 1
class Segment < ActiveRecord::Base
has_many :products, :conditions => { :status => 1 }
end
In rails 3 or
class Segment < ActiveRecord::Base
has_many :products, -> { where status: 1 }
end
In rails 4
Obviously can use status: true if it's a boolean
Then
#segments = Segment.includes(:products)
The association has_many :products makes it possible to use include: :products in your scope. Therefore you shouldn't doubt in your solution. It is right, and it is just the same as solutions presented in the other answers but by other syntacsis.
This should do the job - and it's compatibile with AREL syntax:
#segments = Segment.joins(:products).where(products: {status: 1})
It's quite different that solution with include (or includes, as it would be Rails 3/4), because it generates query with INNER JOIN, while includes generates LEFT OUTER JOIN. Also, includes is usually used for eager loading associated records, not for queries with JOIN.

rails, how to combine two ActiveRecord query results

I have following association
class Location < ActiveRecord::Base
has_many :items
end
class Item < ActiveRecord::Base
belongs_to :location
end
Suppose I have some instances of Location, I what to query all items belong to those locations. Currently I managed to get the result as an array
items =[]
Location.near(latitude,longitude,distance).find_each do |location|
location.items.find_each do |item|
items << item
end
end
However, is there any way I can get the results as ActiveRecord::Relation. Because I want to further query the results by using "where" with ActiveRecord::Relation.
P.S. The "near" method is from geocoder gem, it returns a ActiveRecord::Relation.
---------------------Edit----------------------------
Thank you for replies I nearly find the solution
locations = Location.near(latitude,longitude,distance)
Item.where(location_id: locations.pluck(:id))
Is it the right way to do it? to me it is a bit unintuitive.
----------------------Edit again ---------------------------
Just a small comment: I say it is unintuitive because I am switching from DataMapper. If it is Datamapper, it would be quite simple, like
Location.near(blabla).items
It is very simply to make queries through associations. Compared to Datamapper, can not understand why ActiveRecord association is so useless?
Edit to use one query with mapping...
What billy said above, but another option that might be faster:
locations = Location.near(1, 2, 3)
items = Item.where(:location_id => locations.map(&:ids)

Join counts across tables

I have three objects (List, ListType and GlobalList) and am trying to get a count of the items in GlobalList with a specific global_id and a specific list_type_id.
This seems to work:
select count(*) from globals_lists gl, lists l where gl.global_id=946 and gl.list_id=l.id and l.list_type_id=10;
The structure is the following:
class ListType < ActiveRecord::Base
has_many: lists
end
class List < ActiveRecord::Base
has_many :global_lists
belongs_to :list_type
end
class GlobalList < ActiveRecord::Base
belongs_to :list
end
Not sure how to do AR call - at this but can seem to put a where on the join. The list_type_id will always be 10.
a=GlobalList.where("global_id=?",119).joins(:list)
How would I do this ActiveRecord call? Most of the stuff I found via Google involved named scopes, but it seems like this should be able to be done without scopes.
First approach:
result = GlobalList
.select("COUNT(*) AS total")
.where("globals_lists.global_id=? AND lists.list_type_id=?",119, 10)
.joins(:list)
Then you will account your result using result.total
Second approach is tho use count method instead of select("COUNT(*) AS total")
GlobalList
.where("globals_lists.global_id=? AND lists.list_type_id=?",119, 10)
.joins(:list)
.count
You can find more about ActiveRecord Query Interface in Rails Guides, or directly at what you are interested in
count and joins

Resources