4

If there's one thing I've learned about Rails 3 is if I'm having a hard time doing something, I'm probably doing it wrong. So I'm looking for help.

I have a few models which are related in a many to many relationship.

I am able to create the associations in the models without a problem. My problem lies in how to build the controllers to work with these relationships. I'll try and give an example if you don't see where I'm going with this.

For instance...

class Account < ActiveRecord::Base
    has_many :locations
end

class Contact < ActiveRecord::Base
    has_many :locations
end

class Location < ActiveRecord::Base
    has_and_belongs_to_many :accounts
    has_and_belongs_to_many :contacts
end

Let's say I have the above models. This would be my resources...

resources :accounts do
    resources :locations
end

resources :contacts do
    resources :locations
end

resources :locations do
    resources :accounts
    resources :contacts
end

So just to keep this shortened a bit, let's say I want a list of all locations for an account. The above routes would presumably be account/1/locations. Thus landing me at locations#index.

Hopefully I haven't screwed up my example at this point but what's the best way to build this action out as it really has multiple jobs... at a minimum the locations for an account, contact, and all locations.

So I end up with something like this...

class LocationController < ApplicationController
    def index
        if params[:account_id]
            @locations = Location.find_all_by_account_id(params[:account_id])
        elsif params[:contact_id]
            @locations = Location.find_all_by_contact_id(params[:account_id])
        else
            @locations = Location.all
        end

        respond_with @locations
    end
end

Update #1: To clarify, as I am getting some answers that suggest I change my Model relationships. I am working with a legacy system in which I can NOT change the relationships at this point. It is ultimately my goal to clean up the database and the relationships but for now I can not. So I need to find a solution that works with this configuration.

Altonymous
  • 783
  • 7
  • 25

3 Answers3

6

Your current approach is not DRY, and would give you a headache if say, for example, you wanted to impose additional scopes on the index; e.g. pagination, ordering, or searching by a field.

Consider an alternative: Note how your if/elsif/else conditional essentially is just finding the lookup scope to send find to? Why not move that responsibility to a method that does just that? Thus simplifying your actions and removing redundant code.

def index
  respond_with collection
end

def show
  respond_with resource
end

protected

# the collection, note you could apply other scopes here easily and in one place,
# like pagination, search, order, and so on.
def collection
  @locations ||= association.all
  #@locations ||= association.where(:foo => 'bar').paginate(:page => params[:page])
end

# note that show/edit/update would use the same association to find the resource
# rather than the collection
def resource
  @location ||= association.find(params[:id])
end

# if a parent exists grab it's locations association, else simply Location
def association
  parent ? parent.locations : Location
end

# Find and cache the parent based on the id in params. (This could stand a refactor)
#
# Note the use of find versue find_by_id.  This is to ensure a record_not_found
# exception in the case of a bogus id passed, which you would handle by rescuing
# with 404, or whatever.
def parent
  @parent ||= begin
    if id = params[:account_id]
      Account.find(id)
    elsif id = params[:contact_id]
      Contact.find(id)
    end
  end
end

inherited_resources is a great gem for cleanly handling scenarios like this. Written by Jose Valim (of Rails). I believe it should work with HABTM, but honestly I'm not positive if I've ever tried it.

The above exmaple is essentially how inherited_resources works, but mostly it works its magic behind the scenes, and you only overwrite methods if you need to. If it works with HABTM (I think it should), you could write your current controller something like this:

class LocationController < InheritedResources::Base
  belongs_to :contact, :account, :polymorphic => true, :optional => true
end
numbers1311407
  • 33,686
  • 9
  • 90
  • 92
  • I really like where you're going with this. I'm going to see if I can extrapolate this out to my more complex model. I didn't use it as an example because I wanted to get my problem across without overwhelming people. I'll be sure to report back with my results. Thanks! – Altonymous Sep 09 '11 at 01:01
4

You shouldn't be providing multiple ways of reaching the same resource. There isn't supposed to be a 1-to-1 mapping of resources to associations.

Your routes file should look like this:

resources :accounts
resources :contacts
resources :locations

The whole point of REST is that each resource has a unique address. If you really want to expose only the accounts/contacts from within a given location, then do so:

resources :locations do
    resources :accounts
    resources :contacts
end

But you absolutely should not be providing both nested accounts/locations and location/accounts routes.

user229044
  • 232,980
  • 40
  • 330
  • 338
  • I don't disagree with you. But you didn't address my question. my question is in regards to how the controller should handle your above scenario. I am working with a legacy system, and these relationships do exist for now, until I can clean them up. So in your example I am still left with trying to figure out how to handle when locations#index receives an account_id vs a contact_id – Altonymous Sep 08 '11 at 03:15
  • Then you're done. If you can't fix the app, you can't fix it. Your controller is as "correct" as the underlying routes allow it to be. – user229044 Sep 08 '11 at 03:34
0

The way I see it, is that since accounts and contacts seems to have a similar behaviour, it would make sense to use Single Table Inheritance (STL) and have another resource, for instance User.

That way you can do this...

class User < ActiveRecord::Base
    has_many :locations
end
class Account < User
end

class Contact < User
end

class Location < ActiveRecord::Base
    has_and_belongs_to_many :user
end

The resources stay the same...

resources :accounts do
    resources :locations
end

resources :contacts do
    resources :locations
end

resources :locations do
    resources :accounts
    resources :contacts
end

Then, you have the same way to access locations, independent of the type of job.

class LocationController < ApplicationController
    def index
        if params[:user_id]
            @locations = Location.find_all_by_account_id(params[:user_id])
        else
            @locations = Location.find_all_by_id(params[:account_id])
        end

        respond_with @locations
    end
end

That way your code becomes reusable, scalable, maintainable and all the other good stuff that we have been told is great.

Hope it helps!

JavierIEH
  • 743
  • 8
  • 19
  • BTW, i absoulutely agree with meagar. If you want to keep your app RESTful, you shouldnt have multiple routes to achieve the same thing. – JavierIEH Sep 08 '11 at 02:42
  • I too agree with meager but I can't change the way it is right now, so I still need to find a way to work within the system. As for you answer I can say that they serve two entirely different behaviors. The ONLY thing they have in common is the locations. It get's more complicated when I start talking about how accounts can have child accounts and they have a billing location, primary location, satellite location all in many-to-many relationships. – Altonymous Sep 08 '11 at 03:17
  • Considering that you can't modify the model, i would say that is the only way to handle nested resources. The only recomendation i can give you is that never have resources nested more than 2 levels deep. Otherwise both your url and your controllers will look nasty – JavierIEH Sep 08 '11 at 03:35