Making a passenger top in rails 4 from a belongs_to table - ruby-on-rails

I have 2 classes that have associations with each other
class Trip < ActiveRecord::Base
has_many :passengers
end
and a class
class Passenger < ActiveRecord::Base
belongs_to :trip
end
And my Idea is to make a top of X passengers that have been to most trips.
I have found out that you can count entries of each passenger using something like
def count_passenger_trips
pas_names = Passenger.pluck(:name)
pas_names.each do |p|
puts p
puts Passenger.where(:name => p).count
pas_names.delete(p)
end
end
But it does not really return the right result(always keeping the last entry in the array and not counting it in/deleting it from the array)
I guess I could make a new array with key-value pairs and then order them by counts. Wanted to know what might be the problem for not counting in the last passenger and also maybe there is a simpler way to do this(because I want to add a block of Top passengers to my webpage).

If name is the unique identifier for these Passenger entities, you could do something like this:
Passenger.group(:name).count
This should give you back results like:
{
'Jane Doe' => 25,
'Bob Smith' => 35,
'Heywood Johnson' => 1,
}
These can be pricey queries if you don't have indexes set up for them, so beware.
If you wanted to, say, only find people with at least 10 trips, you could do this:
Passenger.group(:name).having('count(*) >= 10').count

Related

Rails count linked tables

I have three objects that are linked to each other. It looks like this:
tree[
id: 1,
name: "name"
]
branch[
id: 1,
tree_id: 1
]
leaf[
id: 1,
branch_id:1
]
I need to know how many leaves belong to a every tree so I created a nested for loop:
update
I updated code:
results = []
trees = Tree.all
trees.each do |tree|
branches = Branch.where(tree_id: tree.id)
branches.each do |branch|
leaves_count = leaves.where(branch_id: branch.id).count
end
results.push( {
tree: tree.name,
leaves: leaves_count
})
end
with this code I receive an array of objects like so:
results = [{
tree: "oak",
leaves: 4
}, {
tree: "redwood",
leaves: 6
}]
I would like the same output but in a rails way
This works but feels realy clunky. Does anyone know a rails way of fixing this?
update solved
Yury Lebedev's answer is correct but through his previous answer I found another way. So this works (yury's answer):
Tree.find_each do |tree|
Branch.where(tree_id: tree.id).joins(:leaves).count('leaves.id')
end
This also works:
Tree.find_each do |tree|
Tree.joins(branches: :leaves).where(id: tree.id).count('leaves.id')
end
As a continuation question (not sure if I'm supposed to do that on answered questions?) I would like to know i one is better/faster then the other or whether they'r the same.
You can try it with a join query (this will only make one query to the db):
Leaf.joins('inner join branches on leaves.branch_id = branches.id')
.group('branches.tree_id').count
This will give you a hash with tree_id as a keys, and leaves count as values.
If you want to iterate through the trees, and get the leaves count for each of them, you can do this (this will make n queries to the db, if you have n trees):
trees.each do |tree|
leaves_count = Branch.where(tree_id: tree.id).joins('inner join leaves on leaves.branch_id = branches.id')
.count('leaves.id')
end
And this can be done even easier, if you have associations in your models:
class Three < ActiveRecord::Base
has_many :branches
has_many :trees, through: :branches
end
class Branch < ActiveRecord::Base
belongs_to :tree
has_many :leaves
end
class Leaf < ActiveRecord::Base
belongs_to :branch
delegate :tree, to: :branch
end
Then for a single query:
Leaf.joins(:branch).group('branches.tree_id').count
And for a query for each tree:
Tree.find_each do |tree|
Branch.where(tree_id: tree.id).joins(:leaves).count('leaves.id')
end

Rails: Joining the same table twice and querying a joined attribute

I have a Rails app that parses price information from a variety of sources. The models look something like this:
class Price
attr_accessor :value
belongs_to :product
belongs_to :added_pricelist, :class_name => "Scrape"
belongs_to :removed_pricelist, :class_name => "Scrape"
end
class Product
attr_accessor :name
end
class PriceList
attr_accessor :created_at
has_many :prices
end
Every day, a scraper runs, and parses some prices for products from an API. It creates a new PriceList every time the scraper is run. The scraper records which pricelist a price appeared (or disappeared) in - so the data might look like this.
# NB: Assume product_id is always 1 for these
{ value: 100, added_pricelist_id: 1, removed_pricelist_id: 2 }
{ value: 120, added_pricelist_id: 2, removed_pricelist_id: 3 }
{ value: 140, added_pricelist_id: 3, removed_pricelist_id: 4 }
A new object is only created when the price changes, so if it stayed the same, you'd just have:
{ value: 100, added_pricelist_id: 1, removed_pricelist_id: 4 }
My question is: Using ActiveRecord, how can I find the average price of a product for each of the last 30 days? I need to be able to find all the prices that were active on a particular day, and then average that number. Can that be done in a single query? (NB: This is with Postgres).
Feel free to get me to clarify anything that doesn't make any sense, and thanks in advance for helping!
AVG
Although I was a little confused with what you posted, hopefully I can give you some help:
You have at least two options:
ActiveRecord .average
SQL AVG
Both of these perform the same task (except one is ActiveRecord based), so I would look at firstly getting your SQL Query correct, and then performing the AVG function on the data you have
Query
I need to be able to find all the prices that were active on a
particular day, and then average that number
I would do something like this:
#app/models/PriceList.rb
Class PriceList < ActiveRecord::Base
scope :today, -> { where(created_at: DateTime.now.at_beginning_of_day.utc..Time.now.utc).average(:value) }
end
This would allow you to call #product.pricelist.today

ActiveRecord group by on a join

Really been struggling trying to get a group by to work when I have to join to another table. I can get the group by to work when I don't join, but when I want to group by a column on the other table I start having problems.
Tables:
Book
id, category_id
Category
id, name
ActiveRecord schema:
class Category < ActiveRecord::Base
has_many :books
end
class Book < ActiveRecord::Base
belongs_to :category
end
I am trying to get a group by on a count of categories. I.E. I want to know how many books are in each category.
I have tried numerous things, here is the latest,
books = Book.joins(:category).where(:select => 'count(books.id), Category.name', :group => 'Category.name')
I am looking to get something back like
[{:name => fiction, :count => 12}, {:name => non-fiction, :count => 4}]
Any ideas?
Thanks in advance!
How about this:
Category.joins(:books).group("categories.id").count
It should return an array of key/value pairs, where the key represents the category id, and the value represents the count of books associated with that category.
If you're just after the count of books in each category, the association methods you get from the has_many association may be enough (check out the Association Basics guide). You can get the number of books that belong to a particular category using
#category.books.size
If you wanted to build the array you described, you could build it yourself with something like:
array = Categories.all.map { |cat| { name: cat.name, count: cat.books.size } }
As an extra point, if you're likely to be looking up the number of books in a category frequently, you may also want to consider using a counter cache so getting the count of books in a category doesn't require an additional trip to the database. To do that, you'd need to make the following change in your books model:
# books.rb
belongs_to :category, counter_cache: true
And create a migration to add and initialize the column to be used by the counter cache:
class AddBooksCountToCategories < ActiveRecord::Migration
def change
add_column :categories, :books_count, :integer, default: 0, null: false
Category.all.each do |cat|
Category.reset_counters(cat.id, :books)
end
end
end
EDIT: After some experimentation, the following should give you close to what you want:
counts = Category.joins(:books).count(group: 'categories.name')
That will return a hash with the category name as keys and the counts as values. You could use .map { |k, v| { name: k, count: v } } to then get it to exactly the format you specified in your question.
I would keep an eye on something like that though -- once you have a large enough number of books, the join could slow things down somewhat. Using counter_cache will always be the most performant, and for a large enough number of books eager loading with two separate queries may also give you better performance (which was the reason eager loading using includes changed from using a joins to multiple queries in Rails 2.1).

Rails find conditions... where attribute is not a database column

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

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