2013-01-17 4 views
9

Was ist der beste Weg, um ein atomisches Einfügen/Update für ein Modell mit einem Zähler in Rails zu implementieren? Eine gute Analogie für das Problem zu lösen, ich versuche ein „wie“ Zähler mit zwei Feldern:atomisches Einfügen oder Inkrementieren in ActiveRecord/Rails

url : string 
count : integer 

Beim Einsatz, wenn es nicht noch ein Datensatz mit einer passenden URL ist, sollte ein neuer Datensatz erstellt werden mit Zählung 1; Andernfalls sollte das Feld count des vorhandenen Datensatzes inkrementiert werden.

Zunächst versuchte ich Code wie folgt aus:

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

aber wenig überraschend, zeigt die resultierende SQL, dass die SELECT außerhalb der UPDATE Transaktion geschieht:

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 

Gibt es eine Rails Idiom für den Umgang mit oder muss ich das auf SQL-Ebene implementieren (und damit die Portabilität verlieren)?

+0

4 Jahre später, immer noch auf der Suche nach einer konsistenten/Community-unterstützte Antwort. –

Antwort

5

Mir ist keine ActiveRecord-Methode bekannt, die atomare Inkremente in einer einzigen Abfrage implementiert. Ihre eigene Antwort ist so weit wie möglich.

Ihr Problem ist möglicherweise nicht mit ActiveRecord lösbar. Denken Sie daran: ActiveRecord ist nur ein Mapper, um einige Dinge zu vereinfachen und andere zu verkomplizieren. Einige Probleme sind nur durch einfache SQL-Abfragen in der Datenbank lösbar. Sie werden etwas Portabilität verlieren. Das folgende Beispiel funktioniert in MySQL, aber soweit ich weiß, nicht bei SQLlite und anderen.

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

Ich endete mit einer Variation Ihrer Lösung (quoted_url wird zweimal zitiert). Dies endete etwa fünfmal schneller als das Abfangen von ActiveRecord :: RecordNotUnique. Scheint so, als müsste etwas von Rails bereitgestellt werden. –

0

Rails unterstützt Transactions, wenn das, was Sie suchen, so:

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

Leider wird das alleine nicht funktionieren. Wenn Sie keinen eindeutigen Index für die URL-Spalte haben, ist es möglich, doppelte Einfügungen zu erhalten. Wenn Sie einen eindeutigen Index haben, erhalten Sie ActiveRecord :: RecordNotUnique.Ich habe gerade beide Szenarien getestet, indem ich einen "Schlaf 5" im Transaktionsblock hinzugefügt habe. Ich denke, eine DB-Isolationsstufe von SERIALIZABLE wäre erforderlich, um dies so zu machen, wie es ist. –

7

Hier ist, was ich mit so weit gekommen sind. Dies setzt einen eindeutigen Index für die Spalte url voraus.

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 Methode ist atomar, und übersetzt wie folgt SQL:

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

eine Ausnahme Fangen normale Geschäftslogik zu implementieren scheint ziemlich hässlich, so würde ich Verbesserungsvorschläge zu schätzen wissen.

5

können Sie verwenden permissive lock,

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

0

Für diesen Anwendungsfall und andere, ich denke update_allapproach saubersten ist.

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 

Nicht genau Sie Beispiel, nur von der verknüpften Seite eingefügt. Aber Sie kontrollieren die where Bedingung und was aktualisiert werden soll. Sehr geschickt