Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails: Return only objects where all associations meet a condition

Setup: Rails 3.2.18, Postgres

I have two objects, for sake of example, call them Author and Article, with the following setup:

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

I'm trying to find all Author records where all associated Article records are published over one year ago. This code:

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

... returns Author objects where at least one associated Article is published over one year ago, but I need only Author records where all associated Article records are over one year old. Is this possible with arel / ActiveRecord, or do I have to use raw SQL?

Potentially "Working" SQL Solution:

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 I see there's already a suggested answer using arel, which is outstanding. Anyone have a suggestion for using straight ActiveRecord, if possible?

like image 773
CDub Avatar asked Jan 25 '15 22:01

CDub


1 Answers

Using Arel and a Common Table Expression :

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])
                        )

Then :

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    

returns (formatted for clarity):

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'

Using it :

From

[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
  ]

The query returns :

[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

Without using Arel :

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

If you add .published_last_year to Article :

class Article < ActiveRecord::Base
  belongs_to :author

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

Then

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

Rails4 version

 Author.joins(:articles)
       .where.not(id: Article.published_last_year.select('author_id'))
like image 184
Marth Avatar answered Nov 15 '22 15:11

Marth