Prenons une simple association ...
class Person
has_many :friends
end
class Friend
belongs_to :person
end
Quelle est la manière la plus propre d'obtenir toutes les personnes qui n'ont AUCUN ami dans ARel et / ou meta_where?
Et puis qu'en est-il d'un has_many: through version
class Person
has_many :contacts
has_many :friends, :through => :contacts, :uniq => true
end
class Friend
has_many :contacts
has_many :people, :through => :contacts, :uniq => true
end
class Contact
belongs_to :friend
belongs_to :person
end
Je ne veux vraiment pas utiliser counter_cache - et d'après ce que j'ai lu, cela ne fonctionne pas avec has_many: through
Je ne veux pas extraire tous les enregistrements person.friends et les parcourir en boucle dans Ruby - je veux avoir une requête / portée que je peux utiliser avec le gem meta_search
Le coût des performances des requêtes ne me dérange pas
Et plus vous vous éloignez du SQL réel, mieux c'est ...
ruby-on-rails
arel
meta-where
craic.com
la source
la source
DISTINCT
. Sinon, je pense que vous voudriez normaliser les données et l'index dans ce cas. Je pourrais le faire en créant unfriend_ids
hstore ou une colonne sérialisée. Alors vous pourriez direPerson.where(friend_ids: nil)
not exists (select person_id from friends where person_id = person.id)
(Ou peutpeople.id
- être oupersons.id
, en fonction de votre table.) Je ne sais pas quel est le plus rapide dans une situation particulière, mais dans le passé, cela a bien fonctionné pour moi lorsque je n'essayait pas d'utiliser ActiveRecord.Mieux:
Pour le hmt, c'est fondamentalement la même chose, vous comptez sur le fait qu'une personne sans amis n'aura pas non plus de contacts:
Mettre à jour
Vous avez une question à propos
has_one
des commentaires, donc juste une mise à jour. L'astuce ici est qu'ilincludes()
attend le nom de l'association maiswhere
attend le nom de la table. Pour un,has_one
l'association sera généralement exprimée au singulier, de sorte que cela change, mais lawhere()
partie reste telle qu'elle est. Donc, siPerson
seulementhas_one :contact
alors votre déclaration serait:Mise à jour 2
Quelqu'un a demandé l'inverse, des amis sans personne. Comme je l'ai commenté ci-dessous, cela m'a fait réaliser que le dernier champ (ci-dessus: le
:person_id
) n'a pas à être lié au modèle que vous renvoyez, il doit simplement s'agir d'un champ dans la table de jointure. Ils vont tous l'être,nil
donc ça peut être n'importe lequel d'entre eux. Cela conduit à une solution plus simple à ce qui précède:Et puis changer cela pour renvoyer les amis sans personne devient encore plus simple, vous ne changez que la classe à l'avant:
Mise à jour 3 - Rails 5
Merci à @Anson pour l'excellente solution Rails 5 (donnez-lui quelques + 1 pour sa réponse ci-dessous), vous pouvez utiliser
left_outer_joins
pour éviter de charger l'association:Je l'ai inclus ici pour que les gens le trouvent, mais il mérite les + 1 pour cela. Excellent ajout!
Mise à jour 4 - Rails 6.1
Merci à Tim Park d' avoir souligné que dans la prochaine version 6.1, vous pouvez le faire:
Merci aussi au post auquel il a lié.
la source
has_one
association, vous devez changer le nom de l'association dans l'includes
appel. Donc, en supposant qu'il était à l'has_one :contact
intérieur,Person
votre code seraitPerson.includes(:contact).where( :contacts => { :person_id => nil } )
self.table_name = "custom_friends_table_name"
), utilisezPerson.includes(:friends).where(:custom_friends_table_name => {:id => nil})
.missing
méthode pour faire exactement cela !smathy a une bonne réponse Rails 3.
Pour Rails 5 , vous pouvez utiliser
left_outer_joins
pour éviter de charger l'association.Consultez la documentation de l' API . Il a été introduit dans la demande d'extraction # 12071 .
la source
.includes
un coût supplémentaire en temps de chargement ne serait pas quelque chose que je craindrais beaucoup d'optimiser. Votre cas d'utilisation peut être différent.Person.joins('LEFT JOIN contacts ON contacts.person_id = persons.id').where('contacts.id IS NULL')
cela fonctionne aussi très bien comme lunette. Je fais cela tout le temps dans mes projets Rails.includes
, tous ces objets AR sont chargés en mémoire, ce qui peut être une mauvaise chose car les tables deviennent de plus en plus grandes. Si vous n'avez pas besoin d'accéder à l'enregistrement de contact, leleft_outer_joins
ne charge pas le contact en mémoire. La vitesse de requête SQL est la même, mais l'avantage global de l'application est beaucoup plus important.Person.where(contacts: nil)
ouPerson.with(contact: contact)
si l'utilisation de where empiète trop loin dans la `` properness '' - mais étant donné ce contact: est déjà analysé et identifié comme une association, il semble logique qu'arel puisse facilement déterminer ce qui est nécessaire ...Les personnes qui n'ont pas d'amis
Ou qui ont au moins un ami
Vous pouvez le faire avec Arel en configurant des étendues sur
Friend
Et puis, les personnes qui ont au moins un ami:
Les sans amis:
la source
DEPRECATION WARNING: It looks like you are eager loading table(s)
Currently, Active Record recognizes the table in the string, and knows to JOIN the comments table to the query, rather than loading comments in a separate query. However, doing this without writing a full-blown SQL parser is inherently flawed. Since we don't want to write an SQL parser, we are removing this functionality. From now on, you must explicitly tell Active Record when you are referencing a table from a string
Les réponses de dmarkow et d'Unixmonkey m'apportent ce dont j'ai besoin - Merci!
J'ai essayé les deux dans ma vraie application et j'ai obtenu des horaires pour eux - Voici les deux champs d'application:
Ran this avec une vraie application - petite table avec ~ 700 enregistrements 'Person' - moyenne de 5 essais
L'approche d'Unixmonkey (
:without_friends_v1
) 813ms / requêteApproche de dmarkow (
:without_friends_v2
) 891ms / requête (~ 10% plus lent)Mais ensuite, je me suis rendu compte que je n'avais pas besoin de l'appel pour
DISTINCT()...
rechercher desPerson
enregistrements avec NONContacts
- ils doivent donc simplement êtreNOT IN
la liste des contactsperson_ids
. J'ai donc essayé cette portée:Cela donne le même résultat mais avec une moyenne de 425 ms / appel - presque la moitié du temps ...
Maintenant, vous pourriez avoir besoin du
DISTINCT
dans d'autres requêtes similaires - mais pour mon cas, cela semble fonctionner correctement.Merci de votre aide
la source
Malheureusement, vous êtes probablement à la recherche d'une solution impliquant SQL, mais vous pouvez la définir dans une portée et ensuite simplement utiliser cette portée:
Ensuite, pour les obtenir, vous pouvez simplement le faire
Person.without_friends
, et vous pouvez également l'enchaîner avec d'autres méthodes Arel:Person.without_friends.order("name").limit(10)
la source
Une sous-requête corrélée NOT EXISTS doit être rapide, en particulier lorsque le nombre de lignes et le ratio d'enregistrements enfants / parents augmentent.
la source
Aussi, pour filtrer par un ami par exemple:
la source