在单个REST调用中执行多个数据库操作

时间:2010-01-29 23:29:25

标签: ruby-on-rails database rest transactions client-server

我正在尝试找出调用REST操作的最佳方法,这些操作可以通过单个调用执行多个操作和多个数据库更新。

在我的数据模型中,我有Diners和LunchBoxes以及Foods。 LunchBoxes只是Diners和Foods之间的多对多关系,但有一个count属性,表示给定的Diner有多少种类的食物。

我想设置一个电话,表明Diner吃了他们的一个食物,这相应地增加了晚餐的健康。某些食物比其他食物营养更丰富,因此不同程度地增加了晚餐的健康。构成这一点的行动将是:

  • 使用正确的金额减少Diner's LunchBox上给定食物的count属性
  • 相应地提高晚餐的健康状况

因此,这里需要更新两个表:Diner和Lunchbox,两者都在一次交易中。

尝试使用名词,我能想到的最好的是:

POST / diner / frank / meal

描述用餐的XML类似于

<meal>
  <food>
    <id>apple</id>
  </food>
  <count>2</count>
</meal>

然而,这让我感到非常沮丧。在REST中发布一顿饭应该会创建一个Meal资源。在这种情况下,我们不仅不创建Meal资源,而且我们正在更新另外两个资源:Diner和LunchBox。

我想一种方法是让客户端在两个单独的调用中处理它 - 一个用于更新Diner,另一个用于更新LunchBox。但是,这似乎是错误的,因为我们有多个客户端(HTML,Flash等)都需要执行此操作。如果我们在将来更新用于食用食物的业务逻辑,那么我们需要在许多客户端而不是在单个服务器上进行更改。

其他人如何接近这个公认的非常基本的问题?

2 个答案:

答案 0 :(得分:1)

如果您将 meal 视为 Diner 上的操作,而不是资源本身,这会更有意义。但是,我很想将名称改为动词,例如 eat 。在为REST系统建模时,您做出的一些决策将是任意的。从理论的角度来看,此操作可以在 Diner LunchBox 上进行。我倾向于根据我的应用程序的使用方式进行建模,因此适合用户界面以及更容易向文档等第三方解释的内容。

REST模型中没有任何内容指示底层结构或阻止您处理动作中非常复杂的事务。在这种情况下,我只需要一个动作,根据需要使用事务处理所有逻辑。

该行动将在餐馆运作,列出食物和数量。

在rails中,您现在可以使用

# routes.rb
map.resources :diner, :member => {:eat => :post}

#controller
def eat
  @diner = Diner.find(params[:id])
  @diner.eat(params[:foods])
  respond_to ...
  end
end

您会注意到我实际上已将逻辑推入模型中。我假设Diner模型与LunchBox模型有关联。 eat 方法将增加健康状况并更改相关LunchBox中的食物量。通过这种方式,您可以非常巧妙地封装所有逻辑。

<强>更新 我认为让Resources具有一些特定的命名操作是一种非常常见的模式。我经常只是向我的控制器添加操作,但通过使用HTTP和Rails约定公开这些操作,保持在REST的一般框架内。

您当然可以使用Meal作为资源对系统进行建模,但我认为这会使您的需求更加复杂。

也可以使用映射到标准HTTP方法的操作为整个系统建模,但对于真实世界的系统来说,它很麻烦且笨拙。在这个世界视图中,您开始沿着协调多个http动作的路径走下去,组成一个更高阶的API。这样的系统几乎不可能用一半体面的UI构建,如果你向第三方公开API,他们会讨厌你。

答案 1 :(得分:1)

首先,用餐和午餐盒的更新绝对应该在一个请求中完成。不要陷入尝试通过REST API进行事务的陷阱。

在我们讨论您的具体问题之前,我们可以为客户如何与您的服务进行互动奠定基础。

客户端应始终从根服务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>

我不知道您的用户将如何与客户端软件进行交互,但我们假设我们首先需要确定谁将要用餐。我们可以通过查看与rel =“diners”链接的响应来检索用户列表,并点击该链接。

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>

回来的是食客名单。为简单起见,我选择创建自定义媒体类型,但您可能最好使用Atom提要这些列表。 客户需要将Frank视为晚餐,所以现在我们想要访问他的午餐盒。我们的自定义媒体类型的规则说弗兰克的午餐盒的网址 可以在带有rel =“lunchbox”的link元素中找到。 我们从响应文档中获取该URL并按照它进行操作。

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>

我们得到的是另一种定义午餐盒内容的自定义媒体类型以及描述我们可以用午餐盒做什么的链接。一旦客户选择要吃的食物,我们就可以通过查找rel =“eat”并且跟随该URL的链接来识别要遵循的URL。在这种情况下,它是一个帖子。

POST /DiningService/Frank?food=/DiningService/Food/CucumberSandwich
Content-Type: None
200 OK

我没想太多关于构建这个网址的最佳方法是什么,因为如果我下周改变主意并做到这一点

<Link rel="eat" Method="POST" href="/DiningService/Frank/Mouth?food=/DiningService/Food?id=759"/>

甚至

<Link rel="eat" Method="POST" href="/DiningService/Food/CheeseSandwich?eatenBy=Frank"/>

它对客户端无关紧要,因为它将继续查找rel =“eat”的链接,并将跟随URL。您选择的URL结构最适合您选择的Web框架。 URL结构属于服务器,您应该能够随时更改它,对客户端影响很小或没有影响。

如果你采用这种方法,你可以不再强调提出完美的URL。这种人为的“RESTful URL”概念已经做了更多的工作来防止人们学习REST而不是SOAP曾经做过!