I have a query that I run in Rails:
me = User.find(1)
my_groups = me.groups
my_groups can return more than one row, potentially.
Is there a quick and dirty way to use a method to determine if my_groups or me.groups is greater than one?
Maybe something like my_groups.greater_than_one? If not, what would you recommend in determining if the query is return >1 row?
me.groups is essentially another table that is associated with User. It basically shows what "groups" a particular user belongs to.
There needn’t be a method for everything, you can simply compare against size:
me.groups.size > 1
However, ActiveRecord::Relation does have many? which will return true if there is more than one record. From the docs:
Returns true if the collection has more than one record. Equivalent to
collection.size > 1.
class Person < ActiveRecord::Base
has_many :pets
end
person.pets.count #=> 1
person.pets.many? #=> false
person.pets << Pet.new(name: 'Snoopy')
person.pets.count #=> 2
person.pets.many? #=> true
If you only cared about if there are any elements (i.e. >0) there’s any (which is also part of Ruby core’s Enumerable). But beware [nil, false].any? #=> false.
You can get this by:
if me.groups.count > 1 # or me.groups.size > 1 or me.groups.any?
'bla bla...'
else
....
end
But I do recommend to have counter cache in User class.
To do so:
Add a column groups_count to users table
add_column :users, :groups_count, :integer, default: 0
In Group model
belongs_to :user, counter_cache: true
Thus you can achieve your goal by:
if me.groups_count > 1
'bla bla...'
else
....
end
This will reduce the db query
Related
I'm using an ActiveRecord::Base.transaction to make a whole bunch of calls pertaining to a grouping of objects that must update/create simultaneously or not at all. One method in this transaction is supposed to use where to find and delete all Trades that match certain parameters.
class Trade < ActiveRecord::Base
include Discard::Model
belongs_to :trade_requester, class_name: "User"
belongs_to :wanted_share, class_name: "Share"
validates :trade_requester, :wanted_share, presence: true
validates :amount, presence: true
def new_wanted_total
wanted_share.amount - amount
end
def update_wanted_share_amount(new_wanted_total)
wanted_share.update_attribute(:amount, new_wanted_total)
end
def delete_extraneous_wanted_trades(wanted_share)
self.class.where("wanted_share_id = ? AND amount > ? AND approved_at = ? AND discarded_at = ?", wanted_share.id, new_wanted_total, nil, nil).delete_all
end
def accept
ActiveRecord::Base.transaction do
delete_extraneous_wanted_trades(wanted_share)
update_wanted_share_amount(new_wanted_total) if new_wanted_total >= 0
Share.create(user_id: self.trade_requester.id, item_id: self.wanted_share.item.id, amount: self.amount, active: false)
self.touch(:approved_at)
end
end
end
When I accept and check the output in my terminal, one line I get says this:
SQL (0.3ms) DELETE FROM "trades" WHERE (wanted_share_id = 8 AND amount > 25 AND approved_at = NULL AND discarded_at = NULL).
I am passing the correct information to the method, and the rest of the terminal output shows that the related records have been updated with the appropriate attributes (one Share set to amount:25 and another Share created with amount:50). But then I check my database, and it says that there is still one Trade for amount: 60. This record exceeds the available total, which is now 50 (it was previously 75), and should have been deleted. But according to the terminal output, it was ignored. Why did this record go untouched?
approved_at = ? AND discarded_at = ? in the where clause should be approved_at IS NULL AND discarded_at IS NULL
I have a model named Awardunit. Awardunit have many Awardleaders. One Award unit can have one or many Award leaders.
If I get all the record or search and get a collection of records to a variable named awardunits how can I count the number of Awardleaders in all the units in this collection?
Here's what I did :
#leaders = 0
#awardunits.each do |unit|
#leaders = #leaders + unit.awardleaders.size
end
Again to count the disabled leaders I use this :
#disabledleaders = 0
#awardunits.each do |unit|
#disabledleaders = #disabledleaders + unit.awardleaders.where(disabled: true).size
end
If I use this, it will have to go through all the records every time the page loads. Isn't there a better way of doing this?
You can make counting the associations cheap by adding a counter cache:
class AwardUnit << ActiveRecord::Base
has_many :award_units
end
class AwardLeader << ActiveRecord::Base
belongs_to :award_unit, counter_cache: true
end
Now add a new column called award_leaders_count to your AwardUnit table in a new migration:
def change
add_column :award_units, :award_leaders_count, :integer, default: 0
AwardUnit.all.each do |unit|
AwardUnit.reset_counters(unit.id, :award_leaders)
end
end
Rails will now automatically cache the number of award_leaders for every AwardUnit and #my_award_unit.award_leaders.count will give you the count without running another database query.
By default, Rails counter_cache only works for all award_leaders. If you need to count only those award_leaders that have a condition, you will have to add your own counter_cache:
class AwardUnit << ActiveRecord::Base
has_many :award_units
end
class AwardLeader << ActiveRecord::Base
belongs_to :award_unit
scope :disabled, -> { where(disabled: true) }
after_save :update_counter_cache
after_destroy :update_counter_cache
def update_counter_cache
award_unit.update_attribute(:disabled_award_units_count, award_unit.award_leaders.disabled.count)
end
end
migration:
def change
add_column :award_units, :disabled_award_leaders_count, :integer, default: 0
AwardUnit.all.each do |unit|
unit.update_attribute(:disabled_award_units_count, unit.award_leaders.disabled.count)
end
end
Now, when you have an array of AwardUnits, getting their combined count of disabled award leaders is as simple as
#award_units = AwardUnit.limit(5).to_a # or a similar query
#award_units.inject(0){|sum,unit| sum + unit.disabled_award_leaders_count }
You can eager load Awardleaders when fetching Awardunits, so you don't have to execute a count query for every Awardunit, like this:
#awardunits = Awardunit.includes(:awardleaders).where('awardleaders.disabled = ?', true) # the rest of the query
Or, you can query the count directly like this:
#leaders = Awardleader.where('awardunit_id IN (?)', #awardunits.map(&:id)).where(:disabled => true).count
This will run Database query only once
Awardunit.includes(:awardleaders).map {|award_unit| award_unit.awardleaders.size}.inject(0){|sum,item| sum + item }
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 )
")
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.
I think it's safe to say everyone loves doing something like this in Rails:
Product.find(:all, :conditions => {:featured => true})
This will return all products where the attribute "featured" (which is a database column) is true. But let's say I have a method on Product like this:
def display_ready?
(self.photos.length > 0) && (File.exist?(self.file.path))
end
...and I want to find all products where that method returns true. I can think of several messy ways of doing it, but I think it's also safe to say we love Rails because most things are not messy.
I'd say it's a pretty common problem for me... I'd have to imagine that a good answer will help many people. Any non-messy ideas?
The only reliable way to filter these is the somewhat ugly method of retrieving all records and running them through a select:
display_ready_products = Product.all.select(&:display_ready?)
This is inefficient to the extreme especially if you have a large number of products which are probably not going to qualify.
The better way to do this is to have a counter cache for your photos, plus a flag set when your file is uploaded:
class Product < ActiveRecord::Base
has_many :photos
end
class Photo < ActiveRecord::Base
belongs_to :product, :counter_cache => true
end
You'll need to add a column to the Product table:
add_column :products, :photos_count, :default => 0
This will give you a column with the number of photos. There's a way to pre-populate these counters with the correct numbers at the start instead of zero, but there's no need to get into that here.
Add a column to record your file flag:
add_column :products, :file_exists, :boolean, :null => false, :default => false
Now trigger this when saving:
class Product < ActiveRecord::Base
before_save :assign_file_exists_flag
protected
def assign_file_exists_flag
self.file_exists = File.exist?(self.file.path)
end
end
Since these two attributes are rendered into database columns, you can now query on them directly:
Product.find(:all, :conditions => 'file_exists=1 AND photos_count>0')
You can clean that up by writing two named scopes that will encapsulate that behavior.
You need to do a two level select:
1) Select all possible rows from the database. This happens in the db.
2) Within Ruby, select the valid rows from all of the rows. Eg
possible_products = Product.find(:all, :conditions => {:featured => true})
products = possible_products.select{|p| p.display_ready?}
Added:
Or:
products = Product.find(:all, :conditions => {:featured => true}).select {|p|
p.display_ready?}
The second select is the select method of the Array object. Select is a very handy method, along with detect. (Detect comes from Enumerable and is mixed in with Array.)