Search code examples
ruby-on-railscontrollernested-attributes

Filtering nested attributes in rails API json output


Im building an API in which i have a model called Studio. Studio has many movies and many series, and movies and series have many characters of their own. Also, a character can be in many movies and/or many series, and belongs to a studio. I got all the models relational tables logistics figured out by now, but what i'm trying to do right now, i can't think of a way.

So my routes are designed so i can do something like localhost:3000/api/v1/studios/1/characters -> this way i display all the studio's (studio_id: 1) characters (from all the movies and series that belong to that studio). That is fairly easy with just doing something like:

@characters = Character.where(studio_id:@studio.id)

In my CharactersController, where i have

def get_studio
    @studio = Studio.find(params[:studio_id])
end

Now i am trying to design the logistics so i can do localhost:3000/api/v1/studios/1/seriees/1/characters And only get the characters from that specific seriees. localhost:3000/api/v1/studios/1/movies/1/characters should also be valid.

I tried

def get_movie
    @movie = Movie.find(params[:movie_id])
end`

and

def get_serie
    @serie = Seriee.find(params[:seriee_id])
end

in my CharactersController, with before_action :get_serie and before_action :get_movie at the very start of the code, but it gives me an error because i'm never getting both a movie_id and a seriee_id, it's either one or the other, so it can't search for both at the same time.

The POST from character i will only do from localhost:3000/studios/1/characters so that's not a problem, i only want to be able to GET from localhost:3000/studios/1/movies/1/characters and localhost:3000/studios/1/seriees/1/characters

I hope i didn't ramble to much and you are able to understand me. If you think that my way of solving this problem is just not right, please tell me as i am just learning and any advice would be very valuable.


Solution

  • I doubt you actually want to nest those resources more then one level deep:

    Rule of thumb: resources should never be nested more than 1 level deep. A collection may need to be scoped by its parent, but a specific member can always be accessed directly by an id, and shouldn’t need scoping (unless the id is not unique, for some reason).
    - Jamis Buck

    Given the domain here it does not make sense that you would have to specify the studio to get the characters for a given movie. The goal of nesting in REST is to make the relationships between two entities explicit by making them part of the URL structure - not to be overly explicit.

    Excessive nesting leads to some very overcomplicated code when it comes to stuff like url generation - I mean do you really want to write?

     url_for [:ap1, :v1, studio, movie, :characters]
    

    You can fix these issues by using the shallow: true option:

    resources :studios do
      resources :movies, shallow: :true do
        resources :characters
      end
    end
    

    or by avoiding "russian doll" calls to resources:

    resources :studios do
      resources :movies, only: [:new, :create]
    end
    
    resources :movies, only: [:show, :update, :destroy] do
      resources :characters, only: [:create, :index]
    end
    

    You also really should be using assocations to fetch the nested items.

    class CharactersController
      before_action :set_movie, only: [:index, :create]
    
      # GET /api/v1/movies/1/characters.json
      def index
        @characters = @movie.characters
      end
    
    
      private
    
      def set_movie
        @movie = Movie.find(params[:movie_id])
      end
    end
    

    In general code like Character.where(studio_id: @studio.id) should be avoided as it leaks the implementation details of the relationship. Your controller should not need to know how characters and studios are related - it just knows that movie has a method named characters.

    And I don't really want to get into the details of your modeling here but Character.where(studio_id: @studio.id) will not work with the requirement that movies most likely contains a subset of the characters of a studio - not their entire cinematic universe. Instead you need a join table that links movies and characters.