Rails 3: add sum column to the result hash - ruby-on-rails

I have this models:
Event
has_many :incidents
and
Incident
belongs_to :event
I want to getting all events with the incident numbers for each one in one query. In Rails I know exist "group by" and "sum" methods but I don't know how mix both.
thanks.

You need to make a left join, group by object id and count elements:
Events.joins('LEFT JOIN incidents ON incidents.event_id = events.id').group_by('events.id').select('events.*, COUNT(incidents.id) AS incidents_count')
You could also look into counter_cache option.

Related

Find records with at least one association but exclude records where any associations match condition

In the following setup a customer has many tags through taggings.
class Customer
has_many :taggings
has_many :tags, through: :taggings
end
class Tagging
belongs_to :tag
belongs_to :customer
end
The query I'm trying to perform in Rails with postgres is to Find all customers that have at least one tag but don't have either of the tags A or B.
Performance would need to be taken into consideration as there are tens of thousands of customers.
Please try the following query.
Customer.distinct.joins(:taggings).where.not(id: Customer.joins(:taggings).where(taggings: {tag_id: [tag_id_a,tag_id_b]}).distinct )
Explanation.
Joins will fire inner join query and will make sure you get only those customers which have at least one tag associated with them.
where.not will take care of your additional condition.
Hope this helps.
Let tag_ids is array of A and B ids:
tag_ids = [a.id, b.id]
Then you need to find the Customers, which have either A or B tag:
except_relation = Customer.
joins(:tags).
where(tags: { id: tag_ids }).
distinct
And exclude them from the ones, which have at least one tag:
Customer.
joins(:tags).
where.not(id: except_relation).
distinct
INNER JOIN, produced by .joins, removes Customer without Tag and is a source of dups, so distinct is needed.
UPD: When you need performance, you probably have to change your DB schema to avoid extra joins and indexes.
You can search examples of jsonb tags implementation.
Get ids of tag A and B
ids_of_tag_a_and_b = [Tag.find_by_title('A').id, Tag.find_by_title('B').id]
Find all customers that have at least one tag but don't have either of the tags A or B.
#Customer.joins(:tags).where.not("tags.id in (?)", ids_of_tag_a_and_b)
Customer.joins(:tags).where.not("tags.id = ? OR tags.id = ?", tag_id_1, tag_id_2)

ActiveRecord return uniq item by title condition nearest distance

I have following relation, because same product can exist in multiple stores, so DB stores the same product record in database with different store_id for each store, I also stores the coords for each product, now my query returns duplicate products by title for each store.
For quick fix how can I modify the query to it returns the closest products only and group by the product title maybe. BTW I am using geocode gem and near function to select nearby product near([#lat, #long], 20, order: #sort.blank? ? 'distance' : false)
class Product < ActiveRecord::Base
belongs_to :store
end
class Store < ActiveRecord::Base
has_many :products
end
ID TITLE STORE_ID
1 product_1 1
2 product_1 2
3 product_1 3
4 product_2 1
5 product_2 2
6 product_2 3
I think you have a misconception in your model. If the same product can be in several stores, then A product does NOT belong to a store. Cause it could be in several.
You should change your associations to a has_and_belongs_to_many or a has_many through one. But if you really want this query done, you can use the group method in your query.
Like Product.near(...).group(:title)
You can also filter after in Rails. Assuming it's already an ordered set, something like
#products.to_a.uniq(&:title)
would work.

What is the best way to order by parent and child's created_at?

Suppose I have a parent Comment that has many Replies.
class Comment < ActiveRecord::Base
has_many :replies
class Reply < ActiveRecord::Base
belongs_to :topic
I would like to order my Comments by created_at, but if they have a newer Reply, I would like that parent Comment to be sorted by its most latest Reply's created_at and not its own created_at.
So if for example I have 3 Comments, one posted for Day 1, Day 2, and Day 3. Day 1 being the oldest, but Day 2 has a reply that was posted today, how can I sort it so that the result would be :
[ Day 2, Day 3, Day 1 ]
Where Day 2 has had the the most recent activity, more recent than the Day 3 comment that would have been posted earlier in the day.
What would be the approach for that?
Handling the dates is best done through your models:
class Comment < ActiveRecord::Base
has_many :replies
end
class Reply < ActiveRecord::Base
belongs_to :comment, touch: true
end
Note the touch: true on the belongs_to: comment association. This will update the updated_at attribute of the parent Comment when saving a Reply
Now just order by updated_at when selecting your comments.
The best query for that (as in the one that will perform best) is to add an extra last_activity column. Maintain the latter (automatically, using triggers or some RoR built-in sugar) and add an index on it. You'll then be able to fetch rows ordered by that column using trivial queries.
The alternative option is an ugly join with an aggregate (see the answer from an hour ago). It won't be pretty and it will perform terribly as your table grows in size.
I imagine something like this might be "okay", but check out the query plan. (I only deal with LINQ and SQL Server myself - YMMV.)
select * from comments c
left join (select r.comment_id, max(r.created_at) as created_at
from replies r
group by r.comment_id) lr
on lr.comment_id = c.comment_id
order by isnull(lr.created_at, c.created_at) desc
After a LEFT JOIN to the latest response (which may not exist, hence the LEFT), use (SQL standard) COALESCE in ORDER BY. But only SELECT columns from comments, according to your requirement:
SELECT c.*
FROM comments c
LEFT JOIN (
SELECT comment_id, max(created_at) AS created_at
FROM replies
GROUP BY 1
) r USING (comment_id)
ORDER BY COALESCE(r.created_at, c.created_at) DESC
Since logic dictates that replies must come after comments. If that wasn't so, you'd use GREATEST(r.created_at, c.created_at) instead.

ActiveRecord find categories which contain at least one item

Support I have two models for items and categories, in a many-to-many relation
class Item < ActiveRecord::Base
has_and_belongs_to_many :categories
class Category < ActiveRecord::Base
has_and_belongs_to_many :items
Now I want to filter out categories which contain at least one items, what will be the best way to do this?
I would like to echo #Delba's answer and expand on it because it's correct - what #huan son is suggesting with the count column is completely unnecessary, if you have your indexes set up correctly.
I would add that you probably want to use .uniq, as it's a many-to-many you only want DISTINCT categories to come back:
Category.joins(:items).uniq
Using the joins query will let you more easily work conditions into your count of items too, giving much more flexibility. For example you might not want to count items where enabled = false:
Category.joins(:items).where(:items => { :enabled => true }).uniq
This would generate the following SQL, using inner joins which are EXTREMELY fast:
SELECT `categories`.* FROM `categories` INNER JOIN `categories_items` ON `categories_items`.`category_id` = `categories`.`id` INNER JOIN `items` ON `items`.`id` = `categories_items`.`item_id` WHERE `items`.`enabled` = 1
Good luck,
Stu
Category.joins(:items)
More details here: http://guides.rubyonrails.org/active_record_querying.html#joining-tables
please notice, what the other guys answererd is NOT performant!
the most performant solution:
better to work with a counter_cache and save the items_count in the model!
scope :with_items, where("items_count > 0")
has_and_belongs_to_many :categories, :after_add=>:update_count, :after_remove=>:update_count
def update_count(category)
category.items_count = category.items.count
category.save
end
for normal "belongs_to" relation you just write
belongs_to :parent, :counter_cache=>true
and in the parent_model you have an field items_count (items is the pluralized has_many class name)
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
in a has_and_belongs_to_many relation you have to write it as your own as above
scope :has_item, where("#{table_name}.id IN (SELECT categories_items.category_id FROM categories_items")
This will return all categories which have an entry in the join table because, ostensibly, a category shouldn't have an entry there if it does not have an item. You could add a AND categories_items.item_id IS NOT NULL to the subselect condition just to be sure.
In case you're not aware, table_name is a method which returns the table name of ActiveRecord class calling it. In this case it would be "categories".

Find all objects with broken association

I have two models in my rails app with a has many and belongs to association.
Category has many items and Item belongs to category.
These models are associated in the normal way through a category_id column in the Item model.
I'm looking for a quick way of finding all elements in the database with broken associations.
i.e. find all categories that exist with no associated items and items that exist with no associated category.
For example, if I have an item with a category_id of 7, but the category with id 7 has been deleted then this would be considered broken.
For your example, to find items with category_id's for categories which don't exist any more:
Item.where('NOT EXISTS (SELECT * FROM categories where category.id = item.category_id)')
You might want to look at this as well:
A rake task to track down missing database indexes (not foreign keys though, but indexes): https://github.com/eladmeidar/rails_indexes
A very effective way is using find_by_sql to let the database do the heavy lifting:
uncategorized_items = Item.find_by_sql("select * from items where category_id IS NULL")
Another way is a named scope:
class Item < ActiveRecord::Base
scope :uncategorized, where(:category_id => nil) # rails 3
# or...
named_scope :uncategorized, :conditions => 'category_id IS NULL'
end
These are just a couple of ideas. I assume that once you've found these broken associations you plan to fix them, right? You might want to use validates_associated in both models if it's important to you that this not happen again.
You can use find_by_sql and a left outer join to find all the items in one table but not another. Here, I use a downloads table and an image_files table (I've only included the SQL):
SELECT d.*, d.image_file_id
from downloads as d
LEFT OUTER JOIN image_files as i
ON i.id = d.image_file_id
WHERE d.image_file_id IS NULL

Resources