Ruby complex query elegant solution - ruby-on-rails

in my rails 4 project, I have the following tables
In this SO question, I searched for a SQL query to fetch the projects with the actual project status id = XX. By actual, I mean the one with the max(created_at).
I got an answer for my query which is
select p.* from projects p
left join projects_status_projects psp on (psp.project_id = p.id)
where created_at = (select max(created_at) from projects_status_projects
where project_id = p.id)
and project_status_id = XX
My models are defined
class Project < ActiveRecord::Base
has_many :projects_status_projects
has_many :projects_statuses, :through => :projects_status_projects
end
class ProjectStatus < ActiveRecord::Base
has_many :projects_status_projects
has_many :projects, :through => :projects_status_projects
end
class ProjectsStatusType < ActiveRecord::Base
belongs_to :project
belongs_to :project_status
end
In my Project model, I have the following method
def self.with_status(status)
joins(:projects_status_projects)
.where('projects_status_projects.created_at = (select max(created_at) from
projects_status_projects where project_id = p.id)')
.where('projects_status_projects.project_status_id = ?', status)
end
While the query is correct, the received results are well filtered, I find this solution terrible and not elegant at all.
Is there any way to get the same result with scopes ?
Thanks for your help.

What do you think of
scope :with_status, -> (status) {
ProjectsStatusType.where(:project_status_id, status).order(:created_at).last.project
}
EDIT based on comments :
As sockmonk said, scopes should be chainable. Here is a cleaner way to do it, which also fix the problem if no project is found.
# ProjectStatusType model
scope :with_ordered_status, -> (status) {
where(:project_status_id, status).order(:created_at)
}
# Project model
def self.find_by_status(status)
project_statuses = ProjectsStatusType.with_ordered_status(status)
project_statuses.any? ? project_statuses.last.project : nil
end

how about?
scope :with_status, ->(status = "default_status") {
joins(:projects_status_projects).
where('projects_status_projects.project_status_id = ?', status).
order("projects_status_projects.created_at DESC").first
}

scope :with_status, ->(status = "default_status") {
joins(:projects_status_projects)
.where('projects_status_projects.project_status_id = ?', status)
.order("projects_status_projects.created_at DESC")
}
When you call it, you'll want to tack a '.first' to the end of it; can't include the .first in the scope itself, as that would make it unchainable.

Related

How to retrieve ActiveRecord records based on the presence of a related model

I have two models, ParentProfile and RoomParentAssignment:
class ParentProfile < ActiveRecord::Base
has_many :room_parent_assignments
and
class RoomParentAssignment < ActiveRecord::Base
belongs_to :room_parent_profile, class_name: 'ParentProfile', foreign_key: 'parent_profile_id'
I would like to retrieve all ParentProfile records where there are no room_parent_assignments. I'm looking for something like the following statement (which needless to say, is invalid):
#non_room_parents = ParentProfile.where(!room_parent_assignments.present?)
How would I do this?
The below query should do
ParentProfile.joins("left outer join room_parent_assignments on room_parent_assignments.parent_profile_id = parent_profiles.id").where(room_parent_assignments: {parent_profile_id: nil})
use the below code:
parent_ids = RoomParentAssignment.select(parent_profile_id)
#non_room_parents = ParentProfile.where.not(:id => parent_ids)
You have two options here:
OPTION 1
#non_room_parents = ParentProfile.where.not(id: RoomParentAssignment.all.map(&:parent_profile_id))
OPTION 2
#non_room_parents = ParentProfile.where("id NOT IN (?)", RoomParentAssignment.all.map(&:parent_profile_id))
Both of them are equal to get no parent rooms.

Rails acts as nested set return descendant of all records in a has_many relation

I am using the awesome_nested_set gem and am trying to get all the descendants of a has_many relationship. A User has many tasks shared to them and each task has many children. Ho do I get a list of a Users tasks_shared_to and all their children (note the children are not shared to the user individually)?
User model:
has_many :tasks_shared_to, through: :user_task_shares, source: :task
Task Model:
acts_as_nested_set
belongs_to :created_by, :class_name => 'User', :foreign_key => 'created_by_id'
belongs_to :parent, :class_name => 'Task', :foreign_key => 'parent_id'
I know this would be easy to do with a loop through each tasks_shared_to and then calling self_and_descendants but I would like to be able to do this without doing a separate SQL call within each loop.
I ended up ditching the awesome_nested_set gem and followed through this excellent blog:
http://hashrocket.com/blog/posts/recursive-sql-in-activerecord
I then modified the class methods to accept an array of objects, extracted to a module and added a few methods like so:
module RecursiveTree
def self.included(recipient)
recipient.extend(ModelClassMethods)
end
module ModelClassMethods
def tree_for_ids(methods)
self.where("#{table_name}.id IN (#{self.tree_sql_for_ids(self.sql_clean(methods))})")
end
def tree_for_id(id)
self.where("#{table_name}.id IN (#{self.tree_sql_for_ids("id = #{id}")})")
end
def sql_clean(methods)
sql_to_exec = ""
methods.each do |method|
sql_string = method.to_sql #note we can pass methods and they will not get executed unless we chain something to them - because of lazy instatiation
#remove everything after the "ORDER" string as can't order more than once and order doesnt matter anyway
sql_string = sql_string.slice(0..(sql_string.index('ORDER')-2)) unless sql_string.index('ORDER').nil?
#add an OR to the front if there is already some text in the sql_to_exec
sql_to_exec += "#{" OR " unless sql_to_exec.empty?}#{table_name}.id IN (#{sql_string})"
end
sql_to_exec.gsub! '*', 'id' #sub to only return the ids
end
def tree_sql_for_ids(sql_id_array)
tree_sql = sql_id_array ? <<-SQL
WITH RECURSIVE search_tree(id, path) AS (
SELECT id, ARRAY[id]
FROM #{table_name}
WHERE #{sql_id_array}
UNION ALL
SELECT #{table_name}.id, path || #{table_name}.id
FROM search_tree
JOIN #{table_name} ON #{table_name}.parent_id = search_tree.id
WHERE NOT #{table_name}.id = ANY(path)
)
SELECT id FROM search_tree ORDER BY path
SQL
: "NULL"
end
def ancestors_sql_for(instance)
ancestors_sql = instance.id ? <<-SQL
WITH RECURSIVE search_tree(id, path) AS (
SELECT parent_id, ARRAY[parent_id]
FROM #{table_name}
WHERE id = #{instance.id}
UNION ALL
SELECT #{table_name}.parent_id, path || #{table_name}.parent_id
FROM search_tree
JOIN #{table_name} ON #{table_name}.id = search_tree.id
WHERE NOT (#{table_name}.parent_id IS NULL)
)
SELECT id FROM search_tree ORDER BY path
SQL
: "NULL"
end
end
#instance methods go here:
def ancestors
self.class.where("#{self.class.table_name}.id IN (#{self.class.ancestors_sql_for(self)})").order("#{self.class.table_name}.id")
end
def ancestor_complete
ancestors.where(complete: true).any?
end
def top_level_parent
is_top_level_parent? ? self : self.ancestors.where(parent_id: nil).first
end
def is_top_level_parent?
self.parent.nil?
end
def descendants
self.id ? self_and_descendants.where.not(id: self.id) : self.class.none #self.class.none return an empty ActiveRecord relation
end
def self_and_descendants
self.id ? self.class.tree_for_id(self.id) : self.class.none #self.class.none return an empty ActiveRecord relation
end
end
and then included in my task model:
class Task < ActiveRecord::Base
include RecursiveTree #in the lib folder
...
end
Ten I can call with any number of methods eg:
u1 = User.find(1)
Task.tree_for_ids([u1.tasks_shared_to, u1.tasks_shared_to_through_groups])
This gets the job done in only 1 query!

Create Rails scope for associated model

I have 2 models:
DeliverySlot has_many :orders
Order belongs_to :delivery_slot
Delivery Slots have a limit of how many orders they can hold. I want to create a scope to give all the available delivery slots. An available delivery slot is one that hasn't reached it's limit of associated orders.
My attempt looks like:
scope :available, where("limit > ?", order.count).joins(:orders)
order.count is pseudocode above.
To do this like you have setup you would need to use orders.count instead of order.count because you're referring to the association. This would prompt ActiveRecord to assemble a query that looks something like SELECT COUNT(*) FROM orders WHERE delivery_slot_id = 1.
Rails is actually smart enough to then use that as a subquery in your where condition when you pass it appropriately, a la where('limit > ', orders.count). But as you might see, this won't work if it's precompiled because the query uses an explicit ID in the condition.
What you need instead is to count orders with an ambiguous condition, then use it as a subquery: where('limit > ?', Order.where(delivery_slot_id: 'delivery_slots.id').count). If you tried to run the query for the order count on its own it would fail on delivery_slots, but because it's in the subquery here you should be smooth sailing.
I'd like to propose another way of doing this altogether though, using a counter cache:
class AddCounterCacheToDeliverySlots < ActiveRecord::Migration
class DeliverySlot < ActiveRecord::Base; end
def change
add_column :delivery_slots, :orders_count, :integer, default: 0
add_index :delivery_slots, [:orders_count, :limit]
DeliverySlot.reset_column_information
DeliverySlot.find_each do |delivery_slot|
DeliverySlot.update_counters delivery_slot.id, orders_count: delivery_slot.orders.count
end
end
end
class Order < ActiveRecord::Base
belongs_to :delivery_slot, counter_cache: true
end
class DeliverySlot < ActiveRecord::Base
has_many orders
scope :available, where('orders_count < limit')
end
Rails will automatically increment and decrement the orders_count column for each DeliverySlot, and because it's indexed, it's ridiculously fast to query.
scope :available, lambda {
|delivery_slot| joins(:orders).
where("limit > ?", order.count)
}
try this
So I found a way to do it in SQL. If anyone knows of a more ruby way without creating loads of database queries please jump in.
scope :available, joins('LEFT JOIN orders
ON orders.delivery_slot_id = delivery_slots.id')
.where("delivery_slots.limit > (
SELECT COUNT(*) FROM orders
WHERE orders.delivery_slot_id = delivery_slots.id )
")

how to perform a somewhat complicated ActiveRecord scope query for a polymorphic association?

So, I have a Notification model that is polymorphic, and I want to be able to filter out notifications that are of a notifiable_type Comment where comment.user == current_user. In other words, I want all notification records--except for ones referring to comments that were made by the current user.
class Notification
belongs_to :notifiable, :polymorphic => true
scope :relevant, lambda { |user_id|
find(:all, :conditions => [
"notifiable_type != 'Comment' OR (notifiable_type = 'Comment' AND " <<
"comments.user_id != ?)",
user_id ],
:include => :comments
)
}
end
What I don't understand is what I need to do to get access to comments? I need to tell ActiveRecord to outer join the comment model on notifiable_id.
First, lambda scopes with parameters are deprecated. Use a class method instead:
class Notification
belongs_to :notifiable, polymorphic: true
def self.relevant(user_id)
# ...
end
end
I usually move scope functions into their own module, but you can leave it there.
Next, find(:all) is deprecated, as is :conditions. We use ActiveRelation queries now.
Unfortunately, the ActiveRecord::Relation API isn't quite robust enough to do what you need, so we'll have to drop down to ARel instead. A little bit tricky, but you definitely don't want to be doing string substitution for security reasons.
class Notification
belongs_to :notifiable, polymorphic: true
def self.relevant(user_id)
n, c = arel_table, Comment.arel_table
predicate = n.join(c).on(n[:notifiable_id].eq(c[:id]))
joins( predicate.join_sql ).
where{ ( notifiable_type != 'Comment' ) |
(( notifiable_type == 'Comment' ) & ( comments.user_id == my{user_id} ))
}
end
end
I'm using a combination of ARel and Squeel here. Squeel is so good it should be a Rails core feature. I tried writing that where clause without Squeel, but it was so difficult I gave up.
Hard to test something like this without your project handy, but hopefully that should at least get you closer.
Oops, your code has :include => :comments, plural, which threw me off. How about this?
class Notification
belongs_to :notifiable, :polymorphic => true
scope :relevant, lambda { |user_id|
find(:all, :conditions => [
"notifiable_type != 'Comment' OR (notifiable_type = 'Comment' AND " <<
"comments.user_id != ?)",
user_id ],
:include => :notifiable
)
}
end
...then Notification.relevant.first.notifiable should work. From the docs:
Eager loading is supported with polymorphic associations.
class Address < ActiveRecord::Base
belongs_to :addressable, :polymorphic => true
end
A call that tries to eager load the addressable model
Address.find(:all, :include => :addressable)
This will execute one query to load the addresses and load the
addressables with one query per addressable type. For example if all
the addressables are either of class Person or Company then a total of
3 queries will be executed. The list of addressable types to load is
determined on the back of the addresses loaded. This is not supported
if Active Record has to fallback to the previous implementation of
eager loading and will raise
ActiveRecord::EagerLoadPolymorphicError. The reason is that the
parent model’s type is a column value so its corresponding table name
cannot be put in the FROM/JOIN clauses of that query.
(Emphasis mine.)
This is what I've resorted to..... I am still hoping someone can show me a better way.
Notification.find_by_sql( "SELECT * from Notifications " <<
"INNER JOIN comments ON notifiable_id = comments.id " <<
"WHERE notifiable_type != 'Comment' " <<
"OR (notifiable_type = 'Comment' AND comments.user_id = '#{user_id}')"
)

Ruby on Rails 3: Combine results from multiple has_many or has_many_through associations

I have the following models. Users have UserActions, and one possible UserAction can be a ContactAction (UserAction is a polymorphism). There are other actions like LoginAction etc. So
class User < AR::Base
has_many :contact_requests, :class_name => "ContactAction"
has_many :user_actions
has_many_polymorphs :user_actionables, :from => [:contact_actions, ...], :through => :user_actions
end
class UserAction < AR::Base
belongs_to :user
belongs_to :user_actionable, :polymorphic => true
end
class ContactAction < AR::Base
belongs_to :user
named_scope :pending, ...
named_scope :active, ...
end
The idea is that a ContactAction joins two users (with other consequences within the app) and always has a receiving and a sending end. At the same time, a ContactAction can have different states, e.g. expired, pending, etc.
I can say #user.contact_actions.pending or #user.contact_requests.expired to list all pending / expired requests a user has sent or received. This works fine.
What I would now like is a way to join both types of ContactAction. I.e. #user.contact_actions_or_requests. I tried the following:
class User
def contact_actions_or_requests
self.contact_actions + self.contact_requests
end
# or
has_many :contact_actions_or_requests, :finder_sql => ..., :counter_sql => ...
end
but all of these have the problem that it is not possible to use additional finders or named_scopes on top of the association, e.g. #user.contact_actions_or_requests.find(...) or #user.contact_actions_or_requests.expired.
Basically, I need a way to express a 1:n association which has two different paths. One is User -> ContactAction.user_id, the other is User -> UserAction.user_id -> UserAction.user_actionable_id -> ContactAction.id. And then join the results (ContactActions) in one single list for further processing with named_scopes and/or finders.
Since I need this association in literally dozens of places, it would be a major hassle to write (and maintain!) custom SQL for every case.
I would prefer to solve this in Rails, but I am also open to other suggestions (e.g. a PostgreSQL 8.3 procedure or something simliar). The important thing is that in the end, I can use Rails's convenience functions like with any other association, and more importantly, also nest them.
Any ideas would be very much appreciated.
Thank you!
To provide a sort-of answer to my own question:
I will probably solve this using a database view and add appropriate associations as needed. For the above, I can
use the SQL in finder_sql to create the view,
name it "contact_actions_or_requests",
modify the SELECT clause to add a user_id column,
add a app/models/ContactActionsOrRequests.rb,
and then add "has_many :contact_actions_or_requests" to user.rb.
I don't know how I'll handle updating records yet - this seems not to be possible with a view - but maybe this is a first start.
The method you are looking for is merge. If you have two ActiveRecord::Relations, r1 and r2, you can call r1.merge(r2) to get a new ActiveRecord::Relation object that combines the two.
If this will work for you depends largely on how your scopes are set up and if you can change them to produce a meaningful result. Let's look at a few examples:
Suppose you have a Page model. It has the normal created_at and updated_at attributes, so we could have scopes like:
:updated -> { where('created_at != updated_at') }
:not_updated -> { where('created_at = updated_at') }
If you pull this out of the database you'll get:
r1 = Page.updated # SELECT `pages`.* FROM `pages` WHERE (created_at != updated_at)
r2 = Page.not_updated # SELECT `pages`.* FROM `pages` WHERE (created_at = updated_at)
r1.merge(r2) # SELECT `pages`.* FROM `pages` WHERE (created_at != updated_at) AND (created_at = updated_at)
=> []
So it did combine the two relations, but not in a meaningful way. Another one:
r1 = Page.where( :name => "Test1" ) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test1'
r2 = Page.where( :name => "Test2" ) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test2'
r1.merge(r2) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test2'
So, it might work for you, but maybe not, depending on your situation.
Another, and recommended, way of doing this is to create a new scope on you model:
class ContactAction < AR::Base
belongs_to :user
scope :pending, ...
scope :active, ...
scope :actions_and_requests, pending.active # Combine existing logic
scope :actions_and_requests, -> { ... } # Or, write a new scope with custom logic
end
That combines the different traits you want to collect in one query ...

Resources