Handle many-to-many relationship in rails - ruby-on-rails

I have a many-to-many relationship in a db I use with rails.
Say I have a schema that looks like this :
Cinemas
- cinema_id
- name
Movies
- movie_id
- name
Showtimes
- showtime_id
- timestamp
- movie_id
- cinema_id
So basicaly : [Cinemas] 1 -- N [Showtimes] N -- 1 [Movies]
In one of my pages I have a cinema id and want to show the movies based on the cinema_id and group the showtimes per movie. I am not exactly sure on how to handle this properly. In .Net I would use a SelectMany linq operator and create a collection of movies but this does not seem to exists in ruby/rails.
One way to do this I think would be to do something like:
#showtimes = Showtime.where(:cinema_id, cinema_id)
#showtimes.each do |st|
#movies.add(st.Movie) unless #movies.include? st.Movie
end
That would work ... but it seems ... ugly or it is just me ?

Based on your schema, each showtime record will have exactly one movie and one cinema. You can easily get separate lists this way:
#showtimes = Showtime.where(:cinema_id, cinema_id).order(:showing_at)
#movies = #showtimes.map {|s| s.movie }
You might also want to look at the .group and .includes methods in ActiveRecord.
If you want to go straight to a list of movies showing at a certain cinema, you could also do this:
#movies = Movie.joins(:showtimes).where('showtimes.cinema_id = ?', cinema_id)
With that, you can loop over the movies and show each one's showtimes.

If you have your associations setup correctly in your model, you should be able to do something like this:
Controller:
#showtimes = Showtime.where(:cinema_id, cinema_id)
View:
<%= #showtimes.each do |showtime| %>
<p><%= showtime.movie.name %></p>
<% end %>

Related

Print what is not in a join table

I have a join table created from 2 other tables.
Say one model is called cat, another is called request, and the join table is called catrequest (this table would have cat_id and request_id)
How would I print all of the cats not intersected in the join table i.e. all of the cats NOT requested using rails. I saw some DB based answers, but I am looking for a rails solution using ruby code.
I get how to print a cat that belongs to a request i.e.:
<% #requests.each do |request| %>
<% request.cats.each do |cat| %>
<%= cat.name %>
<% end %>
but I don't understand how to do the reverse of this.
To get a list of cats that have never been requested you'd go with:
Cat.includes(:cat_requests).where(cat_requests: { id: nil })
# or, if `cat_requests` table does not have primary key (id):
Cat.includes(:cat_requests).where(cat_requests: { cat_id: nil })
The above assumes you have the corresponding association:
class Cat
has_many :cat_requests
end
It sounds like what you need is an outer join, and then to thin out the cats rows that don't have corresponding data for the requests? If that's the case, you might consider using Arel. It supports an outer join and can probably be used to get what you're looking for. Here is a link to a guide that has a lot of helpful information on Arel:
http://jpospisil.com/2014/06/16/the-definitive-guide-to-arel-the-sql-manager-for-ruby.html
Search the page for "The More the Merrier" section which is where joins are discussed.

Querying or iterating join tables in ActiveRecord/Rails

I have two tables:
Venues
has_many :venue_intel_maps
VenueIntelMap
belongs_to :venue
In the venue_intel_map table there's a column called :tag_label that I want to grab for each particular venue that has a venue_intel_map
In my controller below this returns an array of each venue where then each venue has a venue_id
def show
#venues = Venue.where(parent_venue_id: current_admin.parent_venue_id)
end
So in my views, I can do this for particular venue.
- #venues.each do |venue|
=venue.name
=venue.address
=venue.location
But because I want to get to venue_intel_maps table so I can call a column called :tag_label I wanted to avoided a nested each statement that might look something like this
- #venues.each do |venues|
- venues.venue_intel_maps.each do |intel_maps|
= intel_maps.tag_label
Is there anyway I can clean this up or any suggestions? I was trying to play around with .joins or .selects in active record but didn't know how to successfuly do that so I can somehow put the logic in my Venues model.
EDIT
Would it also be better somehow to define in the controller my venue doing this? In the views I'm going to be pulling things from my venue_intel_maps table so would this serve any advantage?
#venues = Venue.joins(:venue_intel_maps).where(parent_venue_id: current_admin.parent_venue_id)
depending on your Rails version, try:
#venues.venue_intel_maps.pluck(:tag_label)
#venues.venue_intel_maps.map(&:tag_label)
see: http://rubyinrails.com/2014/06/rails-pluck-vs-select-map-collect/
You could preload (Rails 3+) the venue_intel_maps in the controller
def show
#venues = Venue.where(
parent_venue_id: current_admin.parent_venue_id
).preload(:venue_intel_maps)
end
Then in the view, use the iteration you suggested
- #venues.each do |venue|
= venue.name
= venue.address
= venue.location
= venue.venue_intel_maps.each do |intel_maps|
= intel_maps.tag_label

How to sort through an associated attribute on two levels at once?

This is a simple ruby question I believe. In my app, I have Product model that has_many Reviews. Each Review has an attribute of an "overall" rating which is an integer.
What I want to do is display the top ten Products based on the average of their overall ratings. I've already gotten this to work, BUT, I also want to sort Products that have the SAME overall rating by a secondary aggregate attribute, which would be how MANY reviews that Product has. Right now, if I have 3 products with the same average overall rating, they seem to be displayed in random order.
So far my code is:
Controller
#best = Product.has_reviews.get_best_products(10)
Product Model
scope :has_reviews, joins{reviews.outer}.where{reviews.id != nil}
def self.get_best_products(number)
sorted = self.uniq
sorted = sorted.sort { |x, y| y.reviews.average("overall").to_f <=> x.reviews.average("overall").to_f }
sorted.first(number)
end
I've tried this for my model code:
def self.get_best_products(number)
sorted = self.uniq.sort! { |x, y| x.reviews.count.to_f <=> y.reviews.count.to_f }
sorted = sorted.sort { |x, y| y.reviews.average("overall").to_f <=> x.reviews.average("overall").to_f }
sorted.first(number)
end
...but it does not do what I want it to do. I am just iterating through the #best array using each in my view.
---UPDATE
OK now I am trying this:
Controller:
#best = Product.get_best_products(6)
Model:
def self.get_best_products(number)
self.joins{reviews}.order{'AVG(reviews.overall), COUNT(reviews)'}.limit(number)
end
But I am getting this error:
PGError: ERROR: column "products.id" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: SELECT "products".* FROM "products" INNER JOIN "reviews" ...
I am using the Squeel gem btw to avoid having direct SQL code in the model.
----UPDATE 2
Now I added the 'group' part to my method but I am still getting an error:
def self.get_best_products(number)
self.joins{reviews}.group('product.id').order{'AVG(reviews.overall), COUNT(reviews)'}.limit(number)
end
I get this error:
PGError: ERROR: missing FROM-clause entry for table "product"
LINE 1: ...eviews"."product_id" = "products"."id" GROUP BY product.i...
product.rb
scope :best_products, (lambda do |number|
joins(:reviews).order('AVG(reviews.overall), COUNT(reviews)').limit(number)
)
products_controller.rb
Product.best_products(10)
This makes sure everything happens in the database, so you won't get records you don't need.
If I got it right here is my idea of how I would do it:
As products has many reviews and reviews has an overall attribute I would add a reviews_counter column to the products table that will increment with each added review, this way you'll be able to gain a little more db performance as you don't have to count all the products reviews to get the most reviewed one.
Now you'll get the products ordered by reviews_counter:
#best_products = Products.order("reviews_counter desc")
and next you'll get the reviews for each product ordered by overall:
<% for prod in #best_products %>
<%= prod.reviews.order("overall desc") %> # can do all this or more in helper
<% end %>
also ordering this way, if you have 3 reviews with the same overall you can one more order() statement and sort it by name or id or whatever you like so they don't display in random order.
This is just my idea of how I would do it, I worked recently on an app that required something similar and we just added a counter_field to our model, it's not illegal to do so :)
p.s. it's not very clear for me how many records you would want to display for each so you'll just need to add .limit(5) for exemple to get only the first 5 reviews of a product.

Rails ancestry: Get children records of ancestry model

I've two models:
Post
- category_id
Category (ancestry model)
The category tree looks for example like this:
- Root Category
- Sub Category
Now lets say a post gets categorized to Sub Category. I'm in the Root Category and i would like to see all posts which are in Sub Category as well. Is it possible to get these as well with ancestry?
The category tree always has just one nested level so maybe ancestry is anyways too much..
Thanks in advance
Working example for just one nested level
#category = Category.find(params[:id])
category_ids = #category.children.map(&:id) << #category.id
category_ids = category_ids.join(",")
#posts = Post.recent.where("category_id IN (#{category_ids})").page(params[:page])
I suggest that the following would do the trick:
Post.where(category_id: #category.subtree_ids)
And would also probably be the most efficient way without doing something really horrible.
Something like
Post.where('subcategory_id in ('+#category.subcategories.select(&:id).join(',')+')')
You then request the posts, which have their category-id in the list generated by #category.sucategories, from which you select the id and merge them into a string like '1,5,77'.
Something very very bad to do is (just for fun):
#mylist = Array.new
Post.all.each do |item|
#mylist << item if item.subcategory.category = #selectedrootcategory
Update:
Just thought of some other solution:
#allposts = Array.new
#selectedrootcategory.includes(:subcategory => :posts).subcategories.each do |subcat|
#allposts << subcat.posts
All postst are retrieved in one query (thanks to includes), then you iterate over the subcategories and posts.

ActiveRecord and SELECT AS SQL statements

I am developing in Rails an app where I would like to rank a list of users based on their current points. The table looks like this: user_id:string, points:integer.
Since I can't figure out how to do this "The Rails Way", I've written the following SQL code:
self.find_by_sql ['SELECT t1.user_id, t1.points, COUNT(t2.points) as user_rank FROM registrations as t1, registrations as t2 WHERE t1.points <= t2.points OR (t1.points = t2.points AND t1.user_id = t2.user_id) GROUP BY t1.user_id, t1.points ORDER BY t1.points DESC, t1.user_id DESC']
The thing is this: the only way to access the aliased column "user_rank" is by doing ranking[0].user_rank, which brinks me lots of headaches if I wanted to easily display the resulting table.
Is there a better option?
how about:
#ranked_users = User.all :order => 'users.points'
then in your view you can say
<% #ranked_users.each_with_index do |user, index| %>
<%= "User ##{index}, #{user.name} with #{user.points} points %>
<% end %>
if for some reason you need to keep that numeric index in the database, you'll need to add an after_save callback to update the full list of users whenever the # of points anyone has changes. You might look into using the acts_as_list plugin to help out with that, or that might be total overkill.
Try adding user_rank to your model.
class User < ActiveRecord::Base
def rank
#determine rank based on self.points (switch statement returning a rank name?)
end
end
Then you can access it with #user.rank.
What if you did:
SELECT t1.user_id, COUNT(t1.points)
FROM registrations t1
GROUP BY t1.user_id
ORDER BY COUNT(t1.points) DESC
If you want to get all rails-y, then do
cool_users = self.find_by_sql ['(sql above)']
cool_users.each do |cool_user|
puts "#{cool_user[0]} scores #{cool_user[1]}"
end

Resources