default_scope and associations - ruby-on-rails

Suppose I have a Post model, and a Comment model. Using a common pattern, Post has_many Comments.
If Comment has a default_scope set:
default_scope where("deleted_at IS NULL")
How do I easily retrieve ALL comments on a post, regardless of scope?
This produces invalid results:
Post.first.comments.unscoped
Which generates the following queries:
SELECT * FROM posts LIMIT 1;
SELECT * FROM comments;
Instead of:
SELECT * FROM posts LIMIT 1;
SELECT * FROM comments WHERE post_id = 1;
Running:
Post.first.comments
Produces:
SELECT * FROM posts LIMIT 1;
SELECT * FROM comments WHERE deleted_at IS NULL AND post_id = 1;
I understand the basic principle of unscoped removing all existing scopes, but shouldn't it be aware and to keep the association scope?
What is the best way to pull ALL comments?

For some strange reasons,
Comment.unscoped { Post.last.comments }
includes the default_scope of Comment,
however,
Comment.unscoped { Post.last.comments.to_a }
Comment.unscoped { Post.last.comments.order }
do not include the default_scope of Comment.
I experienced this in a rails console session with Rails 3.2.3.

with_exlusive_scope is deprecated as of Rails 3. See this commit.
Before (Rails 2):
Comment.with_exclusive_scope { Post.find(post_id).comments }
After (Rails 3):
Comment.unscoped { Post.find(post_id).comments }

Rails 4.1.1
Comment.unscope(where: :deleted_at) { Post.first.comments }
Or
Comment.unscoped { Post.first.comments.scope }
Note that I added .scope, it seems like this block should return kind of ActiveRecord_AssociationRelation (what .scope does) not ActiveRecord_Associations_CollectionProxy (without a .scope)

This is indeed a very frustrating problem which violates the principle of least surprise.
For now, you can just write:
Comment.unscoped.where(post_id: Post.first)
This is the most elegant/simple solution IMO.
Or:
Post.first.comments.scoped.tap { |rel| rel.default_scoped = false }
The advantage of the latter:
class Comment < ActiveRecord::Base
# ...
def self.with_deleted
scoped.tap { |rel| rel.default_scoped = false }
end
end
Then you can make fun things:
Post.first.comments.with_deleted.order('created_at DESC')
Since Rails 4, Model.all returns an ActiveRecord::Relation , rather than an array of records.
So you can (and should) use all instead of scoped:
Post.first.comments.all.tap { |rel| rel.default_scoped = false }

How about this?
# Use this scope by default
scope :active, -> { where(deleted_at: nil) }
# Use this whenever you want to include all comments regardless of their `deleted_at` value
scope :with_soft_deleted, -> { unscope(where: :deleted_at)
default_scope, -> { active }
post.comments would fire this query:
SELECT "comments".* FROM "comments" WHERE "comments"."deleted_at" IS NULL AND "comments"."post_id" = $1;
post.comments.with_soft_deleted would send this:
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1;

class Comment
def post_comments(post_id)
with_exclusive_scope { find(all, :conditions => {:post_id => post_id}) }
end
end
Comment.post_comments(Post.first.id)

Related

What is good way to query chaining conditionally in rails?

If I need to query conditionally, I try a way like this:
query = Model.find_something
query = query.where(condition1: true) if condition1 == true
query = query.where(condition2: true) if condition2 == true
query = query.where(condition3: true) if condition3 == true
It's work well.
But I think that it is a way to repeat same code and a look is not good.
Is it possible query does not reassign to variable per every conditional expression? like this:
query.where!(condition1: true) # But it can not be in rails :)
I recently started as Rails5.
What else is the best way to do Rails5?
You can use model scopes:
class Article < ApplicationRecord
scope :by_title, ->(title) { where(title: title) if title }
scope :by_author, ->(name) { where(author_name: name) if name }
end
Just chain scopes anywhere you need:
Article.by_title(params[:title]).by_author(params[:author_name])
If parameter present you get scoped articles, if there is no such parameter - you get all Articles

Scope Order by Count with Conditions Rails

I have a model Category that has_many Pendencies. I would like to create a scope that order the categories by the amount of Pendencies that has active = true without excluding active = false.
What I have so far is:
scope :order_by_pendencies, -> { left_joins(:pendencies).group(:id).order('COUNT(pendencies.id) DESC')}
This will order it by number of pendencies, but I want to order by pendencies that has active = true.
Another try was:
scope :order_by_pendencies, -> { left_joins(:pendencies).group(:id).where('pendencies.active = ?', true).order('COUNT(pendencies.id) DESC')}
This will order by number of pendencies that has pendencies.active = true, but will exclude the pendencies.active = false.
Thank you for your help.
I guess you want to sort by the amount of active pendencies without ignoring categories that have no active pendencies.
That would be something like:
scope :order_by_pendencies, -> {
active_count_q = Pendency.
group(:category_id).
where(active: true).
select(:category_id, "COUNT(*) AS count")
joins("LEFT JOIN (#{active_count_q.to_sql}) AS ac ON ac.category_id = id").
order("ac.count DESC")
}
The equivalent SQL query:
SELECT *, ac.count
FROM categories
LEFT JOIN (
SELECT category_id, COUNT(*) AS count
FROM pendencies
GROUP BY category_id
WHERE active = true
) AS ac ON ac.category_id = id
ORDER BY ac.count DESC
Note that if there are no active pendencies for a category, the count will be null and will be added to the end of the list.
A similar subquery could be added to sort additionally by the total amount of pendencies...
C# answer as requested:
method() {
....OrderBy((category) => category.Count(pendencies.Where((pendency) => pendency.Active))
}
Or in straight SQL:
SELECT category.id, ..., ActivePendnecies
FROM (SELECT category.id, ..., count(pendency) ActivePendnecies
FROM category
LEFT JOIN pendency ON category.id = pendency.id AND pendnecy.Active = 1
GROUP BY category.id, ...) P
ORDER BY ActivePendnecies;
We have to output ActivePendnecies in SQL even if the code will throw it out because otherwise the optimizer is within its rights to throw out the ORDER BY.
For now I developed the following (it's working, but I believe that it's not the best way):
scope :order_by_pendencies, -> { scoped = Category.left_joins(:pendencies)
.group(:id)
.order('COUNT(pendencies.id) DESC')
.where('pendencies.active = ?', true)
all = Category.all
(scoped + all).uniq}

How to isolate a query inside a scope from pundit policy scope?

I'm using Rails 5 + Pundit gem and trying to fetch some chats with policy scope and model scope. Model scope has a query inside it and the problem is that policy scope applies to this inner query. The question is how to isolate the query from outer scope? Here's some code:
# model
scope :with_user, ->(user_id=nil) {
user_id ? where(chats: { id: User.find(user_id).chats.ids }) : all
}
# policy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.joins(:chat_users).where(chat_users: { user_id: user.id })
end
end
end
So I decided to output the inner sql query, which should get user chats' ids from the scope. I updated the model scope:
scope :with_user, ->(user_id=nil) {
puts User.find(user_id).chats.to_sql
where(chats: { id: User.unscoped.find(user_id).chats.ids } )
}
and here are results:
when I run ChatPolicy::Scope.new(User.first, Chat).resolve.with_user(358) I get:
SELECT "chats".* FROM "chats" INNER JOIN "chat_users"
"chat_users_chats" ON "chat_users_chats"."chat_id" = "chats"."id"
INNER JOIN "chat_users" ON "chats"."id" = "chat_users"."chat_id" WHERE
(chat_users.user_id = 350) AND "chat_users"."user_id" = 358
When I run Chat.with_user(358) I get:
SELECT "chats".* FROM "chats" INNER JOIN "chat_users" ON "chats"."id"
= "chat_users"."chat_id" WHERE "chat_users"."user_id" = 358
It generates the correct query if I run it without policy scope. Is there a workaround?
This is a Community Wiki answer replacing the answer having been edited into the original question as recommended by Meta.
This has been solved by the OP with a different unscoped model scope:
scope :with_user, ->(user_id=nil) {
user_id ? where(chats: { id: Chat.unscoped.joins(:chat_users).where(chat_users: { user_id: user_id }).ids } ) : all
}

Combining two ActiveRecord queries with pagination

I have two active record queries:
feedbacks = Activity.where(subject_type: Feedback.name).select{ |f| f.subject.application == #application }
activities = Activity.where(subject: #application)
.order(created_at: :desc).page(params[:page])
.per(10) + feedbacks
I feel like there must be a better way to combine the results of these two queries. Also, pagination isn't going to work right since feedbacks could return n records.
If I added pagination to both queries, then I could get double the items than I actually want to display.
Edit: Here's an attempt which seems to be working, though - not pretty:
activities = Activity.where('subject_id = ? OR subject_type = ?', #application.id, Feedback.name)
.order(created_at: :desc)
.page(params[:page])
.per(20).select { |record|
if record.subject_type == Feedback.name
record.subject.application == #application
else
true
end
}
Add this to your Activity Model :
belongs_to :feedback, -> { where(activities: {subject_type: 'Feedback'}) }, foreign_key: 'subject_id'
then in your controller :
feedbacks = Activity.includes(:feedback).where(feedbacks:{application:#application})
other_activities = Activity.where(subject: #application)
matching_ids = (feedbacks.map(&:id)+other_activities.map(&:id))
activities = Activity.where(id:matching_ids).order(created_at: :desc).page(params[:page])
This solution is not really scalable, you should make quite a complex query because of two conditions based on polymorphic associations

Any possible way to add parameters to ON clause on include left joins on rails?

I have a huge complex query like this:
#objects = Object.joins({ x: :y }).includes(
[:s, { x: { y: :z } }, { l: :m },:q, :w,
{ important_thing:
[:h, :v, :c,:l, :b, { :k [:u, :a] }]
}
]).where(conditions).order("x.foo, x.bar")
Then i want to show all Objects and only Important_things that were created at between two dates.
If i put this on there where clause i dont get all Objects, only Objects that has Important_things between informed dates.
A solution using raw sql was this:
select * from objects left join important_things on important_things.object_id = objets.id and important_things.created_at between 'x' and 'y'
Instead of:
select * from objects left join important_things on important_things.object_id = objets.id where important_things.created_at between 'x' and 'y'
I really need all those objects and i don't want to use a raw SQL, any workaround or a possibility to pass parameters to the ON clause on an association?
I do this,
class VendorsRatings < ActiveRecord::Base
def self.ratings(v_ids,sort = "DESC")
joins("RIGHT OUTER JOIN vendors_lists v
ON v.vendor_id = vendors_ratings.vendor_id").where(conditions)
end
end
I did a ugly workaround:
class Parent < ActiveRecord::Base
cattr_accessor :dt_begin, dt_end
has_many :children, conditions: Proc.new { { created_at: (##dt_begin..##dt_end) } }
end
class MetasController < ApplicationController
def index
Parent.dt_begin = Date.parse(param[:dt_begin])
Parent.dt_end = Date.parse(param[:dt_end])
#parents = Parent.includes(:children).where("children.age = ?", params[:age])
end
end
So this way i get all Parents even if i dont have Children created_at between those specified dates.
And the most important part of it i have all objects inside the ActiveRecord.
But be careful because i did messed with cattr_accessor so i have to set everytime before i search for Parents.

Resources