1

I'm refactoring my application to use 1 level deep nested resources everywhere, it's a JSON-only API. Here's a trimmed version of my routes.rb:

resources :measurements, only: [:index, :show] do
  resource :tag_node, controller: :physical_nodes, only: [:show]
  resource :anchor_node, controller: :physical_nodes, only: [:show]
  resource :experiment, only: [:show]
end

resources :physical_nodes, only: [:index, :show] do
  resources :tag_nodes, controller: :measurements, only: [:index]
  resources :anchor_nodes, controller: :measurements, only: [:index]
end

resources :experiments, only: [:index, :show] do
  resources :measurements, only: [:index]
end

And my trimmed down models:

class Measurement < ActiveRecord::Base
  self.table_name = 'measurement'
  self.primary_key = 'id'

  belongs_to :physical_node, foreign_key: :tag_node_id
  belongs_to :physical_node, foreign_key: :anchor_node_id
  belongs_to :experiment, foreign_key: :experiment_id
end

class PhysicalNode < ActiveRecord::Base
  self.table_name = 'physical_node'
  self.primary_key = 'id'

  has_many :measurements, foreign_key: :tag_node_id
  has_many :measurements, foreign_key: :anchor_node_id
end

class Experiment < ActiveRecord::Base
  self.table_name = 'experiment'
  self.primary_key = 'id'

  has_many :measurements, foreign_key: :experiment_id
end

1.:

What works:

  • GET /experiments/4/measurements.json works fine

What doesn't work: (everything else ;) )

  • GET /measurements/2/experiment.json

Error message:

Processing by ExperimentsController#show as HTML
Parameters: {"measurement_id"=>"2"}

ActiveRecord::RecordNotFound (Couldn't find Experiment without an ID)

This should be easy to fix. More important is:

2.:

GET "/measurements/2/tag_node"

Processing by PhysicalNodesController#show as HTML
Parameters: {"measurement_id"=>"2"}

How can I get rails to call it tag_node_id instead of measurement_id?

Solution:

After a long chat with 'dmoss18', it became clear that it makes no sense to put the tag_nodes and anchor_nodes as child elements of physical_nodes, as they only exist in the measurements table.

So now my routes.rb looks like this:

resources :measurements, only: [:index, :show, :create]

resources :physical_nodes, only: [:index, :show]

resources :tag_nodes, only: [] do
  resources :measurements, only: [:index]
end

resources :anchor_nodes, only: [] do
  resources :measurements, only: [:index]
end

resources :experiments, only: [:index, :show] do
  resources :measurements, only: [:index]
end

I've also removed all those only childs, because this is not the way the database was designed.

Benjamin M
  • 23,599
  • 32
  • 121
  • 201
  • #2: Your parent resource is :measurements. Rails automatically assumes the id parameter is called [parent]_id, or measurement_id in this case. This is also, I believe, why #1 is failing. Your controller action (ExperiementsController#show) is probably looking for params[:id], when the router is passing in params[:measurement_id]. You can read more http://guides.rubyonrails.org/routing.html#nested-resources – dmoss18 Mar 01 '13 at 19:45
  • Problem #1 is a bit different. If I call `GET /measurements/2/experiment` and would then call the parameter `id` (instead of `measurement_id`) when going to `ExperiementsController#show`, I would get the experiment with id=2. But what I want is of course: "give me the experiment where measurement_id=2". Seems like I have to build a: `if(params[:measurement_id]) then @experiement = Experiment.where("measurement_id = ?", params[:measurement_id])`. Or is there a simpler solution? – Benjamin M Mar 01 '13 at 19:50
  • correct. The assumption is that you aren't using the experiment_id to find experiments, but rather are using the measurement_id. I will post an answer – dmoss18 Mar 01 '13 at 19:54
  • Oh sorry. You're right. I would have to `JOIN` measurements to find the correct experiment, 'cause the experiment_id is stored in the measurements table. That makes things a little bit more difficult, but could still be solved using an `if` block. But I'm looking for a more simple/elegant/DRY solution if possible ;) – Benjamin M Mar 01 '13 at 19:56

1 Answers1

1

1: Your ExperimentsController#show action is likely looking for params[:id] to find the experiment when rails is passing in the measurement id. You will need to do something like the following:

@experiment = Experiment.find(params[:measurement_id])

However, this will not work since your experiment table doesn't have a measurement_id column. I wouldn't suggest nesting experiments as a resource of measurements. That is not how your database is laid out. If you still want to nest it though, here is what you need to do:

@experiment = Measurement.find(measurement.experiment_id).experiment

Your "has_many" and "belongs_to" don't need the "foreign_key" attribute on them. Rails will take care of this itself.

2: Since your parent resource of this route is Measurement, rails will assume the id parameter is called :measurement_id. Update your associations like this:

class Measurement < ActiveRecord::Base
  self.table_name = 'measurement'
  self.primary_key = 'id'

  belongs_to :tag_node, :class_name => 'PhysicalNode', :foreign_key => :tag_node_id
  belongs_to :anchor_node, :class_name => 'PhysicalNode', :foreign_key => :anchor_node_id
  belongs_to :experiment
end

class PhysicalNode < ActiveRecord::Base
  self.table_name = 'physical_node'
  self.primary_key = 'id'

  has_many :tag_node, :class_name => 'Measurement', :foreign_key => :tag_node_id
  has_many :anchor_node, :class_name => 'Measurement', :foreign_key => :anchor_node_id
end

class Experiment < ActiveRecord::Base
  self.table_name = 'experiment'
  self.primary_key = 'id'

  has_many :measurements
end

I would not nest anything under the measurements resource since it is a child resource, and not a parent resource. Since your /measurements/1/[anythingelse] is a measurements route, rails assumes the id is called :measurement_id. You are nesting things under the measurement resource/object whose id is 1. In other words, you are saying that measurement x HAS tag_nodes and HAS anchor_nodes, which isn't really true.

If you still wanted to, you could create individual actions in your controller for each resource, like this:

resources :measurements, only: [:index, :show] do
  resource :tag_node, controller: :physical_nodes, only: [:show_tag]
  resource :anchor_node, controller: :physical_nodes, only: [:show_anchor]
  resource :experiment, only: [:show]
end

Create a show_tag and show_anchor action in your physical_nodes controller. These actions would then look for params[:measurement_id)

dmoss18
  • 867
  • 1
  • 12
  • 25
  • Here is a sample project using nested resources. Note that the nested resource controller (comments) assumes that the parameter will always be called :article_id, since a comment is a resource of article. This approach assumes that the user will never hit a comment route directly (e.g. /comments/1), in which case the param would be just :id https://github.com/railscasts/139-nested-resources/blob/master/blog/app/controllers/comments_controller.rb – dmoss18 Mar 01 '13 at 20:03
  • #1 could be done simpler I think: `@e = Measurement.where(params[:measurement_id]).experiment`. It should work since it's connected using the model relations, right? – Benjamin M Mar 01 '13 at 20:04
  • Yeah I just wanted to make the approach very clear. You are right though – dmoss18 Mar 01 '13 at 20:06
  • The problem with #2 is: `Measurement` has two different connections to `PhysicalNode`, one by `tag_node_id` and one by `anchor_node_id`. I need a way to distinguish between them... – Benjamin M Mar 01 '13 at 20:06
  • Your "belongs_to" and "has_many" need updating. belongs_to :tag_node, :class_name => 'PhysicalNode', :foreign_key => :tag_node_id belongs_to :anchor_node, :class_name => 'PhysicalNode', :foreign_key => :anchor_node_id, and change has_many :measurements to :tag_node and :anchor_node. I wouldn't nest "tag_node" and "anchor_node" in your measurements resource, since they aren't child resources of measurements. They are parent resources. – dmoss18 Mar 01 '13 at 20:17
  • I will try and report back soon :) ... btw: I hate that oldschoolish `=>` – Benjamin M Mar 01 '13 at 20:19
  • I've done that, but `rake routes` looks still the same, and calling `http://127.0.0.1:3000/measurements/2/tag_node` still tells me that it has received `Parameters: {"measurement_id"=>"2"}` :( ... and `GET "/physical_nodes/2/tag_nodes.json"` still passes `Parameters: {"physical_node_id"=>"2"}` – Benjamin M Mar 01 '13 at 20:25
  • Have a look here (http://robots.thoughtbot.com/post/159809070/rails-patch-change-the-name-of-the-id-parameter-in), this looks like the solution, but setting `key: :tag_node_id` simply results in: `Parameters: {"key"=>:tag_node_id, "physical_node_id"=>"2"}` – Benjamin M Mar 01 '13 at 20:33
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/25385/discussion-between-dmoss18-and-benjamin-m) – dmoss18 Mar 01 '13 at 20:44
  • Shit. I stumbled over another little question. And I don't know how to contact you. So here it is: As we said `/tag_nodes/2/measurements` does make sense. But what about `/tag_nodes`? This would execute something like `Measurement.select("DISTINCT tag_node_id").tag_nodes`. Or is this again against the database structure? – Benjamin M Mar 01 '13 at 22:13