11

在 Rails 中为带有计数器的模型实现原子插入/更新的最佳方法是什么?我试图解决的问题的一个很好的类比是具有两个字段的“喜欢”计数器:

url   : string
count : integer

插入时,如果当前没有匹配 url 的记录,则应创建计数为 1 的新记录;count否则应该增加现有记录的字段。

最初我尝试了如下代码:

Like.find_or_create_by_url("http://example.com").increment!(:count)

但不出所料,生成的 SQL 表明SELECT发生在UPDATE事务之外:

Like Load (0.4ms)  SELECT `likes`.* FROM `likes` WHERE `likes`.`url` = 'http://example.com' LIMIT 1
(0.1ms)  BEGIN
(0.2ms)  UPDATE `likes` SET `count` = 4, `updated_at` = '2013-01-17 19:41:22' WHERE `likes`.`id` = 2
(1.6ms)  COMMIT

是否有用于处理此问题的 Rails 习语,或者我是否需要在 SQL 级别实现它(从而失去一些可移植性)?

4

6 回答 6

12

您可以使用悲观锁定

like = Like.find_or_create_by_url("http://example.com")
like.with_lock do
    like.increment!(:count)
end
于 2014-05-23T19:10:01.757 回答
7

到目前为止,这是我想出的。这假定url列上有一个唯一索引。

begin
  Like.create(url: url, count: 1)
  puts "inserted"
rescue ActiveRecord::RecordNotUnique
  id = Like.where("url = ?", url).first.id
  Like.increment_counter(:count, id)
  puts "incremented"
end

Rails 的increment_counter方法是原子的,并转换为 SQL,如下所示:

UPDATE 'likes' SET 'count' = COALESCE('count', 0) + 1 WHERE 'likes'.'id' = 1

捕获异常来实现正常的业务逻辑似乎相当难看,所以我会很感激改进建议。

于 2013-01-18T15:00:42.297 回答
7

我不知道在单个查询中实现原子增量的任何 ActiveRecord 方法。你自己的答案是你能得到的。

所以你的问题可能无法使用 ActiveRecord 解决。请记住:ActiveRecord 只是一个映射器,用于简化某些事情,同时使其他事情复杂化。有些问题只能通过对数据库的普通 SQL 查询来解决。您将失去一些可移植性。下面的示例适用于 MySQL,但据我所知,不适用于 SQLlite 等。

 quoted_url = ActiveRecord::Base.connection.quote(@your_url_here)
::ActiveRecord::Base.connection.execute("INSERT INTO likes SET `count` = 1, url = \"#{quoted_url}\" ON DUPLICATE KEY UPDATE count = count+1;");
于 2013-01-19T09:37:08.923 回答
0

Rails 支持Transactions,如果这是您正在寻找的,那么:

Like.transaction do
  Like.find_or_create_by_url("http://example.com").increment!(:count)
end
于 2013-01-17T21:10:51.383 回答
0

到目前为止我最喜欢的解决方案:

Like.increment_counter(
  :count, 
  Like.where(url: url).first_or_create!.id
)
于 2019-01-18T18:34:47.787 回答
-2

对于这个用例和其他用例,我认为update_all 方法是最干净的。

num_updated =
  ModelClass.where(:id             => my_active_record_model.id,
                   :service_status => "queued").
             update_all(:service_status => "in_progress")
if num_updated > 0
  # we updated
else
  # something else updated in the interim
end

不完全是您的示例,只是从链接页面粘贴。但是您可以控制where条件以及要更新的内容。非常漂亮

于 2016-08-22T21:11:15.980 回答