Je rencontre une condition de course à ActiveRecord avec PostgreSQL où je lis une valeur puis incrémenter et l'insertion d'un nouveau record:PostgreSQL et ActiveRecord subselect pour connaître l'état de la course
num = Foo.where(bar_id: 42).maximum(:number)
Foo.create!({
bar_id: 42,
number: num + 1
})
A l'échelle, plusieurs threads seront lire simultanément puis écrire la même valeur de number
. Envelopper ceci dans une transaction ne fixe pas la condition de concurrence car le SELECT ne verrouille pas la table. Je ne peux pas utiliser un incrément automatique, car number
n'est pas unique, il est seulement unique compte tenu d'un certain bar_id
. Je vois 3 solutions possibles: (un blocage de niveau ligne)
- Explicitement utiliser un verrouillage postgres
- Utilisez une contrainte unique et essayez de nouveau sur échoue
Override sauf à utiliser un sous-(beurk!) , C'EST À DIRE
INSERT INTO foo (bar_id, number) VALUES (42, (SELECT MAX(number) + 1 FROM foo WHERE bar_id = 42));
Toutes ces solutions semblent comme je serais Réimplémenter une grande partie de ActiveRecord::Base#save!
est-il un moyen plus facile?
MISE À JOUR: Je pensais que j'ai trouvé la réponse avec Foo.lock(true).where(bar_id: 42).maximum(:number)
mais qui utilise SELECT FOR UDPATE
ce qui est interdit sur les requêtes globales
MISE À JOUR 2: Je viens d'être informé par notre DBA, que même si nous pouvions faire INSERT INTO foo (bar_id, number) VALUES (42, (SELECT MAX(number) + 1 FROM foo WHERE bar_id = 42));
qui ne fixe pas quoi que ce soit, puisque le SELECT fonctionne dans une serrure différente de celle INSERT
Si elle était calculée à la volée, elle changerait au fil du temps et serait soumise aux mêmes conditions de concurrence –