Imagine a Book and a Chapter model. Each Chapter belongs_to :book, and a Book has_many :chapters. We have scopes on the Chapter, for example :very_long returns chapters with over 300 pages.
Many times we want to get all books with any chapters over 300 pages. The way we usually achieve this is like so:
# book.rb
scope :has_very_long_chapter, -> { where(id: Chapter.very_long.select(:book_id) }
However, as you can imagine, it gets pretty tedious to proxy the scope every time we want to filter Books by chapter scopes. Is there any more idiomatic or cleaner way to achieve this?
To get those books you can use ActiveRecord::SpawnMethods#merge, and you don't have to use another scope:
Book.joins(:chapters).merge(Chapter.very_long)
I think one thing you could do would be to use joins/merge
class Book < ARBase
scope :with_long_chapters, ->{ joins(:chapter).merge(Chapter.very_long) }
end
class Chapter < ARBase
scope :very_long, -> { logic for querying chapters with over 300 pages }
end
EDIT(#2): A more reusable scope
class Book < ARBase
scope :by_chapter_page_count, ->(pages){ joins(:chapter).merge(Chapter.by_page_count pages) }
scope :with_climax, -> { joins(:chapter).merge(Chapter.by_category :climax) }
scope :long_with_climax, -> { by_chapter_page_count(400).with_climax }
end
class Chapter < ARBase
scope :by_page_count, ->(pages) { where("pages > ?", pages }
scope :by_category, ->(category) { where category: category }
end
Book.by_chapter_page_count(100)
You can get fairly creative with how you write your scopes
Related
I'm reading the book 'Beginning Rails 6 - From Novice to Professional' (awesome book, btw), and I'm on a chapter about Advanced Active Record, great information, but one part of the text is driving me crazy, it works, but I don't get the explanation, could someone please clarify it to me?
As in a regular where method, you can use arrays as parameters. In fact, you can chain finder methods with other named scopes. You define the recent scope to give you articles recently published: first, you use the published named scope, and then you chain to it a where call
To me, the code example isn't exactly what text says (I've tried to find any erratas, with no success):
class Article < ApplicationRecord validates :title, :body, presence: true
belongs_to :user has_and_belongs_to_many :categories has_many :comments
scope :published, -> { where.not(published_at: nil) }
scope :draft, -> { where(published_at: nil) }
scope :recent, -> { where('articles.published_at > ?', 1.week.ago.to_date) }
def long_title
"#{title} - #{published_at}"
end end
I don't see the You define the recent scope to give you articles recently published: first, you use the published named scope, and then you chain to it a where call, plus, can't I just use scope :recent, -> { where('published_at > ?', 1.week.ago.to_date) } instead scope :recent, -> { where('articles.published_at > ?', 1.week.ago.to_date) } removing the articles from the where?
Thanks a lot!
So what the text tries to say are two things.
One is that you can use arrays in where and active record understands and translates it properly to SQL. For example Article.where(user_id: [1,2,3]) will find all articles where the user_id is either 1, 2 or 3.
The second thing they try to explain is that you can chain scopes. So in this case, what they try to explain is that you can do this: Article.recent.published or Article.recent.draft
For your last question, yes you can write it like you suggest and it would work :recent, -> { where('published_at > ?', 1.week.ago.to_date) }. The reason they use articles. is to ensure the scope works when you try to chain scopes with joins.
I am trying to create a scope that would provide me with all clients that made a purchase after they've :transitioned_to_maintenance (a term we use internally).
My models and scopes are organized as follows:
class Client < ApplicationRecord
has_many :program_transitions
has_many :purchases
scope :transitioned_to_maintenance, -> { where(id: ProgramTransition.to_maintenance.pluck(:client_id)) }
scope :has_purchases, -> { where(id: Purchase.pluck(:client_id)) }
end
class ProgramTransition < ApplicationRecord
belongs_to :client, required: true
scope :to_fm, -> { where(new_status: "full maintenance") }
scope :to_lm, -> { where(new_status: "limited maintenance") }
scope :to_maintenance, -> { to_fm.or(to_lm)}
end
class Purchase < ActiveRecord::Base
belongs_to :client
end
I could accomplish what I am trying to do by looping through the clients and selected those that meet my criteria, but I was hoping it'd be possible through a scope. Here it is as a model level function:
def self.purchased_after_maintenance
clients = Client.transitioned_to_maintenance.has_purchases
final = []
clients.each do |client|
min_date = client.program_transitions.to_maintenance.first.created_at
final << client if client.purchases.last.created_at >= ? min_date
end
end
Is this possible to do with a scope and without looping through all clients?
A couple of things before addressing the main question. Don't mean to be annoying and not address the main question first, but I thought this could be helpful.
Instead of scope :transitioned_to_maintenance, -> { where(id: ProgramTransition.to_maintenance.pluck(:client_id)) }, I would do this (see below). merge is really powerful and something we have been using a lot lately at our company.
scope :transitioned_to_maintenance, -> { joins(:program_transitions).merge(ProgramTransition.to_maintenance) }
Instead of doing scope :has_purchases, -> { where(id: Purchase.pluck(:client_id)) }, I would just do scope :has_purchases, -> { joins(:purchases) }, as the joins on purchases will only return clients that have associated purchases.
Regarding the main question, I can't think of a good way to do this with scopes, but it's probably possible. If I were you I would ask your data team what the SQL would look like for that query, and then try to figure out the active record needed to create it. However, if you stick with your way, here is a slight refactor I would recommend. Using select instead of each and eager loading to avoid n+1 queries.
def self.purchased_after_maintenance
Client.joins(:purchases, :program_transitions).includes(:purchases, :program_transitions).transitioned_to_maintenance.select do |client|
transition_date = client.program_transitions.to_maintenance.first.created_at
client.purchases.last.created_at >= transition_date
end
end
Hope this helps, and sorry I don't have the answer with the scopes.
I cannot figure the correct wording for this scope method. The task is to create a scope method that specifies the requests(which have notes) to display only the requests with the word "kids" in the note . I can't seem to get the right wording for this to work
I have tried scope :requests_with_kids_in_note, -> { where('note').includes(text: "kids")}
scope :requests_with_kids_in_note, -> { select('note').where('note').includes(text: "kids")}
error messages aren't helping
You can do this very easily through scopes, but you should format the where clause differently. Also I'd advise to make it more generic:
scope :note_contains, -> (word) { where('note LIKE %?%', word) }
And use it like this:
Model.note_contains('kids')
You can use Arel to compose that query.
Assuming that you have a Request that have a has_many :notes association, you can write the scope like this:
class Request < ApplicationRecord
has_many :notes
scope :with_kids_in_note, -> {
joins(:notes).where(Note.arel_table[:content].matches('%kids%'))
}
end
class Note < ApplicationRecord
# In this model, i'm assuming that "content" is the column
# in which you have to find the word "kids".
belongs_to :request
end
User has many Posts
Post has many Comments
User has many Comments
class Post < ApplicationRecord
..stuff...
scope :created_by, ->(user) { where(creator: user) }
scope :with_comments_by, ->(user) { joins(:comments).where('comments.creator_id = ?'. user.id) }
##########========= this is my failure:
scope :related_to, ->(user) { created_by(user).or(with_comments_by(user) }
(not my real models, just sticking with SO basic app structure)
This last scope doesn't work, as is clearly noted:
The two relations must be structurally compatible, they must be scoping the same model, and they must differ only by WHERE or HAVING.
So, how do I get around this? (please don't say a messy, long SQL sentence)
I want to be able to call Posts.related_to(user) and get one ActiveRecord collection of all posts the user created or commented on.
I was headed down this path, but I know this is perverse:
class Post < ApplicationRecord
..stuff...
scope :created_by, ->(user) { where(creator: user) }
scope :with_comments_by, ->(user) { joins(:comments).where('comments.creator_id = ?'. user.id) }
##########========= this is my failure:
# scope :related_to, ->(user) { created_by(user).or(with_comments_by(user) }
def self.related_to(user)
ary = []
ary << Post.created_by(user).map(&:id)
ary << Post.with_comments_by(user).map(&:id)
Post.find(ary.uniq)
# so...bad...so yucky
end
Help me, SO community. I'm trapped in my own mind.
Your with_comments_by scope isn't quite what you want. That scope should be finding posts whose comments have a comment from user so you should be saying exactly that:
scope :with_comments_by, ->(user) { where(id: Comment.select(:post_id).where(creator_id: user.id)) }
You should be able to use that scope with your or in related_to without any complaints.
This version of with_comments_by will also neatly take care of the duplicate posts that your JOIN could produce if someone has commented multiple times on one post.
I have the following models defined:
class Group < ActiveRecord::Base
end
class Person < ActiveRecord::Base
end
class Policeman < Person
end
class Firefighter < Person
end
Inside Group, I would like to get all groups that have Policemen, for example:
class Group < ActiveRecord::Base
has_many :policemen
scope :with_policemen, -> { joins(:policemen).uniq }
end
This works as expected. Now if I want to grab all groups that have a Policeman that has status: 3, I would do:
class Group < ActiveRecord::Base
has_many :policemen
scope :with_policemen, -> { joins(:policemen).where(policemen: { status: 3 }).uniq }
end
But unfortunately this doesn't work, since ActiveRecord constructs the query using policemen table, which obviously doesn't exist. A solution would be to use where(people: { status: 3 }) inside the scope, but I was wondering why can't ActiveRecord put the correct table in the WHERE clause, since it has the necessary associations set.
According to the docs, the format expected for the hash syntax is table_name: { column_name: val }.
scope :with_policemen, -> { joins(:policemen).where(people: { status: 3 }).uniq }
I agree with you - it would make more sense if the where and joins syntax were similar. Another inconsistency - the group method doesn't take a hash, only a string or array.