Validates_uniqueness_of et la duplicatio n de données


#1

Bonjour,

Je suis entrain de développer une application dans laquelle j’ai
besoin, à un moment donné, d’insérer plusieurs entrées dans une table:


property.rb

class Property < ActiveRecord::Base
belongs_to :version
validates_presence_of :name, :value
validates_uniqueness_of :name, :scope => :version_id
end

la table associée à ce model contient les colonnes : id, name, value
et version_id


version.rb

class Version < ActiveRecord::Base
has_many :properties ,:dependent => :destroy #e.g ON CASCADE
delete
accepts_nested_attributes_for :properties,:allow_destroy => true
end

la table associée contient les colonnes : id et numero

Le problème que j’ai est le suivant : lorsque j’insère via un
formulaire associé à la version 1 par exemple

prop1 de valeur p1
et
prop1 de valeur p2

les deux entrées sont insérées dans la table properties, ici, je viole
la contrainte validates_uniqueness_of, mais je ne reçois aucune
erreur, et les entrées sont insérées :frowning: Est-ce validates_uniqueness_of
qui ne fonctionne pas bien ? ou bien j’ai autres chose à rajouter ?

Si ma question n’est pas claire je peux la détailler encore.

Merci d’avance

Meilleures salutations / Best Regards

Rachid ALAHYANE


#2

Ce n’est pas super clair mais si je pige bien, tu fais deux nouvelles
instances de Property avec le même name et pas la même value, tu les lie
Ã
la même instance de Version, tu les sauves et ça passe malgré la
contrainte
d’unicité. Je veux bien que tu montres le code où tu fais cette manip en
console avec les retours…

Michel B.

2009/5/6 ALAHYANE Rachid removed_email_address@domain.invalid


#3

ALAHYANE Rachid a écrit :

validates_presence_of :name, :value
has_many :properties ,:dependent => :destroy #e.g ON CASCADE
et
prop1 de valeur p2

les deux entrées sont insérées dans la table properties, ici, je viole
la contrainte validates_uniqueness_of, mais je ne reçois aucune
erreur, et les entrées sont insérées :frowning: Est-ce validates_uniqueness_of
qui ne fonctionne pas bien ? ou bien j’ai autres chose à rajouter ?

Si ma question n’est pas claire je peux la détailler encore.

Tu peux le détailler avec un test unitaire? Ca sera largement plus
simple a lire.


Cyril M.
http://blog.shingara.fr


#4

2009/5/7 Cyril M. removed_email_address@domain.invalid

class Property < ActiveRecord::Base

Tu peux le détailler avec un test unitaire? Ca sera largement plus
simple a lire.

C’est un débat courant avec ruby on rails : la validation d’uniqueness
ne
devrait pas être situé au niveau modèle, mais au niveau base de données
sous
forme de contrainte. C’est la seule façon de valider correctement ton
unicité.
Tu devrais déclarer un index unique sur la colone (ou son ensemble) que
tu
souhaites puis catcher l’exception levée en cas de violation de la
contrainte (ActiveRecord::StatementInvalid). Enfin, vérifier qu’il
s’agit
bien de la contrainte d’unicité qui est relevée puis traiter le cas et
raiser ActiveRecord::Rollback pour remettre la transaction sql dans un
état
correct.

Il existe un plugin permettant de gérer ces contraintes (et d’autres)
pour
postgresql, c’est en cours de discussion sur rails-core pour une
éventuelle
modification du core afin de gérer plus proprement ce genre de cas. Mais
on
est encore loin d’avoir une chose uniforme pour cela :
http://github.com/pedz/activerecord_constraints/tree/master

Je crois que certains parlent même de développer une sorte de
gestionnaire
de validations qui permettrait à un endroit de déclarer ses règles de
validation, d’avoir les contraintes sql liées et de remonter les erreurs
de
validations ActiveRecord.


http://fabien.jakimowicz.com


#5

Dans ton log, où sont situés les

SELECT * FROM “properties” |
WHERE version_id = 3

Car d’après l’API, le principal risque de validates_uniqueness_of se
situe ici.

Si tous les select sont fait avant les insert…ben automatiquement,
tous retourneront que la propriété dans cette version n’existe pas
encore autorisant du même coup tous les insert

2 solutions :

  • la contrainte au niveau de la table comme dit plus haut
  • effectuer tes insertion complètement avant de permettre la suivante
    (en effet, pas forcement simple en javascript, il te faudrait bloquer la
    création de nouvelles propriétés tant que tu n’as pas eu le retour avec
    confirmation de la sauvegarde de la propriété précédente.)

#6

Merci pour vos réponse, le plugin que Fabien propose est pas mal, il
faudra que je le teste.

En fait, lorsque je fais des tests via la console Ruby tout marche
comme prévu, c’est à dire que si je fais :

p2 = Property.new
=> #<Property id: nil, type: “Property”, version_id: nil, name: nil,
value: nil, created_at: nil, updated_at: nil>

p3 = Property.new
=> #<Property id: nil, type: “Property”, version_id: nil, name: nil,
value: nil, created_at: nil, updated_at: nil>

v = Version.find :first
=> #<Version id: 1, num: 1, description: “des v1”, created_at:
“2009-05-06 16:10:50”, updated_at: “2009-05-06 16:10:50”>

p2.version = v
=> #<Version id: 1, num: 1, description: “des v1”, created_at:
“2009-05-06 16:10:50”, updated_at: “2009-05-06 16:10:50”>

p3.version = v
=> #<Version id: 1, num: 1, description: “des v1”, created_at:
“2009-05-06 16:10:50”, updated_at: “2009-05-06 16:10:50”>

p3.name = “req22”
=> “req22”

p2.name = “req22”
=> “req22”

p2.value = “r22”
=> “r22”

p3.value = “r33”
=> “r33”

p3.save and p2.save
=> false

v.properties
=> [#<Property id: 21, type: “Property”, version_id: 1, name: “req22”,
value: “r33”, created_at: “2009-05-07 20:36:53”, updated_at:
“2009-05-07 20:36:53”>]

Ici, on voit très bien que p2 n’est pas ajouté à la base de données.

Le problème avec mon application, c’est que la saisie se fait via un
formulaire généré à la volé (du javascript)

Je passe par de tas de partiels pour afficher ce formulaire…

Je me suis dis que ce peut être ActiveRecord insère les deux lignes en
une seule requête, mais lorsque j’ai consulté les logs j’ai trouvé que
ce n’est pas le cas. C’est atomique, voici un bout de log :

SQL (0.4ms) INSERT INTO “properties” (“name”, “updated_at”,
“version_id”, “value”, “created_at”) VALUES(E’req2’, ‘2009-05-06
18:23:49.090473’, 3, E’hgjhg’, ‘2009-05-06 18:23:49.090473’) RETURNING
“id”

SQL (0.4ms) INSERT INTO “properties” (“name”, “updated_at”,
“version_id”, “value”, “created_at”) VALUES(E’req2’, ‘2009-05-06
18:23:49.096753’, 3, E’lkj’, ‘2009-05-06 18:23:49.096753’) RETURNING
“id”

SQL (0.4ms) INSERT INTO “properties” (“name”, “updated_at”,
“version_id”, “value”, “created_at”) VALUES(E’req2’, ‘2009-05-06
18:23:49.098239’, 3, E’kjlkj’, ‘2009-05-06 18:23:49.098239’) RETURNING
“id”

SQL (0.4ms) INSERT INTO “properties” (“name”, “updated_at”,
“version_id”, “value”, “created_at”) VALUES(E’req2’, ‘2009-05-06
18:23:49.099697’, 3, E’sdfasdf’, ‘2009-05-06 18:23:49.099697’)
RETURNING “id”

cela ne vient pas du fait que j’utilise du javascript ?

On 7 mai 09, at 03:08, Fabien J. wrote:

property.rb

Le problème que j’ai est le suivant : lorsque j’insère via un
validates_uniqueness_of
niveau base de données sous forme de contrainte. C’est la seule
d’autres) pour postgresql, c’est en cours de discussion sur rails-
http://fabien.jakimowicz.com

–~--~---------~–~----~------------~-------~–~----~
Vous avez reçu ce message, car vous êtes abonné au groupe
“Railsfrance” de Google G…
Pour transmettre des messages à ce groupe, envoyez un e-mail Ã
l’adresse removed_email_address@domain.invalid
Pour résilier votre abonnement envoyez un e-mail à l’adresse removed_email_address@domain.invalid
-~----------~----~----~----~------~----~------~–~—

Meilleures salutations / Best Regards

Rachid ALAHYANE


#7

ALAHYANE Rachid a écrit, le 05/07/2009 11:00 PM :

Le problème avec mon application, c’est que la saisie se fait via un
formulaire généré à la volé (du javascript)

Le problème avec une application web c’est qu’il peut y avoir plusieurs
requêtes simultanées. Ton cas n’est donc pas exceptionnel. Le
validates_uniqueness_of ne protège pas contre deux requêtes simultanée
ayant pour effet de créer des entrées conflictuelles puisqu’il se
contente de vérifier si une entrée conflictuelle existe avant de
l’ajouter.

Comme l’expliquait Fabien, la seule solution est d’ajouter la contrainte
d’unicité en base.
Une autre consisterait à créer une transaction et verrouiller la table
entière avant d’essayer de sauver l’objet, mais d’une part les
performances baisseraient beaucoup et d’autre part il serait très facile
de se retrouver en situation de deadlock.

“version_id”, “value”, “created_at”) VALUES(E’req2’, ‘2009-05-06
18:23:49.099697’, 3, E’sdfasdf’, ‘2009-05-06 18:23:49.099697’)
RETURNING “id”

Note que ces requêtes sont séparées par quelques millisecondes au plus :
le SELECT fait par le processus qui génère le dernier INSERT pour
vérifier qu’il n’y a pas de conflit a déjà eu lieu lorsque le premier
INSERT s’exécute (ce que tu peux aisément vérifier dans tes logs) et ne
voit donc pas de problème.

cela ne vient pas du fait que j’utilise du javascript ?

Non, ça vient du fait qu’il n’y a pas moyen de vérifier qu’un autre
processus ne fait pas quelquechose derrière ton dos dans la base de
données si tu ne demandes pas à la base de données de le vérifier pour
toi (en ajoutant les contraintes qui te sont nécessaires dans cette
dernière). Les requêtes faites en Javascript illustrent juste le
problème (qui se produirait de toute façon lorsque plusieurs
utilisateurs accèderaient à ton appli pour créer des entrées
conflictuelles).

Lionel


#8

Merci pour vos explications claires et précises.
Alors je testerai le plugin et puis je ferai un retour ici.

On 8 mai 09, at 01:07, Lionel B. wrote:

validates_uniqueness_of ne protège pas contre deux requêtes simultanée
facile

cela ne vient pas du fait que j’utilise du javascript ?

Lionel

–~--~---------~–~----~------------~-------~–~----~
Vous avez reçu ce message, car vous êtes abonné au groupe
“Railsfrance” de Google G…
Pour transmettre des messages à ce groupe, envoyez un e-mail Ã
l’adresse removed_email_address@domain.invalid
Pour résilier votre abonnement envoyez un e-mail à l’adresse removed_email_address@domain.invalid
-~----------~----~----~----~------~----~------~–~—

Meilleures salutations / Best Regards

Rachid ALAHYANE