2013-01-17 19 views
9

Qual è il modo migliore per implementare un inserto/aggiornamento atomico per un modello con un contatore in Rails? Una buona analogia per il problema che sto cercando di risolvere è un "come" bancone con due campi:Inserimento o incremento atomico in ActiveRecord/Rails

url : string 
count : integer 

Su inserto, se non v'è attualmente un record con un URL corrispondente, deve essere creato un nuovo record con conteggio 1; altrimenti il ​​campo count del record esistente deve essere incrementato.

Inizialmente ho provato il codice come il seguente:

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

Ma non sorprende, il codice SQL risultante mostra che il SELECT accade al di fuori del UPDATE transazione:

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 

C'è un idioma Rails per affrontare questo, o devo implementarlo a livello SQL (e quindi perdere una certa portabilità)?

+0

4 anni dopo, ancora alla ricerca di una risposta coerente/supportata dalla comunità. –

risposta

5

Non sono a conoscenza di alcun metodo ActiveRecord che esegua incrementi atomici in una singola query. La tua risposta è molto lontana.

Quindi il tuo problema potrebbe non essere risolvibile usando ActiveRecord. Ricorda: ActiveRecord è solo un mappatore per semplificare alcune cose, mentre complicando gli altri. Alcuni problemi sono risolvibili solo con semplici query SQL sul database. Perderai qualche portabilità. L'esempio seguente funzionerà in MySQL, ma per quanto ne so, non su SQLlite e altri.

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

Ho finito per utilizzare una variante della soluzione (quoted_url è quotata due volte). Questo ha finito per essere circa 5 volte più veloce di prendere ActiveRecord :: RecordNotUnique. Sembra una cosa che Rails dovrebbe fornire. –

0

Rails supporta Transactions, se è questo che stai cercando, quindi:

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

Purtroppo questo di per sé non funzionerà. Se non si dispone di un indice univoco nella colonna url, è possibile ottenere inserimenti duplicati. Se hai un indice univoco, otterrai ActiveRecord :: RecordNotUnique.Ho appena testato entrambi gli scenari aggiungendo un "sleep 5" nel blocco della transazione. Penso che sarebbe necessario un livello di isolamento DB di SERIALIZABLE per rendere questo lavoro così com'è. –

7

Ecco quello che sono venuto in mente finora. Ciò presuppone un indice univoco sulla colonna url. Metodo increment_counter

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' è atomico, e si traduce in SQL come la seguente:

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

Facendo un'eccezione per implementare la logica di business normale sembra piuttosto brutto, così mi avrebbe apprezzato i suggerimenti di miglioramento.

5

È possibile utilizzare permissive lock,

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

0

Per questo caso d'uso e altri, penso update_allapproach è più pulita.

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 

Non esattamente l'esempio, appena incollato dalla pagina collegata. Ma tu controlli la condizione where e cosa deve essere aggiornato. Molto elegante

Problemi correlati