has_many i wielokrotne zapytanie where

Mam klasę Post która ma has_many comments z polem title typu string
Zalozmy ze mamy w bazie jedna instancję klasy Post oraz dwie instancje klasy Comment które należą do Post i mają title odpowiednio “Foo” i “Bar”
Chcę znaleźć w bazie wszystkie instancje Post które mają komentarze zawierające tytuły “Foo” i “Bar”
Próbowałem:
Post.joins(:comments).where(“comments.title = ?”, “Foo”).where(“comments.title = ?”, “Bar”) # => []

mogę zrobić tak
w post.rb

has_many :comments_foo, conditions: {title: “foo”}, class_name: “Comment”
a potem:

Post.joins(:comments).preload(:comments_foo).where(“comments.title = ?”, "
bar") # => [Post_1]

Problem w tym, że w przyrodzie mam relację i parametry i chce ograniczyć scope w pętli
filters.each do |title|
scope = scope.where(“comments.title = ?”, title)
end
Czy ktoś ma pomysł na rozwiązanie problemu?

“Foo” i “Bar”, czy “Foo” lub “Bar”? Łączenie where w łańcuch w SQL-u tworzy klauzule połączone operatorem AND. Druga sprawa, to czy chcesz wyszukiwać dokładnego dopasowania tytułu, czy wystarczy jego część? Jeśli część, to trzeba użyć operatora LIKE (lub ILIKE). Do tworzenia zaawansowanych zapytań polecam gema Squeel: https://github.com/activerecord-hackery/squeel

Foo i Bar, dokładnie dopasowany tytuł

Dzięki za odpowiedź.
Eleganckie rozwiązanie dla stałej ilości parametrów(title). Gorzej będzie się generować kiedy nie znamy z góry ile aliasów będziemy potrzebować. Przeglądam dokumentację Squeel’a, może tam będzie taka funkcjonalność

Post.joins(:comments).where(“comments.title IN (?)”, [“Foo”, “Bar”])

Wydaje mi się, że rozwiązałem problem przy pomocy takiej konstrukcji

def self.with_comment_title(title)
subquery = Post.joins(:comments).where(comments: {title: title}).select(:post_id)
scoped.where(id: subquery)
end
Post.with_comment_title(“Foo”).with_comment.title(“Bar”)

może w ten sposób?

  def self.with_comment_titles(*titles)
    self_scope = where(nil)

    titles.map {|title| joins(:comments).merge(Comment.where(title: title)).select(:id)}.
      inject(self_scope) {|scope, subquery| scope.where(id: subquery)}   
  end 


  Post.with_comment_titles("Foo", "Bar")

to zapytanie zabije wydajność bazy przy większej ilości danych.

To prawda, zapytanie sql generowane przez mój kod wygląda następująco:
SELECT posts.* FROM posts WHERE posts.id IN (SELECT post_id FROM posts INNER JOIN comments ON comments.post_id = posts.id WHERE comments.title = ‘foo’) AND posts.id IN (SELECT post_id FROM posts INNER JOIN comments ON comments.post_id = posts.id WHERE posts.id IN (SELECT post_id FROM posts INNER JOIN comments ON comments.post_id = posts.id WHERE comments.title = ‘foo’) AND comments.title = ‘bar’)

Podczas gdy zapytanie wygenerowane przez zaproponowany kod:
SELECT posts.* FROM posts WHERE posts.id IN (SELECT post_id FROM posts INNER JOIN comments ON comments.post_id = posts.id WHERE comments.title = ‘foo’) AND posts.id IN (SELECT post_id FROM posts INNER JOIN comments ON comments.post_id = posts.id WHERE comments.title = ‘bar’)

Niestety nie mogę przekazać tablicy tytułów, ponieważ use-case dla metody jest z chain’owaniem z góry nie wiadomej ilości filtrów.
PS: Różnica czasowa dla bazy mysql przy 10k rekordów i 10k zapytań wynosi 4.1156 vs 4.2900

mała różnica w czasie wynika z faktu że zapytania są podobne i relatywnie łatwe, Query Analyzer potrafi dobrać optymalny plan w tym przypadku, ale miałem na myśli coś innego.

Zobacz wydajność w ten sposób:

def self.with_comment_titles(*titles)
  titles.inject(where(nil)) do |s,t|
    s.where("EXISTS (SELECT 1 FROM comments WHERE title = ? LIMIT 1)", t)
  end
end 

to powinno znacząco poprawić wydajność (i lepiej już raczej nie będzie).

Ten scope możesz dowolnie chainować tak jak własny przykład, tj. nic nie stoi na przeszkodzie żeby zrobić

Post.with_comment_titles('Foo', 'Bar').with_comment_titles('Baz').with_comment_text('something')

Wersja dla dowolnej liczby argumentow:

def self.find_with_comments_having_titles(*titles)    
  query = titles.each_with_index.map do |title, index|
    {
      :join => " inner join comments as c#{index} on c#{index}.post_id = posts.id ",
      :conditions => "c#{index}.title = ?"
    }      
  end

  joins = query.map { |q| q[:join] }
  conditions = query.map { |q| q[:conditions] }
  self.joins(joins * '').where(conditions * ' and ', *titles)
    
end

I wersja nieco bardziej kompaktowa:

def self.find_with_comments_having_titles(*titles)

  query = titles.each_with_index.map do |title, index|
    [
      " inner join comments as c#{index} on c#{index}.post_id = posts.id ",
      "c#{index}.title = ?"
    ]      
  end
  self.joins(query.map(&:first) * '').where(query.map(&:last) * ' and ', *titles)   

end

Uzycie:

Post.find_with_comments_having_titles(‘Foo’)

Post.find_with_comments_having_titles(‘Foo’, ‘Bar’)

Post.find_with_comments_having_titles(‘Foo’, ‘Bar’, ‘Bar2’)

Post.find_with_comments_having_titles() tez dziala (zwraca wszystko - brak warunkow)

query.map(&:first).join
query.map(&:last).join(' and ')