Jointure sqlite

Hello !
J aie beau chercher, je ne comprends pas ce que je fais de faux dans ce
cas basique: une jointure avec 2 tables:

def self.jointure
conditions=“table1.id = table2.id”
my_results=self.find(:all, :include=>‘table2’, :conditions=>
[conditions])
result_details = my_results[0]
for res in result_details.table2 do
logger.debug “nb hits #{res.nb_hits}”
end
return my_results
end

Les données:
table1:
id name
1 toto
table2:
id nb_hits log_date
1 10 2009/01/01
1 16 2009/01/02

Lors dela jointure, les logs me sortent 2 lignes , jusque la ça va, mais
c est 2 fois la même :frowning:
nb hits 16
nb hits 16

Je craque un peu , a l aide !
Renaud

Hou que c’est pas beau, un champs “id” qui peut avoir plusieurs fois la
même
valeur.

Ce serait mieux de l’appeler “table1_id” histoire de montrer que c’est
l’id
d’un table1 (quoi qu’un table1 puisse être). D’ailleurs, vu que les
table1s
on un name, c’est peut-être des people (singulier : person), et ce
serait
mieux d’appeller la table people du coup, et l’objet modèle Person
semblera
tout naturel. En plus, comme les table2s sont apparemment des
évènements, on
pourrait les appeler Event, et ça ferait une table nommée events. On
progresse. Du coup, on pourrait même écrire ça :

Dans app/models/person.rb

class Person
has_many :events
named_scope :with_events, :include => :events
end

Dans app/models/event.rb

class Event
belongs_to :person
named_scope :with_person, :include => :person
end

Du coup tu pourrias écrire des truc fabuleux comme :

Person.with_events.find :all

Et il ferait la jointure tout seul comme un grand.

Mais les conventions sont des trucs chiants qui servent à rien, après
tout
on peut très bien appeller des choses “truc”, “machin”, “toto”, ou
“table1”
et ça marche, non ? Quoi, ça rend le code plus lisible en incitant Ã
déclarer son intention, et ça facilite l’écriture de la suite en
fournissant
des méthodes triviales qui marchent bien ? Ta ta ta, c’est pour les
amateurs
ça. Vaut mieux nommer ses tables table1, table2, etc., comme ça quand on
veut en rajouter une, eh bien on l’appelle tablen+1 et y’a pas besoin de
se
casser la tête. D’ailleurs, on devrait faire la même chose avec les
champs,
les appeller champs1, champs2, champs3, etc., sauf les id pour les
jointures
qu’on appellerait just “id” que ce soit une clef primaire, étrangère,
etc.
C’est tellement plus fun, c’est comme un puzzle, Ã chaque fois on a le
plaisir de retrouver un gros sac de noeuds et on passe des après-midi
super
à tout démêler.

Michel B.

2009/7/13 Renaud H. [email protected]

Merci de ta réponse.
Pour les besoins de l explication, j aie simplifié mon datamodel de 7
tables a 2 avec une simple jointure et gardé seulement 2 collonnes , d
ou les noms bateaux car si j avais gardé ceux a l origine, ca n avait
plus de sens.
Effectivement, dans mon code je n aie pas de tels noms … ca me
paraissait évident mais visiblement ca t a perturbé au plus haut point

Bonjour,
Merci pour ta réponse, c est vrai que mon exemple ne s inscrit pas du
tout dans la philosophie de Rails :frowning:
Je vais essayer avec un named_scope, cela dit, j aie du rater quelque
chose car ca devrait marcher dans mon exemple.
Je crois que je vais re-prendre a 0, vu que aujourd dhui on travaille
pas, j aie tout mon temps :slight_smile:

Non pas que ça me perturbe, juste que je t’encourage à changer pour des
noms
explicites, ça rendra ton code plus maintenable, plus facile à lire, et
ce
même pour un nouveau venu qui n’aurait pas des dons de divination. De
plus,
sur une mailing-list ou un forum, il vaut toujours poser une question
avec
des noms explicites pour que les gens comprennent le contexte d’un
problème
du premier coup d’oeil et te donne de bons conseils en connaissance de
cause.

Ce qui est plus gênant, c’est d’avoir un champs nommé “id” qui est en
fait
une clef étrangère sur une autre table. Ca c’est crado, et c’est marrant
parce que c’est la cause de ton problème justement.

Les conventions de Rails sont là pour faciliter la mise en place de
mécanismes triviaux et répétitifs. Ne pas les respecter pour faire
quelque
chose de trivial c’est vouloir avoir des problèmes, les problèmes que
Rails
résoud naturellement quand on suit les conventions. Après c’est
peut-être un
choix…

Ensuite, si tu en es encore au design de tes modèles, pourquoi ne pas
s’en
tenir aux conventions justement ? Tu n’as rien à y perdre, mais plein Ã
y
gagner. Rails n’est pas PHP (sans framework), tu n’y entre pas plus
facilement en créant tes bases de données “à la cow-boy” et en faisant
des
vues avec du code métier et des bouts de contrôleur…

Michel B.

2009/7/14 Renaud xxxx [email protected]

2 précisions:
SELECT *
FROM users
INNER JOIN logs ON users.id = logs.user_id
WHERE users.name = “truc”

est équivalent a:
SELECT *
FROM users, logs
WHERE users.id = logs.user_id
AND users.name = “truc”

(cf SQL92)
Dans les 2 cas l ordre des tables est donne par la requête.
Par contre la 1ere requête laisse a l optimizer la chance de pouvoir
faire son travail (cf Oracle).
De plus, l ordre n impacte que le cout et non pas les résultats, donc
sans rapport avec le problème.

Merci pour le paragraphe sur les jointures ouvertes / fermées, je
connais assez bien, et si j avais voulu en faire une, je l aurai
précisé.

J aie fini par trouver ce qui causait problème…

C’est équivalent dans les effets, c’est juste plus lisible, plus facile
Ã
retoucher, plus propre, et plus facile à faire évoluer à terme.

De toute façon, find avec include ne fait pas forcément ça. Il fait
peut-être :
SELECT *
FROM users
LEFT JOIN logs ON users.id = logs.user_id

Du coup, quand tu rajoute une condition, ça risque de faire
SELECT *
FROM users
LEFT JOIN logs ON users.id = logs.user_id
WHERE users.id = logs.user_id

Hop, ce qui commence comme une left join devient tout d’un coup dans les
effets une inner join, et exclue donc les users qui n’ont pas de logs Ã
ton
insu.

ActiveRecord fait porter la “responsabilité” complète de la jointure aux
méthodes belong_to, has_one, has_many et has_and_belong_to_many. C’est

que tu met le champs de jointure, et c’est ce sur quoi repose le
“include”
de find. Si tu ne fais pas comme ça dans un cas trivial comme celui-ci,
tu
te prive inutilement d’un des intérêts fondamentaux d’ActiveRecord. Et
dans
le cas que tu as présenté, je ne vois pas de bonne raison pour faire ça.

Ensuite, appeller un champs id quand ce n’est pas la clef primaire de la
table, là c’est vraiment pas propre, et c’est très probablement ça qui
causait ton bug. Ca ne te coûte rien de l’appeller user_id, au contraire
ça
te permet de faire des clauses rails de jointure sans paramètre
supplémentaire pour préciser quelle est la clef étrangère, et ça t’évite
aussi d’indiquer à la table logs que le champs “id” n’est pas en réalité
la
clef primaire.

Enfin, c’est une question de paradigme. Un langage de programmation de
haut
niveau comme Ruby et Rails sert à exprimer dans le code d’une façon
simple,
lisible, réutilisable. Rien ne t’empêche de faire des choses illisibles,
compliquées inutilement et de réinventer la roue si ça te chante. Mon
conseil, c’est de faire simple, lisible et réutilisable, même quand tu
écris
du SQL à la mano, même si tu es un super cador du SQL. De même je n’ai
pas
tellement envie de repasser derrière un super-crack du code qui
appellerait
toutes ces variables a, b, c, d, e, f, … et qui ne commenterais rien.
L’idée, c’est de communiquer l’idée, pas seulement à la machine, mais
aussi
aux autres humains (et à toi-même 3 mois, 6 moins, 5 ans plus tard quand
tu
rouvre le capot pour voir ce qu’il y avait dedans). Ruby est inventé sur
ce
concept, les sucres syntaxiques servent à rendre les choses plus claires
et
plus lisibles, les conventions de Rails aussi. Tu peux les ignorer pour
répondre à un besoin particulier, mais les respecter te feras aller plus
vite dans les cas triviaux.

Michel B.

Avant même de faire des named_scope (ce qui est bien) ce serait bien de
comprendre pourquoi ta jointure ne marche pas. Et c’est assez simple, en
fait. En SQL, une jointure “propre” c’est :
SELECT *
FROM users
INNER JOIN logs ON users.id = logs.user_id
WHERE users.name = “truc”

Quelle étrange chose, qu’est-ce donc que cet “INNER JOIN table ON
condition”
? He ben c’est une jointure justement, mais écrite proprement, c’est Ã
dire
en exprimant :

  • quelle est la table primaire, celle “d’où l’on part” : FROM users
  • quelle est la table jointe, celle que l’on recolle : INNER JOIN
    logs
  • quelle est la clause de jointure (la condition qui permet de
    restreindre le produit cartésien résultant) : ON users.id =
    logs.user_id

Note au passage que la convention Rails, c’est de nommer une clef
étrangère
{nom_de_la_table_etrangere}_id, sinon tes jointures sont plus
compliquées Ã
faire. Tu n’as pas besoin de mettre un id à la table logs aussi, mais Ã
ta
place je le ferais, ça ne te coûte rien mais ça pourrait servir. Autre
petite notion de SQL, si tu faisais un INNER JOIN, il ne te trouverait
que
les users qui ont au minimum un log. Pour trouver les users même s’ils
n’ont
pas de log, il faudrait faire LEFT JOIN … ON Ã la place de INNER JOIN

ON.

Maintenant, des notions de Rails :

class User
has_many :logs
end

En supposant que tu as nommé la clef étrangère de la table log user_id,
ça
te donne plein de méthodes magiques, comme User.find(1).logs par
exemple,
qui va chercher les logs de cet user. Mieux encore, ça permet de faire
de
l’eager-loading :

Users.find(1, ;include => ;logs)

Et il va faire tout seul comme un grand la jointure, et te retourner le
résultat avec une seule requête d’effectué. Note au passage qu’il n’est
même
pas la peine d’exprimer la condition de la jointure, Rails va
directement
comprendre que la condition de jointure est users.id = logs.user_id
parce
que tu auras cette fois respecté les conventions. Respecter les
conventions
de Rails n’est pas strictement obligatoire, mais les ignorer tout le
temps
c’est comme utiliser un marteau pour visser des vis, tu peux y arriver,
mais
tu ferais mieux d’utiliser un tourne-vis quand même.

A l’occasion, faire un petit tour dans la documentation d’ActiveRecords
ne
te ferais pas de mal, soit dit en passant. Tout ce que je viens de
t’expliquer ici est plutôt trivial…

Michel B.