Validation et associations

Hi there !

Mon message n’est pas vraiment une question, c’est juste une petite
synthese
de reflexions sur le systeme des associations / validation / callbacks
dans
RoR qui peut-etre eviterons a certains de passer des heures sur de
vaines
recherches (argh je deviens aigri !).
J’espere que vous comprendrez mes exemples :slight_smile:

  • Les callbacks:

Alors ce que j’ai decouvert est assez surprenant. Prenons un exemple
simple,
nous avons 2 models differents:

  • model Content qui possede 2 images de type Image

class Content < ActiveRecord::Base
has_one :banner, :class_name => ‘Image’
has_one :vignette, :class_name => ‘Image’
end

  • model Image qui possede un callback (before_save par exemple) qui pour
    une
    raison X entraine un return false (donc casse la “chain”, cf doc api).
    Hypotheses pour ce modele: la sauvegarde de :banner entraine un return
    false
    (cf ci-dessus) MAIS la sauvegarde vignette est OK quant a elle.

Essayons de sauver en base une instance de Content

==> resultats: banner non sauvegardee (car chaine cassee) DE MEME QUE
vignette (rien ne sera “processe” sur vignette, aucune validation, aucun
callback, nada). Excellent non ? Alors que banner et vignette sont 2
objets
differents en memoire !!!
Si on inverse l’ordre des associations dans Content, c’est l’inverse
(normal).

  • Sauvegarde des associations

Genial avec RoR, les associations sont sauvegardees automatiquement que
cela
soit des create ou des update. Dans 90% des cas, c’est mega pratique
mais
bizarrement aucune option n’a ete developpee pour eviter la sauvegarde
automatique. J’ai eu beau cherche avec mon ami google, personne n’a de
soluce ou semble se poser la question.

  • Validation des associations

Ce qui est interessant avec les loggers, c’est que l’on peut voir
comment se
comporte RoR. Et j’ai trouve un truc vachement interessant. Reprenons
mon
premier exemple et virons la deuxieme association par exemple.
Imaginons que le model Image possede une validation tres poussee (genre
calcul pour savoir si les dimensions sont exactes)
On cree ensuite un controller (+ action) avec la vue pour la creation a
la
fois d’un Content et de la banner associee (donc de type image).

1er test: Content et Image sont valides.
Resultat: Pas de probleme, le tout est sauvegarde. No souci.
On remarque juste dans les logs, que la validation de la banner s’est
faite
APRES la sauvegarde du Content. Bizarre.

2eme test: Content est valide mais pas Image.
Resultat: Aucun message d’erreur n’est affiche mais banner n’a pas ete
sauvegardee (create ou update peu importe).
C’est chiant ! Pourtant dans les logs, on a bien vu que la validation
avait
eu lieu mais cette conne s’est faite apres celle de Content. C’est pas
de
chance. En parcourant l’API, on tombe sur la methode
“validates_associated”
qui semble repondre a nos besoins. Cool, mettons la dans notre modele
Content.

3eme test: Content est valide mais pas Image. “validates_associated”
dans
Content.
Resultat: Youpi ca marche, on a un beau message d’erreur (banner is
invalid). Content n’a pas ete sauvegarde. “So far so good”.
Un petit detail nous gene, en fait on aurait du avoir plusieurs erreurs
pour
la banner or une seule (generale) a ete remontee. Dommage.
En regardant, un peu plus dans les logs, on s’apercoit que la validation
de
banner a ete faite 2 FOIS: une fois par validates_associated et l’autre
fois
avant de sauvegarder banner. C’est nul

Conclusion: la double validation n’est pas genante car l’integrite des
donnees est conservee (heureusement !) mais il est evident que cela
ralentit
le serveur surtout si la validation d’un objet associe prend bcp de
ressource (je pense au traitement d’images par exemple). Il n’y a pas
veritablement de work around (enfin pas a ma connaissance), on peut
bidouiller (ce que j’ai fait) mais la solution reste bancale.

Conclusion Generale: sur le coup, j’avoue avoir ete decu un petit peu
par
RoR (pas par Ruby !). Je suis sur que certains d’entre vous sont tombes
sur
ce genre de probleme. J’aimerai bcp avoir vos avis.

Bonne soiree.

Did

Salut,

Quelques réflexions sur tes “soucis”:

  • Sauvegarde automatique des associations: c’est effectivement pas
    simple de savoir ce qu’il se passe “behind the scene” :wink:
    La meilleure lecture possible est la doc Rails sur les associations:
    Peak Obsession

Tout est expliqué, par contre il faut la lire très attentivement et ne
pas hésiter à regarder le code Rails…

  • Pourrais tu envoyer des exemples de ton code pour te donner des
    conseils + précis?

  • J’utilise volontiers les transactions SQL et les exceptions
    ActiveRecord pour blinder les erreurs de validation. A supposer bien sur
    que ta BDD supporte les transactions :wink:
    Par exemple:

begin
Content.transaction do
content = Content.new( params[:content] )
content.save!

banner = Image.new( params[:banner] )
banner.content = content
banner.save!

end
rescue ActiveRecord::ActiveRecordError => err

Exception

détectéeend

En espérant que ca rende service…

Simon

pas hésiter à regarder le code Rails…
Tout a fait ! En etudiant, le code j’ai trouve la parade. Dans le plugin
que
je developpe, j’ai ajoute ces lignes de code dans un de mes fichiers (le
code est le meme que pour has_one() sauf que j’ai remplace
association.save(true)
par association.save(false). Ca marche nickel !

module ActiveRecord
module Associations
module ClassMethods
def has_one_without_automatic_save(association_id, options = {})

association.save(false)

end
end
end

  • Pourrais tu envoyer des exemples de ton code pour te donner des

conseils + précis?

C’est gentil ! Malheureusement le code qui ne “marche” pas, je le jette
!
Donc je n’ai plus de traces.

  • J’utilise volontiers les transactions SQL et les exceptions

ActiveRecord pour blinder les erreurs de validation. A supposer bien sur
que ta BDD supporte les transactions :wink:

Ca, c’est interessant, mais je serais curieux de savoir si tu n’as pas
une
double sauvegarde de l’objet banner (genre un create, suivi d’un update
HORS
transaction). Car meme si tu “override” la methode save de l’objet
Content,
banner sera sauve hors de la methode save. Qu’en penses tu ?

Sinon super sympa tes reponses Simon.
a+

Did

D’après la doc API : "If you wish to assign an object to a has_one
association
without saving it, use the #association.build method "

“Bien vu dit l’aveugle !”. Sauf que c’est la description de la methode
n’est
pas tout a fait claire. Effectivement, l’appel de cette methode ne
sauvegardera pas l’objet associe (contrairement a #association.create)
MAIS
si tu sauvegardes l’objet parent, l’objet associe le sera lui aussi.
Sinon
j’ai pas tente le truc, il se peut qu’en faisaint un object_associe
(dans
mon cas: banner) = nil, on rend inactive la sauvegarde de l’objet. A
tester

Sinon, je te remercie pour ta solution sur le probleme “n validations”,
c’est clairement ce que j’avais fait dans un premier temps. Sauf
qu’entre
temps, j’ai vu qu’il etait assez facile de rajouter une “pseudo
association”
grace a l’extension de classes par des modules. Du coup, j’ai pu
supprimer
la validation lors du aftershave (cf mail precedent ;-))).

Ah oui, evidemment j’ai rien contre Rails, j’adore ce framework. Tu
comprendras, Francois, qu’apres avoir passe plusieurs heures a jouer a
Google et “eplucher-les-sources-avec-vi”, j’etais un peu a cran. Mais je
te
promets, la prochaine fois, je prendrai sur moi, j’interioriserai mes
souffrances et je ne cracherai pas mon venin sur RoR.
C’est malin, maintenant, je regrette mon premier mail. Arggh c’est dur
les
remords.

Bon WE a tous

Did

Didier :

[…]

class Content < ActiveRecord::Base
has_one :banner, :class_name => ‘Image’
has_one :vignette, :class_name => ‘Image’
end

[…]

  • Sauvegarde des associations

Genial avec RoR, les associations sont sauvegardees automatiquement
que cela soit des create ou des update. Dans 90% des cas, c’est mega
pratique mais bizarrement aucune option n’a ete developpee pour eviter la
sauvegarde automatique. J’ai eu beau cherche avec mon ami google,
personne n’a de soluce ou semble se poser la question.

D’après la doc API : "If you wish to assign an object to a has_one
association
without saving it, use the #association.build method "

  • Validation des associations

Ce qui est interessant avec les loggers, c’est que l’on peut voir comment
se comporte RoR.

Tout à fait. Et les sources aussi.

Et j’ai trouve un truc vachement interessant. Reprenons mon
premier exemple et virons la deuxieme association par exemple.
Imaginons que le model Image possede une validation tres poussee
(genre calcul pour savoir si les dimensions sont exactes)
On cree ensuite un controller (+ action) avec la vue pour la creation a la
fois d’un Content et de la banner associee (donc de type image).

1er test: Content et Image sont valides.
Resultat: Pas de probleme, le tout est sauvegarde. No souci.

comme dirait Ophélie Winter.

On remarque juste dans les logs, que la validation de la banner s’est faite
APRES la sauvegarde du Content. Bizarre.

Tout à fait. Si on regarde les sources, AR::B.has_one définit un
aftershave…
oups pardon un after_save.

D’après ma compréhension des choses, supposons que content, instance
de Content soit un nouvel enregistrement. Il n’a pas d’id, on le valide
et on le sauve. Donc il a maintenant un id, donc on peut valider et
sauver
banner, instance de Banner en chosissant l’id de content comme clé
étrangère.

Supposons qu’ActiveRecord fasse le contraire. Donc on valide et sauve
banner, il a NULL comme clé étrangère car content n’a pas d’id. On
valide
et sauve content, maintenant il a un id. Donc on peut updater banner
avec l’id de content, donc on revalide et on resauve.

Qu’est-ce qu’est le mieux ?

2eme test: Content est valide mais pas Image.
Resultat: Aucun message d’erreur n’est affiche mais banner n’a pas ete
sauvegardee (create ou update peu importe).
C’est chiant ! Pourtant dans les logs, on a bien vu que la validation avait
eu lieu mais cette conne

Oh là ! comment tu parles à ma soeur ??
Viens avec moi, on va se filer, tête à tête je vais te fumer derrière
les cyprès, je te bousille, tu te rhabilles et moi je danse le…

euh, je crois que je m’égare.

En regardant, un peu plus dans les logs, on s’apercoit que la validation de
banner a ete faite 2 FOIS: une fois par validates_associated

oui, par un banner.valid? en gros

et l’autre fois avant de sauvegarder banner. C’est nul

Ouais ! C’est nul Rails, c’est de la m***, allez, venez tous avec moi,
on faire du Seaside !

Bon, voyons voir.

Conclusion: la double validation n’est pas genante car l’integrite des
donnees est conservee (heureusement !) mais il est evident que cela ralentit
le serveur surtout si la validation d’un objet associe prend bcp de
ressource (je pense au traitement d’images par exemple). Il n’y a pas
veritablement de work around (enfin pas a ma connaissance), on peut
bidouiller (ce que j’ai fait) mais la solution reste bancale.

Je te propose quelque chose, tu trouves ça bancale ou pas, t’en fais
ce que tu veux.

On peut voir le problème de deux façons. Soit il y a 2 validations
et chaque fois un gros traitement, donc il faut faire en sorte qu’il n’y
ait
qu’une seule validation, honnêtement à chaud, comme ça, je sais pas
si AR est patchable à ce niveau-là , pas trop réfléchi sur la manière
d’éviter cela.

Soit, on se dit, est-ce qu’on peut faire un sorte que le gros traitement
soit fait qu’une seule fois, même s’il y a 2, 3… n validations ?
Voyons voir comment on pourrait faire. On peut définir 2 variables
d’instance
dans la classe Image, @exact_width et @exact_height qui recueillent
les résultats des méthodes respectivement #calculate_exact_width,
#calculate_exact_height. Là j’ai supposé que les calculs étaient
distincts, mais on peut aussi définir @exact_size comme un tableau
à deux éléments équivalent à [@exact_width, @exact_height], résultat
de #calculate_exact_size. Les deux variables d’instance serviront
de cache au calcul en quelque sorte. Tant qu’Ã faire on peut faire en
sorte
que ce soit leur initialisation soit la plus paresseuse possible.

class Image < AR::B
def exact_width
@exact_width ||= calculate_exacte_width
end

end

(pareil pour l’autre accesseur)

Je suppose qu’il y a les attributs dans Image, width, height et
file_path
(le chemin où l’image est stockée car je suppose aussi qu’elle
n’est pas sauvée dans la base)

Pour la validation avant ton save, t’auras une condition du genre :

errors.add(…) unless width == exact_width && height == exact_height

Donc, supposons qu’il y ait 2 validations, la première fois, exact_width
et exact_height seront calculés, mais pas la seconde fois. Ni
éventuellement
les autres fois.

Maintenant, il faut faire en sorte que les valeurs de @exact_width et
@exact_height soient cohérents avec l’image choisie.
Supposons que l’attribut file_path soit modifié, alors il faut faire en
sorte
que les @exact_* ne conservent plus les anciennes valeurs.

quelque chose dans le genre :

def file_path=(path)
write_attribute(‘file_path’, path)
@exact_width = @exact_height = nil
end

Je disais qu’il fallait que l’initialisation soit la plus paresseuse
possible,
on aurait pu faire :

def file_path=(path)
write_attribute(‘file_path’, path)
@exact_width = calculate_exacte_width
@exact_heigth = calculate_exacte_heigth
end

Mais on ne le fait pas pour éviter des calculs inutiles, par exemple
si on avait ça :

my_image.file_path = ‘foo’
my_image.file_path = ‘bar’

Donc maintenant, on a d’une certaine manière dissocié le gros traitement
de la validation. Quand bien même, il y aurait n validations, le calcul
n’aurait été effectué qu’une fois.

100% garanti non testé :slight_smile:
Mais ça me paraît pas mal et pas trop bidouille.

Conclusion Generale: sur le coup, j’avoue avoir ete decu un petit
peu par RoR (pas par Ruby !).

Conclusion : oui, Rails peut décevoir, le framework n’est pas parfait
mais de là à le laisser tomber comme sa première copine, il y a
une marge.

On trouve que Rails a des défauts :

  • soit on arrive à les corriger (ou on soumet un ticket)
  • soit on arrive à faire avec,
  • si c’est un no-no, alors on peut voir si l’herbe est plus verte
    ailleurs…

Mais si on prend un papier et on fait 2 colonnes satisfactions et
inconvénients,
on peut estimer personnellement la longueur de chaque colonne
et prendre sa décision…

est-ce que dans une colonne, il n’y aurait pas plus de place sur le
papier ? :slight_smile:

Je suis sur que certains d’entre vous sont tombes sur ce genre de
probleme. J’aimerai bcp avoir vos avis.

Moi mon avis c’est que nous sommes tous ensemble ce soir pour
une soirée de bonheur musical. Il y a de nombreux super cadeaux
pour les heureux gagnants, il y aura les t-shirts Marlboro, les
autocollants Pioneer, des caleçons des peluches. À la technique,
c’est Momo…

mais je m’égare.

РJean-Fran̤ois.

didier lafforgue wrote:

Ca, c’est interessant, mais je serais curieux de savoir si tu n’as pas
une double sauvegarde de l’objet banner (genre un create, suivi d’un
update HORS transaction). Car meme si tu “override” la methode save de
l’objet Content, banner sera sauve hors de la methode save. Qu’en
penses tu ?
Dans le cas d’un create “from scratch” tel que montré dans le code
exemple, j’ai bien un seul INSERT pour l’objet content et un autre
INSERT pour banner:

content = Content.new
content.save # sauve l’objet content uniquement car pas d’objet banner
initialisé
banner = Banner.new
banner.content = content
banner.save # sauve l’objet banner et en particulier la colonne
content_id

Par contre pour un update, il y a juste besoin de faire un save sur
l’objet content. Comme tu l’as noté, les objets associés sont sauvés
automatiquement à partir du moment où les associations ont été chargées:

content = Content.find(:first)
content.save # sauve l’objet content uniquement

content = Content.find(:first)
content.banner
content.save # sauve l’objet content et l’objet content.banner

content = Content.find(:first, :include => :banner)
content.save # sauve l’objet content et l’objet content.banner

Par contre, bien penser à utiliser “validates_associated” dans le modèle
Content ou a faire “content.valid?” avant le save de content… Mais
tout çà, tu le sais déjà :slight_smile:

Merci d’avoir lancer le thread, ça m’a fait réviser les associations :wink:

Simon