【问题标题】:Performing multiple database operations in a single REST call在单个 REST 调用中执行多个数据库操作
【发布时间】:2010-01-29 23:29:25
【问题描述】:

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

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

我想设置一个呼叫,表明 Diner 吃了他们的一种食物,从而相应地增加 Diner 的健康。某些食物比其他食物更有营养,因此可以不同程度地增加用餐者的健康。构成这一点的行动是:

  • 将给定食物的 Diner's LunchBox 上的计数属性减少正确的数量
  • 相应地增加食客的生命值

因此这里需要更新两个表:Diner 和 Lunchbox,都在一个事务中。

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

POST /diner/frank/meal

描述一顿饭的 XML 类似于

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

然而,这让我觉得很做作。在 REST 中发布餐点应该创建一个餐点资源。在这种情况下,我们不仅没有创建 Meal 资源,而且还更新了另外两个资源:Diner 和 LunchBox。

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

其他人是如何解决这个公认的非常基本的问题的?

【问题讨论】:

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


    【解决方案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 识别为用餐者,因此现在我们要访问他的午餐盒。我们自定义媒体类型的规则说,弗兰克午餐盒的 url 可以在带有 rel="lunchbox" 的链接元素中找到。 我们从响应文档中获取该 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。您可以选择最适合您选择的 Web 框架的任何 URL 结构。 URL 结构属于服务器,您应该可以随时更改它,并且对客户端几乎没有影响。

    如果您采用这种方法,您就可以不用再为想出完美的网址而烦恼了。这种人为的“RESTful URL”概念比 SOAP 更能阻止人们学习 REST!

    【讨论】:

    • 谢谢,这个例子帮助我进一步思考了这个过程。不过,我不太了解您的方法 - 如果 REST 应用程序中的每个项目都应该是资源,那么我不知道在您上次调用中公开的资源是什么。在调用 /DiningService/Frank?food=/DiningService/Food/CucumberSandwich 中,资源是什么?是弗兰克,还是黄瓜三明治?
    • 在那个特定的 URL 中,资源是 Frank。您可能会争辩说,将食物 url 发布到餐厅资源意味着什么并不是很清楚。在第二个 URL 中,我创建了一个名为“mouth”的特殊“处理资源”,它是食客的子资源,当您向嘴资源发布食物 URL 时,执行吃操作。当然这有点傻,但我的意思是,只要你正确使用动词,url 说什么并不重要。 POST 行为没有真正定义,因此很难误用。
    【解决方案2】:

    如果您将 meal 视为对 Diner 的操作,而不是资源本身,那么这更有意义。但是,我很想将名称更改为动词,例如 eat。在对 REST 系统进行建模时,您做出的一些决定将是任意的。从理论上讲,这个动作可以在 DinerLunchBox 上进行。我倾向于根据我的应用程序的使用方式进行建模,因此哪些内容适合 UI,哪些内容更容易在文档等中向第三方解释。

    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 模型与午餐盒模型有关联。 eat 方法将增加生命值并更改相关午餐盒中的食物量。通过这种方式,您可以非常巧妙地封装所有逻辑。

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

    您当然可以使用 Meal as Resource 为您的系统建模,但我认为这会导致您的要求更加复杂。

    也可以使用映射到标准 HTTP 方法的 only 操作为您的整个系统建模,但对于现实世界的系统来说,它既麻烦又笨拙。在这个世界观中,您开始走上协调多个 http 操作以组成更高阶 API 的路径。这样的系统几乎不可能用一个像样的 UI 构建,如果你将 API 暴露给第三方,他们会讨厌你。

    【讨论】:

    • 谢谢。不过,我在这里看到了一些我想问的事情。首先,“吃”不是一个令人困惑的选择,因为它是一个动词吗?这就是为什么我想使用“meal”,因为然后你发布一个新的 Meal(即关于 REST 资源的全部内容都是名词)。其次,既然动作不是幂等的,这不应该是 POST 而不是 PUT 吗?最后,这意味着 /diner/frank/eat 会产生更新午餐盒模型的副作用,即使用户不一定请求更新该模型。在 REST 下这仍然是 kosher 吗?
    • 开始评论,但在我用完字符时修改了我的帖子
    • 哦,你在 put vs post 上是对的,代码已经修改,我从我手头的 routes.rb 中剪切/粘贴。
    • 是的,只遵循 Http 动词有时会让人觉得很麻烦,但这样做确实有好处。我不同意走这条路线需要你协调多个Http请求才能实现更高阶的功能。
    • 我认为我们实际上在谈论相同的方法。我只是使用 Ruby on Rails 术语和约定来构建一个与您建议的非常相似的资源结构。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-04
    • 1970-01-01
    • 1970-01-01
    • 2023-04-03
    • 2014-10-12
    • 1970-01-01
    相关资源
    最近更新 更多