atomic insert or increment in ActiveRecord/Rails

三世轮回 提交于 2019-12-18 04:48:10

问题


What's the best way to implement an atomic insert/update for a model with a counter in Rails? A good analogy for the problem I'm trying to solve is a "like" counter with two fields:

url   : string
count : integer

Upon insert, if there is not currently a record with a matching url, a new record should be created with count 1; else the existing record's count field should be incremented.

Initially I tried code like the following:

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

But unsurprisingly, the resulting SQL shows that the SELECT happens outside the UPDATE transaction:

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

Is there a Rails idiom for dealing with this, or do I need to implement this at the SQL level (and thus lose some portability)?


回答1:


I am not aware of any ActiveRecord method that implemts atomic increments in a single query. Your own answer is a far as you can get.

So your problem may not be solvable using ActiveRecord. Remember: ActiveRecord is just a mapper to simplify some things, while complicating others. Some problems just solvable by plain SQL queries to the database. You will loose some portability. The example below will work in MySQL, but as far as I know, not on SQLlite and others.

 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;");



回答2:


You can use pessimistic locking,

like = Like.find_or_create_by_url("http://example.com")
like.with_lock do
    like.increment!(:count)
end



回答3:


Here is what I have come up with so far. This assumes a unique index on the url column.

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 method is atomic, and translates to SQL like the following:

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

Catching an exception to implement normal business logic seems rather ugly, so I would appreciate suggestions for improvement.




回答4:


Rails supports Transactions, if that's what you're looking for, so:

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



回答5:


For this use case and others, I think update_all approach is cleanest.

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

Not exactly you example, just pasted from the linked page. But you control the where condition and what is to be updated. Very nifty




回答6:


My favorite solution thus far:

Like.increment_counter(
  :count, 
  Like.where(url: url).first_or_create!.id
)


来源:https://stackoverflow.com/questions/14387022/atomic-insert-or-increment-in-activerecord-rails

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!