2015-01-25 10 views
9

Setup: Rails 3.2.18, PostgresRails: Rientro solo gli oggetti in cui tutte le associazioni incontrano una condizione

ho due oggetti, per fare un esempio, chiamarli Author e Article, con la seguente configurazione:

class Author 
    has_many :articles 

    ... 
end 

class Article 
    belongs_to :author 

    class << self 

    def published_over_one_year_ago 
     where(arel_table[:published_at].lt(1.year.ago)) 
    end 

    end 
end 

Sto cercando di trovare tutti i record Author dove tutti associati Article record sono pubblicati più di un anno fa. Questo codice:

Author.joins(:article).merge(Article.published_over_one_year_ago) 

... torna Author oggetti in cui almeno un associato Article viene pubblicato più di un anno fa, ma ho bisogno di soloAuthor record in cui tutti associati Article documenti sono più di un anno vecchio. È possibile con arel/ActiveRecord o devo usare SQL raw?

Potenzialmente Solution "di lavoro" SQL:

query = ERB.new(<<-SQL_END).result(binding) 
    'author'.id in (
    select art.author_id 
    from articles art 
    group by art.author_id 
    having sum(case (art.published_at::date >= current_date) when true then 1 else 0 end) = 0 AND 
     sum(case(art.published_at::date < current_date) when true then 1 else 0 end) > 0 
) 
SQL_END 


# The following now appears to do what I want it to do. 
Author.includes(:articles).where(query) 

UPDATE vedo che c'è già una risposta suggerito di usare AREL, che è eccezionale. Qualcuno ha un suggerimento per l'uso diretto di ActiveRecord, se possibile?

+0

Interessante! Hai in mente una soluzione SQL raw? –

+0

Sì, sembra funzionare. Vedi modifica. – CDub

risposta

10

Utilizzando Arel e un tavolo espressione comune:

articles = Article.arel_table 
authors = Author.arel_table 
cte_table = Arel::Table.new(:cte_table) 
composed_cte = 
    Arel::Nodes::As.new(cte_table, 
         articles.project(articles[:author_id], 
              articles[:published_at].maximum.as("max_published_at")) 
           .group(articles[:author_id]) 
         ) 

Poi:

authors 
    .join(cte_table).on(cte_table[:author_id].eq(authors[:id])) 
    .with(composed_cte) 
    .where(cte_table[:max_published_at].lt(1.year.ago)) 
    .project(authors[:id]) 
    .to_sql  

rendimenti (formattato per chiarezza):

WITH "cte_table" AS (
    SELECT articles.author_id, 
      MAX(articles.published_at) AS max_published_at 
    FROM articles 
    GROUP BY articles.author_id 
) 
SELECT authors.id 
FROM authors 
INNER JOIN cte_table 
ON cte_table.author_id = authors.id 
WHERE cte_tble.max_published_at < '2014-01-25 23:04:16.532796' 

Usandolo:

Da

[2] test» Article.pluck(:author_id, :published_at)                                  
D, [2015-01-26T00:10:06.763197 #21897] DEBUG -- : (0.6ms) SELECT "articles"."author_id", "articles"."published_at" FROM "articles" 
=> [ 
    [0] [ 
    [0] 1, 
    [1] Sun, 25 Jan 2015 23:31:58 UTC +00:00 
    ], 
    [1] [ 
    [0] 1, 
    [1] Fri, 01 Jan 1999 00:00:00 UTC +00:00 
    ], 
    [2] [ 
    [0] 2, 
    [1] Fri, 01 Jan 1999 00:00:00 UTC +00:00 
    ] 

la query restituisce:

[2015-01-26T00:10:25.279587 #21897] DEBUG -- : (0.9ms) WITH "cte_table" AS (SELECT "articles"."author_id", MAX("articles"."published_at") AS max_published_at FROM "articles" GROUP BY "articles"."author_id") SELECT "authors"."id" FROM "authors" INNER JOIN "cte_table" ON "cte_table"."author_id" = "authors"."id" WHERE "cte_table"."max_published_at" < '2014-01-25 23:10:25.277902' 
=> [ 
    [0] { 
    "id" => "2" 
    } 
] 

EDIT

senza utilizzare Arel:

Author.joins(:articles) 
     .where("authors.id != ANY (?)", 
       Article.select('author_id') 
        .where("published_at > ?", 1.year.ago) 
      ) 

Se si aggiunge .published_last_year dell'articolo:

class Article < ActiveRecord::Base 
    belongs_to :author 

    def self.published_last_year 
    where("published_at > ?", 1.year.ago) 
    end 
end 

Poi

Author.joins(:articles) 
     .where("authors.id != ANY (?)", 
       Article.published_last_year.select('author_id')) 
versione

Rails4

Author.joins(:articles) 
     .where.not(id: Article.published_last_year.select('author_id')) 
+0

Risposta gloriosa. Qualche brutto scherzo per una risposta ActiveRecord pura? – CDub

+0

@CDub: Modificato la mia risposta. AR non proprio puro, ma non ho alcun modo di esprimere 'NOT ANY' in AR3. – Marth