首页 > 解决方案 > 服务调用未更新 Active Record 值以增加连接模型

问题描述

我有一项服务会在条带结帐完成时触发。我希望将一张带有唯一编号的票添加到用户的库存中。

请注意 object.client_reference_id 和元数据来自 Stripe webhook 并且都被确认返回正确的数据。

根据该票的比赛的 max_tickets 值减去刚刚创建的票,创建票并给出票号。

# services/ticket_service.rb

class TicketService
    def self.call(event)
      new(event).call
    end
  
    attr_reader :event
    def initialize(event)
      @event = event
    end
  
    def call
      user = User.find_by(id: object.client_reference_id)
      return unless user
      contest = Contest.find_by(id: object.metadata.order_item.to_i)
      # Problem code below
      user.tickets.create!(purchased: true, user_id: user.id, 
      contest_id: contest.id, ticket_number: contest.max_tickets -= 1 )

      OrderMailer.with(contest: contest).new_order_email.deliver_later

    end
  
    private
  
    def object
      @object ||= event.data.object
    end

  end

但是,每次以这种方式购买门票时,竞赛.max_tickets -= 1部分似乎都无法按预期工作。无论我尝试购买这张票多少次,我的库存中都有同一张票,票号为:49 / 50 。

奇怪的是,当我在 rails 控制台中以相同的方式创建票证时,票证编号会按预期递减 49、47、48 等。

irb(main):024:0> user.tickets.create!(purchased: true, user_id: user.id, contest_id: contest.id, ticket_number: contest.max_tickets -= 1 )
   (0.4ms)  BEGIN
  Contest Load (0.5ms)  SELECT "contests".* FROM "contests" WHERE "contests"."deleted_at" IS NULL AND "contests"."id" = $1 LIMIT $2  [["id", 17], ["LIMIT", 1]]
  Ticket Create (0.4ms)  INSERT INTO "tickets" ("contest_id", "user_id", "created_at", "updated_at", "purchased", "ticket_number") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id"  [["contest_id", 17], ["user_id", 63], ["created_at", "2020-07-25 02:18:33.819410"], ["updated_at", "2020-07-25 02:18:33.819410"], ["purchased", true], ["ticket_number", 43]]
   (1.7ms)  COMMIT
=> #<Ticket id: 599, contest_id: 17, user_id: 63, ticket_price: nil, created_at: "2020-07-25 02:18:33", updated_at: "2020-07-25 02:18:33", purchased: true, ticket_number: 43>

标签: ruby-on-railsruby

解决方案


调用-=不会生成update查询以保留您对列所做的任何更改,并且不清楚为什么这在您的数据库示例中似乎有效,因为输出中没有update查询contests表明它可能有效。

您需要在修改其字段后保存比赛,使用contest.save!, 并且您希望在事务中执行此操作以确保两个表 (conteststickets) 都已更新,并使用锁定防止多个并发购票创建竞争条件:

您想要的代码如下所示:

contest = Contest.find_by(id: object.metadata.order_item.to_i)
contest.with_lock do
  # TODO: verify max_tickets is still > 0
  user.tickets.create!(purchased: true, user_id: user.id, 
  contest_id: contest.id, ticket_number: contest.max_tickets -= 1)
  contest.save!
end

请参阅 Rails 的悲观锁定with_lock

只需查看您的代码的一些注释:

  • 你应该有一个唯一的索引tickets,对列[contest_id, ticket_number]。同一场比赛的 33 号门票永远不可能被送出两次。
  • 您应该在锁内验证contest.max_tickets仍然大于 0。您现在没有任何明显的检查,除非您依赖Ticket模型内部的验证。如果没有上述锁定/索引,此验证是不够的。

推荐阅读