Performing multiple database operations in a single REST call
I am trying to figure out the best way to call REST actions that perform multiple actions and multiple database updates from a single call.
In my data model, I have Diners and LunchBoxes, and Foods. LunchBoxes are just many-to-many relationships between Diners and Foods, but with a count attribute that says how many of that type of food the given Diner has.
I want to set up a call that indicates that the Diner has eaten one of their Foods, which increases the health of the Diner accordingly. Certain foods are more nutritious than others, and consequently increase the Diner's health by different amounts. The actions that constitute this would be:
- Reduce the count attribute on the Diner's LunchBox for the given food by the correct amount
- Increase the Diner's health accordingly
So two tables need to be updated here: Diner and Lunchbox, both within a single transaction.
Trying to use nouns, the best I could come up with was:
POST /diner/frank/meal
Where the XML describing a meal would be something like
<meal>
<food>
<id>apple</id>
</food>
<count>2</count>
</meal>
However, this strikes me as pretty contrived. POSTing a meal in REST should create a Meal resource. In this case not only are we not creating a Meal resource, but we are updating two other resources: Diner and LunchBox.
I suppose one approach is to have the client handle this in two separate calls - one to update the Diner, and one to update the LunchBox. However, this seems wrong because we have multiple clients (HTML, Flash, etc.) that all need to perform this action. If we ever update the business logic in the future that开发者_如何学JAVA is used to consume foods then we would need to make that change on many clients instead of on a single server.
How have others approached this admittedly pretty basic problem?
First off, the updating of the diner and the lunchbox should absolutely be done in one request. Don't fall into the trap of trying to do transactions over a REST api.
Before we get to your specific question, lets lay the groundwork for how the client could interact with your service leading up to your question.
The client should always start at the root service url.
GET /DiningService
Content-Type: application/vnd.sample.diningservice+xml
200 OK
<DiningService>
<Link rel="diners" href="./diners"/>
<Link rel="lunchboxes" href="./lunchboxes"/>
<Link rel="foods" href="./foods"/>
</DiningService>
I don't know the way your users will interact with the client software, but lets assume that we first need to identify who is going to do the eating. We can retreive a list of diners from looking in the response for a link with the rel="diners" and follow that link.
GET /DiningService/diners
Content-Type: application/vnd.sample.diners+xml
200 OK
<Diners>
<Diner Name="Frank">
<Link rel="lunchbox" href="./Frank/lunchbox"/>
</Diner>
<Diner Name="Bob">
<Link rel="lunchbox" href="./Bob/lunchbox"/>
</Diner>
</Diners>
What comes back is a list of diners. I have chosen to create custom media types for simplicity, but you may be better off using something like Atom feeds for these lists. The client needs to identify Frank as the diner and so now we want to access his lunchbox. The rules of our custom media type say that the url to Frank's lunch box can be found in a link element with a rel="lunchbox". We get that URL from the response document and follow it.
GET /DiningService/Frank/lunchbox
Content-Type: application/vnd.sample.lunchbox+xml
200 OK
<Lunchbox>
<Link rel="diner" href="/DiningService/Frank"/>
<Food Name="CheeseSandwich" NutritionPoints="10">
<Link rel="eat" Method="POST" href="/DiningService/Frank?food=/DiningService/Food/CheeseSandwich"/>
</Food>
<Food Name="CucumberSandwich" NutritionPoints="15">
<Link rel="eat" Method="POST" href="/DiningService/Frank?food=/DiningService/Food/CucumberSandwich"/>
</Food>
</Lunchbox>
What we get back is another custom media type defining the contents of a lunchbox and links describing what we can do with that lunch box. Once the client chooses the food to eat, we can identify the URL to follow by looking for a link with rel="eat" and following that URL. In this case it is a post.
POST /DiningService/Frank?food=/DiningService/Food/CucumberSandwich
Content-Type: None
200 OK
I didn't think too hard about what the best way of structuring that url is because if I change my mind next week and make it
<Link rel="eat" Method="POST" href="/DiningService/Frank/Mouth?food=/DiningService/Food?id=759"/>
or even
<Link rel="eat" Method="POST" href="/DiningService/Food/CheeseSandwich?eatenBy=Frank"/>
it really does not matter to the client because it will continue to look for a link with rel="eat" and will follow the URL. You choose whatever URL structure works easiest for the web framework you have chosen. The URL structure belongs to the server and you should be able to change it whenever and have little or no impact on the client.
If you take this approach you can stop stressing over coming up with the perfect URL. This artificial notion of a "RESTful URL" has done more to prevent people from learning REST than SOAP ever did!
If you look at meal as an action on Diner, rather than an resource itself, this makes a bit more sense. However, I would be tempted to change the name to a verb, such as eat. When modelling a REST system, some of the decisions you make will be arbitrary. From a theoretical point of view, this action can be on both the Diner and the LunchBox. I tend to model according to how my app is used, so what fits with the UI and what is easier to explain to a third party in documentation etc.
There is nothing in the REST model that dictates the underlying structure or precludes you from handling quite complex transactions inside an action. In this case I would simply have an action that handles all of the logic using a transaction as necessary.
The action would operate on a diner, taking a list of foods and quantities.
In rails you would now have something like
# routes.rb
map.resources :diner, :member => {:eat => :post}
#controller
def eat
@diner = Diner.find(params[:id])
@diner.eat(params[:foods])
respond_to ...
end
end
You will notice that I have actually pushed the logic into the model. I am assuming that the Diner model has an association with a LunchBox model. The eat method will increment the health and change the food amounts in the related LunchBox. This way you can encapsulate all of the logic quite neatly.
UPDATE I think it is quite a common pattern to have Resources with some specific named operations. I often just add actions to my controller, but keep within the general framework of REST by exposing these actions using HTTP and the Rails conventions.
You can certainly model your system with Meal as a Resource, but I think this results in extra complexity for your requirements.
It is also possible to model you entire system with only operations that map to the standard HTTP methods, but for real-world systems it's cumbersome and clumsy. In this world view, you start walking down the path of coordinating multiple http actions to compose a higher-order API. Such a system is pretty much impossible to build with a half-decent UI, and if you are exposing an API to third-parties they're going to hate you.
精彩评论