在Rails中为带有计数器的模型实现原子插入/更新的最佳方法是什么?对于我试图解决的问题,一个很好的类比是一个带有两个字段的"like"计数器:
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级别实现它(从而失去一些可移植性)?
可以使用悲观锁,
like = Like.find_or_create_by_url("http://example.com")
like.with_lock do
like.increment!(:count)
end
我不知道有任何ActiveRecord方法在单个查询中实现原子增量。你自己的答案是你所能得到的。
所以你的问题可能无法解决使用ActiveRecord。记住:ActiveRecord只是一个映射器,用于简化某些事情,同时使其他事情复杂化。有些问题只能通过对数据库的普通SQL查询来解决。您将失去一些可移植性。下面的例子将在MySQL中工作,但据我所知,不适用于sqlite和其他。
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;");
这是我到目前为止所想到的。这假定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
捕捉异常来实现正常的业务逻辑似乎相当丑陋,所以我希望能得到改进的建议。
Rails支持事务,如果这是你正在寻找的,所以:
Like.transaction do
Like.find_or_create_by_url("http://example.com").increment!(:count)
end
目前为止我最喜欢的解决方案:
Like.increment_counter(
:count,
Like.where(url: url).first_or_create!.id
)
对于这个用例和其他用例,我认为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
条件和要更新的内容。非常漂亮的