11

Railsにカウンターがあるモデルのアトミック挿入/更新を実装するための最良の方法は何ですか?私が解決しようとしている問題の良い例えは、2つのフィールドを持つ「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レベルでこれを実装する必要がありますか(したがって、移植性がいくらか失われます)?

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はトランザクションをサポートしています。それがあなたが探しているものであれば、次のようになります。

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 に答える