删除Rails嵌套资源记录时遭遇NoMethodError问题求助
Let's break down what's going wrong here — the core issue is a mismatch between your view's delete link parameters and your nested route structure, which is causing your controller to receive incorrect params, leading to that nil:NilClass error. Here's the step-by-step fix:
1. Recap Your Nested Route Structure
First, let's confirm how your nested routes work:
resources :vendors do resources :services end
This generates a destroy route for services that follows this pattern:DELETE /vendors/:vendor_id/services/:id
To hit this route correctly, the vendor_service_path helper requires two arguments: the parent vendor and the child service.
2. The Bug in Your View
Looking at your delete link in vendor/show.html.erb:
<%= link_to "Delete Service", vendor_service_path(current_vendor), method: :delete, ... %>
You're only passing current_vendor to the path helper — you're missing the service object! This means Rails generates an invalid URL (it’ll look like /vendors/123/services instead of /vendors/123/services/456). When you click this link:
params[:vendor_id]gets set to your vendor's ID (good)params[:id]is nil (bad, because this should be the service's ID)
So in your originaldestroymethod,@service = Service.find_by_id(params[:id])returnsnil, hence theundefined method 'user' for nil:NilClasserror.
3. Why Your "Fix" Broke Tests
When you swapped the params in your controller:
@vendor = Vendor.find_by_id(params[:id]) @service = Service.find_by_id(params[:vendor_id])
You’re essentially reversing what the params are supposed to represent. This "works" only because your broken link is sending the vendor's ID in params[:vendor_id] — but this is completely wrong for your route structure. Your RSpec tests are written to use the correct params (sending vendor ID in vendor_id and service ID in id), so when you swap them, the controller looks for a vendor with the service's ID and vice versa, which fails.
The Correct Fix
- Fix your delete link in the view to pass both the vendor and service:
<%= link_to "Delete Service", vendor_service_path(current_vendor, service), method: :delete, data: {confirm: 'Are you sure? This action cannot be reversed.'}, class: 'btn btn-danger float-right'%>
- Revert your controller's
destroymethod back to the original version — it was logically correct, the problem was just the view passing wrong params:
def destroy @vendor = Vendor.find_by_id(params[:vendor_id]) @service = Service.find_by_id(params[:id]) if @service.user == current_user @service.destroy redirect_to vendor_path(@vendor), notice: "The service has been deleted." else return render text: 'Not Allowed', status: :forbidden end end
- (Optional) To make your controller more robust, you could use
findinstead offind_by_id(which raises an error if the record isn't found) or add abefore_actionto fetch the vendor and service upfront.
Bonus: Why Your Original Tests Passed
Your RSpec tests likely bypassed the view entirely — they probably called the destroy method directly with the correct params (vendor ID and service ID), so they didn't catch the broken link in the view. To prevent this in the future, add system tests or request tests that actually click the delete link to verify end-to-end behavior.
内容的提问来源于stack exchange,提问作者swilliams

