Closed svergeylen closed 6 years ago
Acutellement : les items n'apparaissent pas car les items ne possèdent pas les tags "thèmes" et "science-fiction" :
Idéalement, il faut arriver à obtenir cette liste d'item (ici, j'ai enlevé manuellement les deux tags gris) :
Coucou,
Je ferais quelque chose comme ceci:
def self.having_tags(ar_tags)
Item.includes(:tags).where(tags: {name: ar_tags, filter_items: false})
end
(si ar_tags
contient bien les names des tags) :smiley:
Non, cela renvoie malheureusement aucun item en réponse... (avec ou sans les tags filtrant, j'ai donc essayé rien qu'avec un seul tag pour voir)
Started GET "/tags/785" for 127.0.0.1 at 2018-09-13 15:06:48 +0200
Processing by TagsController#show as HTML
Parameters: {"id"=>"785"}
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ? [["id", 2], ["LIMIT", 1]]
Tag Load (0.1ms) SELECT "tags".* FROM "tags" WHERE "tags"."id" = ? LIMIT ? [["id", 785], ["LIMIT", 1]]
Tag Load (0.2ms) SELECT "tags".* FROM "tags" WHERE "tags"."id" IN (776, 778, 779, 785)
Tag Exists (0.2ms) SELECT 1 AS one FROM "tags" INNER JOIN "ownertags" ON "tags"."id" = "ownertags"."tag_id" WHERE "ownertags"."owner_id" = ? AND "ownertags"."owner_type" = ? LIMIT ? [["owner_id", 785], ["owner_type", "Tag"], ["LIMIT", 1]]
Rendering tags/show.html.erb within layouts/application
Rendered search/_form_tags.html.erb (0.9ms)
Tag Load (0.3ms) SELECT "tags".* FROM "tags" INNER JOIN "ownertags" ON "tags"."id" = "ownertags"."tag_id" WHERE "ownertags"."owner_id" = ? AND "ownertags"."owner_type" = ? ORDER BY "tags"."name" ASC [["owner_id", 785], ["owner_type", "Tag"]]
SQL (4.1ms) SELECT "items"."id" AS t0_r0, "items"."name" AS t0_r1, "items"."series_id" AS t0_r2, "items"."created_at" AS t0_r3, "items"."updated_at" AS t0_r4, "items"."adder_id" AS t0_r5, "items"."number" AS t0_r6, "items"."description" AS t0_r7, "items"."rails_view" AS t0_r8, "tags"."id" AS t1_r0, "tags"."name" AS t1_r1, "tags"."root_tag" AS t1_r2, "tags"."default_view" AS t1_r3, "tags"."letter" AS t1_r4, "tags"."view_alphabet" AS t1_r5, "tags"."filter_items" AS t1_r6 FROM "items" LEFT OUTER JOIN "ownertags" ON "ownertags"."owner_id" = "items"."id" AND "ownertags"."owner_type" = ? LEFT OUTER JOIN "tags" ON "tags"."id" = "ownertags"."tag_id" WHERE "tags"."name" IN ('776', '778', '779', '785') AND "tags"."filter_items" = ? [["owner_type", "Item"], ["filter_items", "f"]]
Rendered tags/show.html.erb within layouts/application (12.6ms)
Rendered shared/_debugging.html.erb (0.7ms)
Completed 200 OK in 67ms (Views: 56.4ms | ActiveRecord: 5.2ms)
Ce serait bien que cette requete fonctionne, parce que c'est vraiment l'essence de l'idée des tags que tu avais proposée.... ("sélection de plusieurs tags et affichage des items qui y sont reliés, sauf pour certains tags qui ne servent qu'à la navigation" ;-) )
Hum...
C'est bizarre, car en testant ça fonctione:
cd /tmp
git clone https://github.com/svergeylen/collector.git
cd collector/
bundle install
bundle exec rake db:setup
rails console
# Creating Tags: a,b,c,d,e
a = Tag.create(name: "a", root_tag: true, filter_items:false)
b = Tag.create(name: "b", root_tag: false, filter_items:true)
c = Tag.create(name: "c", root_tag: false, filter_items:true)
d = Tag.create(name: "d", root_tag: false, filter_items:false)
# Associating Tags: a → b → c → d (optional)
a.tags = [b]
b.tags = [c]
c.tags = [d]
# Creating Item
item = Item.create(name: "Bonjour", adder_id: User.first.id)
item.tags = [a,d] # Tags non being 'filter_items'
# Query
ar_tags= ["a", "d"] # Tags names, not ids
Item.includes(:tags).where(tags: {name: ar_tags, filter_items: false})
# => #<ActiveRecord::Relation [#<Item id: 1, name: "Bonjour", series_id: nil, created_at: "2018-09-14 08:00:52", updated_at: "2018-09-14 08:00:52", adder_id: 1, number: nil, description: nil, rails_view: "general">]>
Même en ajoutant d'autres tags à Item
, ça ne perturbe pas l'output. Peux-tu reproduire et me faire un retour?
Alalaa, mais je te demande si ar_tags
contient des name
ou pas...
(si ar_tags contient bien les names des tags) :smiley:
Dans app/controllers/tags_controller.rb > show, tu utilises session[:active_tags]
qui semble stocker des ids, donc la requête est juste modifiée par id:
au lieu de name:
:
def self.having_tags(ar_tags)
Item.includes(:tags).where(tags: {id: ar_tags, filter_items: false})
end
En daar is de werk!
ok, je vais regarder (stp laisse ouvert les taches sinon je pense qu'il n'y a plus d'action à faire dans le code, ce n'est pas un projet public donc on peut surement s'écarter de la stricte utilisation de github)
Voilà, comme tu me le suggères, je regarde plus en détail les points sur lesquels tu m'aides, au lieu de copier coller ...
La solution a ce problème est la suivante :
applicable_tag_ids = Tag.where(id: ar_tags).where(filter_items: true).pluck(:id)
ownertags = Ownertag.where(tag_id: applicable_tag_ids, owner_type: "Item").group(:owner_id).count.select{|owner_id, value| value >= applicable_tag_ids.size }
Item.where(id: ownertags.keys)
Malheureusement, la solution plus courte que tu donnes en fermant l'issue ne fonctionne pas, pour la raison suivante :
tags: {id: ar_tags}
renvoie tous les ownertags qui contiennent n'importe lequel des tag_ids donnés (OR) , et ne renvoie pas les ownertags qui contiennent tous les tag_ids donnés (AND).
Comme toutes les BD ont le tag "BD", cette requête renvoie toutes les bd d'office, peu importe les autre tag_ids qui sont donnés dans l'array et n'est donc pas directement utilisable. (>2700 records à chaque requête)
A moins de pouvoir modifier cette requête courte pour qu'elle sélectionne des records qui possèdent tous les tag_ids donnés (ce qui me parait impossible en une seule étape vu que le SQL ne peut faire des AND que sur des attributs d'une même ligne et pas entre différentes lignes), il faut conserver la requête originale que j'ai simplement modifiée en enlevant les tag_ids qui ne sont pas filtrants (filter_items: false). La seconde ligne n'utilise donc que les tags filtrants dans la requete.
Voilà, je n'essaie pas de faire le malin, mais simplement te dire que la requête donnée ne fonctionnait pas et ce n'est pas grave. On peut probablement encore la simplifier, mais cela fonctionne a priori :
Avec le tag série "non filtrant", tous les items s'affichent (identique avec ou sans le tag "séries", en fait, ce qui est logique) :
Ok je comprends!
Effectivement, les Tags généraux comme BD vont renvoyer plein de résultats, bien vu. Ceci n'était pas visible dans mon banc de test.
Dans ce cas je propose soit de le faire en trois requêtes comme tu fais, soit directement sur base de OwnerTag (qui contient toutes les infos, tu peux encapsuler la restriction des Tags dans la requête ownertag) et là deux requêtes suffisent:
Impossible de réduire à une requête parce que Owner est polymorphique (et Rails renvoie ActiveRecord::EagerLoadPolymorphicError
), autrement ça marcherait !
def self.having_tags(ar_tags)
item_ids = Ownertag.joins(:tag).where({tags: {filter_items: false, id: ar_tags}}).where(owner_type: "Item").group_by(&:owner_id).select{|item_id, tags| tags.size == ar_tags.size}.keys
Item.where(id: item_ids)
end
Attention c'est bien joins(:tag)
(singulier) mais .where({tags: {...}})
(pluriel), parce que le premier donne l'indication à Rails de comment est nommée l'association (ici belongs_to
donc tag au singulier) et le deuxième la syntaxe des inner joins, toujours au pluriel dans SQL (ceci est visible dans la requête faite par Rails).
A mon sens on ne sait effectivement pas faire mieux à ce stade :smiley:
Pourrait être d'intérêt: Ajouter un scope aux Ownertag
pour filtrer sur ceux ayant un owner item ou un owner tag (toujours utile en association polymorphique):
# Dans app/models/ownertag.rb
scope :where_owner_is_an_item, -> {where owner_type: "Item"}
scope :where_owner_is_a_tag, -> {where owner_type: "Tag"}
# Et les requêtes deviennent:
Ownertag.where_owner_is_an_item.joins(:tag).where(...)
J'ai tenté mais cela ne renvoie aucun item de mon coté. Voici la requete :
Started GET "/tags/798" for 127.0.0.1 at 2018-09-21 11:51:11 +0200
Processing by TagsController#show as HTML
Parameters: {"id"=>"798"}
User Load (1.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ? [["id", 2], ["LIMIT", 1]]
Tag Load (0.1ms) SELECT "tags".* FROM "tags" WHERE "tags"."id" = ? LIMIT ? [["id", 798], ["LIMIT", 1]]
Tag Load (0.2ms) SELECT "tags".* FROM "tags" WHERE "tags"."id" IN (789, 797, 798)
Tag Load (0.2ms) SELECT "tags".* FROM "tags" INNER JOIN "ownertags" ON "tags"."id" = "ownertags"."tag_id" WHERE "ownertags"."owner_id" = ? AND "ownertags"."owner_type" = ? [["owner_id", 798], ["owner_type", "Tag"]]
Ownertag Load (9.0ms) SELECT "ownertags".* FROM "ownertags" INNER JOIN "tags" ON "tags"."id" = "ownertags"."tag_id" WHERE "tags"."filter_items" = ? AND "tags"."id" IN (789, 797, 798) AND "ownertags"."owner_type" = ? [["filter_items", "t"], ["owner_type", "Item"]]
Rendering tags/show.html.erb within layouts/application
Rendered search/_form_tags.html.erb (1.2ms)
Item Load (0.2ms) SELECT "items".* FROM "items" WHERE 1=0
Rendered tags/show.html.erb within layouts/application (6.7ms)
Rendered shared/_debugging.html.erb (0.8ms)
Completed 200 OK in 166ms (Views: 49.1ms | ActiveRecord: 15.4ms)
J'ai tenté avec true au lieu de false, parce que filter_items = true pour les tags filtrants
Du coup, avec un peu de commentaires, ca donnerait ceci au final :
# On sélectionne dans les tags donnés uniquement ceux qui doivent filtrer les items
applicable_tag_ids = Tag.where(id: ar_tags).where(filter_items: true).pluck(:id)
# On sélectionne les items qui correspondent à ces tags filtrants en comptant si chaque item est repris autant de fois que le nombre de tags filtrants donné
# Si il y a deux tags filtrants donnés, il faut que ownertags contiennent 2 lignes pour cet item (une ligne pour chaque tag différent)
ownertags = Ownertag.where(tag_id: applicable_tag_ids, owner_type: "Item").group(:owner_id).count.select{|owner_id, value| value >= applicable_tag_ids.size }
# On charge les items correspondants aux lignes trouvées dans ownertags
Item.where(id: ownertags.keys)
Il faut effectivement mettre true
et non false
. Je ne parviens pas à reproduire le problème, en dumpant data.yml de la DB en production ça renvoie bien des Items, mais je ne parviens pas à reproduire ton exemple car le Tag "Bandes dessinées" n'existe pas pour le moment semble-t-il.
Si tu savais me lister des fixtures pour reproduire, ça serait cool. :wink:
Entretemps, en prenant les tags du premier Item en DB production :
ar_tags = Item.first.tag_ids
# => [1, 4]
item_ids = Ownertag.joins(:tag).where({tags: {filter_items: true, id: ar_tags}}).where(owner_type: "Item").group_by(&:owner_id).select{|item_id, tags| tags.size == ar_tags.size}.keys
# => [1] → Correct, l'Item.first.id est bien 1
Item.where(id: item_ids)
# => #<ActiveRecord::Relation [#<Item id: 1, name: "La Magicienne trahie", series_id: 2, created_at: "2007-12-24 08:51:55", updated_at: "2017-10-23 16:11:31", adder_id: nil, number: 1.0, description: nil, rails_view: "general">]>
Loupe-je un truc? 😇
Edit : sinon c'est déjà bien suffisant il me semble hein (mais juste pr savoir)
COucou, Pour les fixtures, rien de plus simple, il suffit d'importer la database :
rake db:reset
rake db:data:load
Après, il faut migrer toutes les données vers les tags avec les taches rake db:convert_xxxxx ..
Mais si tu veux, je peux recréer un nouveau fichier data.yml directement au bon format avec les tags et tout et le commit dans le prochain commit... comme ca, tu n'auras qu'a faire les 2 lignes de codes ci-dessous... Ca te tentes ? Si non, je peux essayer de décoder les 3 lignes ci-dessus (mais bon, comme ca marche acutellement, autant se concentrer sur les virtual attributes, non ? cela marche, mais pas avec des tags imbriqués et pas avec selectize.js qui veut absolument un
Voilà, je t'i envoyé par email le fichier data.yml déjà migré en "ownertags", comme ca, tu dois juste faire le reset et le load 😎
Coucou, Tu peux m'aider sur la requête ? Il y a des tags qui filtrent les items (ex : BD, Thorgal, ...) mais aussi des tags qui ne servent "à rien", càd uniquement pour la navigation et la structure des tags... (ex : "thèmes", "séries", ...) Tout est déjà pret niveau database, vue, contrôleur et aussi la représentation graphique en gris clair des tags non filtrants (tag.filter_items = false). Il faut juste modifier la requete suivante :
Model > Items > item.having_tags
de façon à y inclure la condition "tag.filter_items = true"... De cette façon, les items ne seront filtrés qu'avec les tags filtrants, et pas les tags non filtrants :-) Merci :-) Eventuellement refaire entièrement la requete pour améliorer les performances (elle est un peu immonde et illisible)