Create Rails scope for associated model - ruby-on-rails

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

Related

Rails how to compare column value with sum of values in associated model

I have a model project and payment
class Project < ActiveRecord::Base
has_many :payments, :dependent => :destroy
end
class Payment < ActiveRecord::Base
belongs_to :project
end
I am trying to find projects which have total sum of its payments higher than is project target amount
#projects=Project.joins(:payments).where('enabled = true and amount < sum(payments.amount)')
this shows me error thay my attempt is too ambiguous
How should I compare field by sum from joined table?
Firstly your error is because of both table have same column name amount you can solve this error using projects.amount but aggregate function i.e sum is not allow in where clause of sql. you can use group by clause with having this way:
#projects=Project.joins(:payments).group("payments.project_id").where('enabled = true').having("sum(payments.amount) > projects.amount")
you can also try this way beacuse PG not support other column to select which are not in group by clause:
#projects=Project.includes(:payments).where("projects.id = payments.project_id and enabled = true").select{ |p| p.amount < p.payements.sum(&:amount) }
ok I've found a solution how to use proposed mysql answer on postgres
Project.select("projects.*").joins(:payments).group("projects.id").where('enabled = true').having("sum(payments.amount) > projects.amount")
found it here
https://github.com/rails/rails/issues/1515

What is the best syntax for using a `where` statement to find association counts

I can think of a few ways to do this, but I'm unsure what to choose..
I have the class Topic and I am trying to scope this so that I only return Topics if it has the associated object Reply or topic.replies as a count greater than 0.
Worst way to do this :
#topics.select{ | topic | topic.replies > 0 && topic.title == "Conversation" }
Ideally, I'd like to use a where scope.
scope = current_user.topics
scope = scope.joins 'left outer join users on topics.registration_id = registration_members.registration_id'
# scope = .. here I want to exclude any of these topics that have both the title "Conversations" and replies that are not greater than 0
I need to "append" these selections to anything else already selected. So my selection shouldn't exclude all others to just this selection. It's just saying that any Topic with replies less than one and also called "Conversation" should be excluded from the final return.
Any ideas?
Update
Sort of a half-hashed idea :
items_table = Arel::Table.new(scope)
unstarted_conversations = scope.select{|a| a.title == "Conversation" && a.replies.count > 0}.map(&:id)
scope.where(items_table[:id].not_in unstarted_conversations)
You can use something called count cache, basically what it does is add a field to the table and store in that field the total of "associates" of the specified type and is automatically updated.
Checkout this old screen/ascii cast: http://railscasts.com/episodes/23-counter-cache-column?view=asciicast
Here is something newer: http://hiteshrawal.blogspot.com/2011/12/rails-counter-cache.html
In your case would be as follow:
# migration
class AddCounterCacheToTopìc < ActiveRecord::Migration
def self.up
add_column :topics, :replies_count, :integer, :default => 0
end
def self.down
remove_column :topics, :replies_count
end
end
# model
class Replay < ActiveRecord::Base
belongs_to :topic, :counter_cache => true
end
Hope it help you.

ActiveRecord Association select counts for included records

Example
class User
has_many :tickets
end
I want to create association which contains logic of count tickets of user and use it in includes (user has_one ticket_count)
Users.includes(:tickets_count)
I tried
has_one :tickets_count, :select => "COUNT(*) as tickets_count,tickets.user_id " ,:class_name => 'Ticket', :group => "tickets.user_id", :readonly => true
User.includes(:tickets_count)
ArgumentError: Unknown key: group
In this case association query in include should use count with group by ...
How can I implement this using rails?
Update
I can't change table structure
I want AR generate 1 query for collection of users with includes
Update2
I know SQL an I know how to select this with joins, but my question is now like "How to get data" . My question is about building association which I can use in includes. Thanks
Update3
I tried create association created like user has_one ticket_count , but
looks like has_one doesn't support association extensions
has_one doesn't support :group option
has_one doesn't support finder_sql
Try this:
class User
has_one :tickets_count, :class_name => 'Ticket',
:select => "user_id, tickets_count",
:finder_sql => '
SELECT b.user_id, COUNT(*) tickets_count
FROM tickets b
WHERE b.user_id = #{id}
GROUP BY b.user_id
'
end
Edit:
It looks like the has_one association does not support the finder_sql option.
You can easily achieve what you want by using a combination of scope/class methods
class User < ActiveRecord::Base
def self.include_ticket_counts
joins(
%{
LEFT OUTER JOIN (
SELECT b.user_id, COUNT(*) tickets_count
FROM tickets b
GROUP BY b.user_id
) a ON a.user_id = users.id
}
).select("users.*, COALESCE(a.tickets_count, 0) AS tickets_count")
end
end
Now
User.include_ticket_counts.where(:id => [1,2,3]).each do |user|
p user.tickets_count
end
This solution has performance implications if you have millions of rows in the tickets table. You should consider filtering the JOIN result set by providing WHERE to the inner query.
You can simply use for a particular user:
user.tickets.count
Or if you want this value automatically cached by Rails.
Declare a counter_cache => true option in the other side of the association
class ticket
belongs_to :user, :counter_cache => true
end
You also need a column in you user table named tickets_count.
With this each time you add a new tickets to a user rails will update this column so when you ftech your user record you can simply accs this column to get the ticket count without additional query.
Not pretty, but it works:
users = User.joins("LEFT JOIN tickets ON users.id = tickets.user_id").select("users.*, count(tickets.id) as ticket_count").group("users.id")
users.first.ticket_count
What about adding a method in the User model that does the query?
You wouldn't be modifying the table structure, or you can't modify that either?
How about adding a subselect scope to ApplicationRecord:
scope :subselect,
lambda { |aggregate_fn, as:, from:|
query = self.klass
.select(aggregate_fn)
.from("#{self.table_name} _#{self.table_name}")
.where("_#{self.table_name}.id = #{self.table_name}.id")
.joins(from)
select("(#{query.to_sql}) AS #{as}")
}
Then, one might use the following query:
users = User.select('users.*').subselect('COUNT(*)', as: :tickets_count, from: :tickets)
users.first.ticket_count
# => 5

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 ...

How to sort Rails AR.find by number of objects in a has_many relationship

How can I write an AR find query to have the results ordered by the number of records in a has_many association?
class User < ActiveRecord::Base
has_many :photos
end
I want to do something like...
User.find(:all, :order => photos.count)
I realize my find is not valid code. Say I have the following data.
User 1, which has 3 photos
User 2, which has 5 photos
User 3, which has 2 photos
I want my find to bring me back the users in the order of...
User 2,
User 1,
User 3
based on the count of of the users photos
The easiest way to achieve this is probably to add a counter cache to that model and then sort by that column.
class Photo < ActiveRecord::Base
belongs_to :user, :counter_cache => true
end
And be sure to add a column to your users table called photos_count.
Then you will be able to...
User.find(:all, :order => 'photos_count')
If you don't want an extra column, you could always ask for an extra column in the returned result set:
User.all(:select => "#{User.table_name}.*, COUNT(#{Photo.table_name}.id) number_of_photos",
:joins => :photos,
:order => "number_of_photos")
This generates the following SQL:
SELECT users.*, COUNT(photos.id) number_of_photos
FROM `users` INNER JOIN `photos` ON photos.user_id = users.id
ORDER BY number_of_photos
If you don't want to add a counter cache column, your only option is to sort after the find. If you :include the association in your find, you won't incur any additional database work.
users = User.find(:all, :include => :photos).sort_by { |u| -u.photos.size }
Note the negative sign in the sort_by block to sort from high to low.
I would advise you not to write direct SQL, since implementations of it may vary from store to store. Fortunately, you have arel:
User.joins(:photos).group(Photo.arel_table[:user_id]).
order(Photo.arel_table[:user_id].count)
Counter cache will help, but you'll need an extra column in the db.
I'd add this as a comment on the top answer, but I can't for some reason. According to this post:
http://m.onkey.org/active-record-query-interface
The User.all(options) method will be deprecated after Rails 3.0.3, and replaced with a bunch of other (handy, chainable) active record type stuff, but it makes it very hard to figure out how to put together the same kind of query.
As a result, I've gone ahead and implemented the counter cache method. This was pretty easy and painless with the exception that you need to remember to update the column information in your migration, otherwise all existing records will have "0."
Here's what I used in my migration:
class AddUserCountToCollections < ActiveRecord::Migration
def self.up
add_column :collections, :collectionusers_count, :integer, :default => 0
Collection.reset_column_information
Collection.all.each do |c|
Collection.update_counters c.id, :collectionusers_count => c.collectionusers.count
end
end
def self.down
remove_column :collections, :collectionusers_count
end
end
In theory this should be faster, too. Hope that's helpful going forward.
Your question doesn't make sense. The :order parameter specifies a column name and an optional ordering direction i.e. asc(ending) or desc(ending).
What is the result that you're trying to achieve?

Resources