2013-01-17 19 views
9

Railsにカウンタを持つモデルのアトミック・インサート/更新を実装する最良の方法は何ですか?現在、一致するURLを持つレコードが、新しいレコードを作成する必要がない場合には、挿入時にActiveRecord/Railsのアトミック・インサートまたはインクリメント

url : string 
count : integer 

:私が解決しようとしている問題のための良いアナロジーは、2つのフィールドを持つ「のような」カウンタですカウント1で、そうでなければ、既存のレコードのcountフィールドをインクリメントする必要があります。

は当初、私は次のようなコードを試してみました:

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

をしかし当然、結果のSQLはSELECTUPDATEトランザクション外起こることを示しています

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レベルでこれを実装する必要があります(したがって、移植性が失われます)。

+0

4年後、一貫した/コミュニティサポートの回答を探していました。 –

答えて

5

アトミックインクリメントを実装している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;"); 
+1

あなたのソリューションのバリエーションを使用しました(quoted_urlは2回引用されています)。これは、ActiveRecord :: RecordNotUniqueをキャッチするよりも約5倍速くなりました。 Railsが提供すべきもののようです。 –

0

Railsのそれはあなたが探しているものですので、もし、Transactionsをサポートしています。ここでは

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

残念ながら、それだけでは機能しません。 URL列にユニークなインデックスがない場合は、重複した挿入を取得することができます。独自のインデックスを持っている場合、ActiveRecord :: RecordNotUniqueを取得します。トランザクションブロックに「スリープ5」を追加して、両方のシナリオをテストしました。私は、SERIALIZABLEのDB分離レベルが、この作業をそのまま行うために必要となると考えています。 –

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 

は、通常のビジネスロジックを実装するために、例外をキャッチむしろ醜いと思われるので、私は改善のための提案をいただければ幸いです。

5

あなたはこのユースケースと他の人のためpermissive lock

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

0

を使用することができ、私はupdate_allapproachがクリーンだと思います。

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の状態と何が更新されるのかを制御します。非常に気の利いた

関連する問題