1

I have a movie app that's comprised of the following models: Actor, Movies, Genre. I've created a many-to-many association where actors have and belong to many movies, vice versa.

class Actor < ActiveRecord::Base
    has_many :movies, through: :actor_movies
    has_many :actor_movies
end

class ActorMovie < ActiveRecord::Base
    belongs_to :actor
    belongs_to :movie
end

class Movie < ActiveRecord::Base
    belongs_to :genre

    has_many :actors, through: :actor_movies
    has_many :actor_movies
end

I also made it so that each movie has their own genre:

class Genre < ActiveRecord::Base
    has_many :movies
end

On the actor's show page I'd like to display the 3 most common genres of movie's they've starred in. This would obviously be based off their movie genres. What I'm trying to figure out is how to put these movies (with their associated genres) in some sort've array and performing an action to produce the prior mentioned result.

Controller

def show
  @actor = Actor.find(params[:id]
  @movies = @actor.movies
end

So for instance I have an actor that's been in movies with genres ranging from Action, Comedy, Drama, Thriller, Sci-Fi, etc. While doing some research I found the Rubular way to create a hash from arrays using inject from the second answer of this post.

Would I do something similar in this rails project?

Community
  • 1
  • 1
Carl Edwards
  • 13,826
  • 11
  • 57
  • 119
  • Also, check this post. It can be useful: http://stackoverflow.com/questions/8696005/rails-3-activerecord-order-by-count-on-association – Laerte Mar 19 '15 at 15:07
  • Just a note that v2.2 gave [Enumerable#max_by](http://ruby-doc.org/core-2.2.1/Enumerable.html#method-i-max_by) an optional argument `n` which, if present, causes `max_by` to return an array of the "top" `n` elements. (Similar for `min_by`.) – Cary Swoveland Mar 19 '15 at 15:12

2 Answers2

2

How about something like this:

class Actor < ActiveRecord::Base
  # rest of your code

  has_many :genres, through: :movies

  def common_genres(limit=3)
    genres
      .group("genres.id")
      .order("count(genres.id) DESC")
      .limit(limit)
  end
end

You can use it like:

actor = Actor.first

actor.common_genres.each do |genre|
  p genre.name
end

With method as provided, you can fetch as many Genres as you need with:

actor.common_genres(5)

Hope that helps!

Paweł Dawczak
  • 9,519
  • 2
  • 24
  • 37
  • Very clean and minimal solution. Thanks! – Carl Edwards Mar 19 '15 at 16:45
  • @CarlEdwards - You are absolutely right! Think of it as one of *Ruby on Rails* features - if you're implementing your solution, and it feels like it takes *to much* code - probably you are right, and you can accomplish your task *easier*, with *cleaner* solution, and *less* code. Dive into [Rails Guides](http://guides.rubyonrails.org/), so you can learn about lot's of shortcuts! Good luck! – Paweł Dawczak Mar 19 '15 at 16:49
  • Thanks for advice and gave a +1 as a token of gratitude. – Carl Edwards Mar 19 '15 at 16:53
  • @CarlEdwards - If I can point one more thing - is there any particular reason you have `has_many :actor_movies` and `has_many :movies, through: :actor_movies` in `Actor`? Because there is shorter way of defining `many-to-many` relations - check [this](http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association)! – Paweł Dawczak Mar 19 '15 at 16:55
  • I originally thought that was the proper way to setup a many-to-many relationship. What way were you thinking? – Carl Edwards Mar 19 '15 at 17:05
  • My apologies - forgotten to give you an example! So, you can update your `Actor` to replace both `has_many` occurrences with simple `has_and_belongs_to_many :movies`, and the same way in `Movie` - `has_and_belongs_to_many :actors`. Check the details [here](http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association). – Paweł Dawczak Mar 19 '15 at 17:11
0

Something like this:

@top_movies = @actor.movies.group(:genre).count

That should output something like:

{ 'thriller' => 7, 'comedy' => 12 }

To sort that hash, you can just do:

@top_movies = @actor.movies.group(:genre).count.sort_by { |_, v| -v }.first(3)

Edit:

To answer your follow up question, you'll just loop through that array in your view like so:

<% @top_movies.each do |movie| %>
  <%= movie[0] %>
<% end %>
Anthony
  • 15,435
  • 4
  • 39
  • 69
  • Thanks. How would this translate to my view? I basically just want to show those top 3 genre's names. Sorry for not mentioning the names part. – Carl Edwards Mar 19 '15 at 15:09
  • I also got this error when I applied that method to my controller: `undefined method 'includes' for #` – Carl Edwards Mar 19 '15 at 15:15
  • That solved the issue within the controller but got another in the view regarding the genre's name attribute `undefined method 'name' for nil:NilClass`. There is in fact a string based name attribute assigned to the Genre model so thats not the issue. – Carl Edwards Mar 19 '15 at 15:21
  • sorry @CarlEdwards - you don't need the object here, just the name right? `movie[0]` will output that. Try now – Anthony Mar 19 '15 at 15:23
  • Unfortunately that only displays one genre and shows it as so: `#` – Carl Edwards Mar 19 '15 at 15:29
  • It's hard for me to debug your exact scenario but it sounds like you do need to use `movie[0].name` but it sounds like you have some movies that don't have genres. – Anthony Mar 19 '15 at 15:45