-
-
Save dhh/981520 to your computer and use it in GitHub Desktop.
# Bulk API design | |
# | |
# resources :posts | |
class PostsController < ActiveController::Base | |
# GET /posts/1,4,50,90 | |
# post_url([ @post, @post ]) | |
def show_many | |
@posts = Post.find(params[:ids]) | |
end | |
# PUT /posts/1,4,50,90 | |
def update_many | |
@posts = Post.find(params[:ids]) | |
Post.transaction do | |
@posts.each do |post| | |
post.update_attributes!(params[:posts][post.id]) | |
end | |
end | |
end | |
# POST /posts/many | |
def create_many | |
end | |
# DELETE /posts/1,4,50,90 | |
def destroy_many | |
end | |
end |
I tried to come up with names for these kind of controller actions and couldn't find any that I was happy with. I'm happy with these. Thanks!
Nice! I rolled my own solution on something very similar recently, really makes sense.
POST /posts/many ? hmm.... doesn't look good to me. IMO, if several objects are posted to /posts, then it's a create_many
.
@Gimi Rails is designed to be able to route most requests without looking at the payload. Routers are expected to be fast, so if a request is slow the developer will at least know which action was called when the slowness was encountered.
@benatkin I understand. So, I just think that /posts/many
may be unnecessary? No matter how many resources are going to be created, just POST them all to /post
is enough. Sometime, you even have no idea how many resources will be created, say I have a form for users to create child groups for a group, and users can add child groups dynamically, which means you may create one or ten groups in the action. create
just feels a little bit different from the other default actions to me.
I agree with @Gimi. /posts/many
doesn't look consistent to me.
I think _multiple makes more sense than _many, since you can use the API with two IDs, but two widgets is not very many widgets.
Is it just me, or does anybody else get the feeling that this could lead to some very unDRY code? It seems that show_many
and index
will typically wind up being copies of each other except that they pull from different parameters. Any any sort of logic added to a create
, update
or destroy
will have to be duplicated in it's _many
counterpart. It seems like the code will typically be identical across single or bulk actions, only their responses seem like they need to be handled differently. And even then how would a failed validation on a create or update be handled? Would it render a new_many
or edit_many
? I'm still fairly new to Rails so maybe I'm not understanding how exactly this will be implemented.
For the record: In my impl. I had something like:
GET /posts/1,2,3,4,... => index
GET /posts/1,2,3,4,.../edit => edit
PUT /posts/1,2,3,4,... => update
POST /posts => create
DELETE /posts/1,2,3,4,... => destroy
So I agree that the proposal is a bit overkill unless I've missed something.
...though I got mixed feelings on the show vs. index, as I used edit action for "edit multiple". =S
grimen's solution seems much cleaner to me. Some changes on Relation methods would also help:
Relation#all(params[:ids]) - would do same as find, but if you pass nil it would return all records. This is to avoid:
@posts = Post.scoped
@posts = @posts.where(:id => params[:ids]) if params[:ids]
Relation#update_attributes (and similar methods) - this could be used instead of:
Post.transaction do
@posts.each do |post|
post.update_attributes!(params[:posts][post.id])
end
end
@sinsiliux, the first case could be avoided by having params[:ids]
to be equivalent to params[:id].split(',')
. That's how I handled it at least, but maybe I was playing with fire. :)
@grimen what I wanted was to return all records when params[:ids] is empty and selected records when it's present.
@sinsiliux I just noticed where(:id => params[:ids])
!= where(:id => nil)
- I think this was not the case in Rails 2.3.x (old finders). Anyway, my basic point was that for me params[:ids]
should be same as params[:id]
to avoid singular vs. plural getting in the way.
@grimen I too wonder if handling the one-or-many cases using a single action makes sense. I'm experimenting with this before_filter:
def separate_bulk_ids
if params[:id].present?
params[:ids] = params[:id].split(',')
params[:id] = params[:ids].first
end
end
Is it surprising that params[:id] is always the first of many IDs? Aside from the obvious case of overwriting an explicitly passed :ids parameter, when would this cause problems? Anyhow, here's how I make use of it in a trivial #update example.
def update
# Gracefully ignores invalid IDs
@posts = Post.where(:id => params[:ids])
@posts.each do |post|
# Enforce policy if desired on each post...
post.update_attributes(params[:posts][post.id.to_s])
end
# What results are returned considering some updates may have failed?
end
I've created a little Rails app with some different examples and tests.
https://github.com/jmorton/bulk-example/blob/tolerant/app/controllers/posts_controller.rb
https://github.com/jmorton/bulk-eaxample/blob/strict/app/controllers/posts_controller.rb
If I recall correctly, Richardson's Restful Web Services recommends ,
for dimensional things (like /locations/lat,long/
) and ;
for lists of things (like /posts/1;2;3
). Allamaraju's Restful Web Services Cookbook, on the other hand, doesn't express a preference between the two.
Great idea! Does it come with bulk INSERT/UPDATE on ActiveRecord's side?