Brandan
Brandan

Reputation: 14983

Fetching many models from a single controller in a RESTful design

I'm trying to DRY up some code, and I'd like to hear some opinions.

Background

We have a set of about 15-20 classes that are nearly identical at the model level but represent different data. They are therefore stored in separate database tables and separate app/models/*.rb files and share behavior by including modules. That works fine.

We need read-only access to these classes through our REST API. We're using MetaSearch to pass search parameters to the model layer, which also works fine.

Problem

I don't want to write a new controller and view (and because of the way the API was designed, helper) for each of these models. They will all be practically identical, and that's 50+ redundant files.

How can I avoid doing that?

My first thought:

It's trivial to determine the model class from the URL parameters, and the view is essentially just a call to model.as_json. I like this solution, but I feel like I might be violating RESTful design by using one controller to manage many models (but keep in mind that the only action is index).

Would it be better to:

Thanks for any suggestions.


Update: I think this question might just be about whether REST trumps DRY or vice versa. A RESTful design would result in a lot of empty or repetitive controllers, which violates DRY. A DRY design would result in a many-to-one mapping of models to controllers, which violates REST. So it might just come down to personal preference, but I'd still like to hear what others think.

Upvotes: 3

Views: 2178

Answers (3)

Tinco
Tinco

Reputation: 607

For your API it's most important that it's RESTful, but for your implementation it's most important that it's DRY. So your are absolutely right for looking for a way to DRY this up.

I think a good way is to make a generic GenericAPIController in your controllers directory. You can define a route that routes all api requests to this controller.

The easiest way to handle exceptions is to make a controller for each model that diverges from the generic one that inherits from your api controller, and then just add a route to that controller above your generic route.

I have thought about the use of metaprogramming or other hacking to make this work dynamically without adding entries to your routes like the other answers, but it doesn't sound like it's worth it to me. If you play it right this will cause a maximum of 2 extra routes in your table. One for the generic api controller and one that regexes a list of exception controllers.

I made a small example as an exercise:

class GenericAPIController < ApplicationController
    def model
       params[:model].classify.constantize
    end

    def show
        model.find(params[:id]).to_json
    end
end

Upvotes: 1

Holger Just
Holger Just

Reputation: 55888

You might want to have a look at inherited_resources. It can be used to dry up your controllers and for simple case (like yours apparently) can reduce your manual controller to just a couple of lines.

If your controllers are are still too similar then, you could also apply some meta-programming and create the controller classes on the fly in some initializer like this

%w[Foo Bar Baz].each do |name|
  klazz = Class.new(ApplicationController) do
    respond_to :html, :json

    def index
      @model = name.constantize.find(params[:id])
      respond_with @model
    end
  end
  Kernel.const_set("#{name}Controller", klazz)
end

This code will create three minimal controllers called FooController, BarController, and BazController.

If you are just calling model.to_json in your views, you don't need views at all. Just use respond_to and respond_with (inherited_resources and my example code above do that). See one of the many articles about its usage for more information.


Edit: The meta-programming approach would help you avoid the copy&paste of many identical controllers. You still should put as much code as you can in a common parent class (or some included modules). In the end, maintaining a couple of almost empty classes isn't that bas as you don't copy complex code.

The example above could also expressed with less meta-programming but exactly the same behavior like the following example. This approach is probably a bit more natural. It still gives you almost all the advantages of the full-meta approach.

class MetaController < ApplicationController
  respond_to :html, :json

  def index(&block)
    @model = model.find(params[:id])
    instance_eval(&block) if block_given? # allow extensions
    respond_with @model
  end

protected
  def model
    @model_class ||= self.class.name.sub(/Controller$/, '').constantize
  end
end

class FooController < MetaController
end

class BarController < MetaController
  def index
    super do
      @bar = Specialties.find_all_the_things
    end
  end
end

class BazController < MetaController
end

As another point of thought, I included a simple extension mechanism. In child classes, you can pass a block to the super call to perform additional steps which might be required by a slightly special view.

Upvotes: 3

Christopher WJ Rueber
Christopher WJ Rueber

Reputation: 2161

Inheritance. You could get tricky by doing some looping through to create your classes as others have presented, but that is not very explicit. That just makes it harder to maintain and understand (particularly if others have to come along and maintain your app). This may take a bit more time and code to implement, but I would still go with something like this...

class UberController < ApplicationController
  def index
    render :text => self.class.name
  end
end

class SubController < UberController; end
class UnderController < UberController; end

Same basic concept for your models, too. You can always interrogate the actual class from the super class to make sure of where you are, or you can implement specific details in the subclasses. At least this way it's closer to a 1-to-1 implementation for RESTs-sake, and more comprehensible for being explicit.

Upvotes: 1

Related Questions