Fetch COUNT(column) as an integer in a query with group by in Rails 3 - ruby-on-rails

I have 2 models Category and Article related like this:
class Category < ActiveRecord::Base
has_many :articles
end
class Article < ActiveRecord::Base
belongs_to :category
def self.count_articles_per_category
select('category_id, COUNT(*) AS total').group(:category_id)
end
end
I'm accessing count_articles_per_category like this
Article.count_articles_per_category
which will return articles that have 2 columns: category_id and total.
My problem is that total column is a string. So the question is: is there a method to fetch that column as an integer?
PS: I tried to do a cast in the database for COUNT(*) and that doesn't help.
I try to avoid doing something like this:
articles = Article.count_articles_per_category
articles.map do |article|
article.total = article.total.to_i
article
end

No, there is no support in ActiveRecord to automatically cast datatypes (which are always transferred as strings to the database).
The way ActiveRecord works when retrieving items is:
for each attribute in the ActiveRecord model, check the column type, and cast the data to that type.
for extra columns, it does not know what data type it should cast it to.
Extra columns includes columns from other tables, or expressions.
You can use a different query, like:
Article.group(:category_id).count
Article.count(:group => :category_id)
These return a hash of :category_id => count. So you might get something like {6=>2, 4=>2, 5=>1, 2=>1, 9=>1, 1=>1, 3=>1}.
Using the count method works because it implicitly lets ActiveRecord know that it is an integer type.

Article.group(:category_id).count might give you something you can use. This will return a hash where each key represents the category_id and each value represents the corresponding count as an integer.

Related

In Rails, how do you write a finder with "where" that compares two dates?

I’m using Rails 4.2. I have a model with these date time attributes
submitted_at
created_at
How do I write a finder method that returns all the models where the submitted_at field occurs chronologically before the created_at field? I tried this
MyModel.where(:submitted_at < :created_at)
But that is returning everything in my database, even items that don’t match.
where(:submitted_at < :created_at) is actually where(false). When you compare two symbols with the lt/gt operators you're actually just comparing them alphabetically:
:a < :b # true
:c > :b # true
where(false) or any other argument thats blank? just returns an "unscoped" relation for chaining.
The ActiveRecord query interface doesn't really have a straight forward way to compare columns like this.
You either use a SQL string:
Resource.where('resources.submitted_at < resources.created_at')
Or use Arel to create the query:
r = Resource.arel_table
Resource.where(r[:submitted_at].lt(r[:created_at]))
The results are exactly the same but the Arel solution is arguably more portable and avoids hardcoding the table name (or creating an ambigeous query).
You can use .to_sql to see what query is generated.
For yours it looks like this:
Resource.where(:submitted_at < :created_at).to_sql
# => SELECT `resources`.* FROM `resources`
If you update like below, you will get some results:
Resource.where('submitted_at < created_at')
# => SELECT * FROM `resources` WHERE (submitted_at < created_at)

Join tables on association

I have three tables:
Product, Object, Info - Product has_many Objects through Product_Objects ...
Object has_many Info, Product has_many Info.
What do I need:
I want to access the values from a column in Info table.
How I do it now:
data = #product.objects
data.infos.where('date(created_at) IN (?)', dates).where(product_id: #product.id).each do |d|
d.value
end
(dates: Date.today, Date.today - 1.day, Date.today - 1.week, Date.today - 1.month)
Is there a way to this differently, because I have a lot of records, and is very slow.
Expected result: [name, value]
Name is a column in the Object table and value is a integer from the Info table, column value.
Thank you
You can speed it up by getting rid of date function and by using pluck instead of iterating over all records (assuming value is a column on Info model). To do this, create a scope on Info model:
scope :created_at, ->(dates) { where(created_at: Array.wrap(dates).map {|date| date..(date + 1.day)}) }
Then you can call:
data = #product.objects
data.infos.created_at(dates).where(product_id: #product.id).pluck(:value)
UPDATE:
Since those two columns are on separate tables, you will need to use joins method. This makes it slightly trickier to use the scope defined above - we need to use merge. ALso pluck do not allows to get more than one column at the time, so we need to use nice select_all trick:
data = #product.objects
query = data.joins(:infos).merge(Info.created_at(dates)).where(product_id: #product.id).select([:name, :value])
ActiveRecord::Base.connection.select_all(query)

ActiveRecord query array intersection?

I'm trying to figure out the count of certain types of articles. I have a very inefficient query:
Article.where(status: 'Finished').select{|x| x.tags & Article::EXPERT_TAGS}.size
In my quest to be a better programmer, I'm wondering how to make this a faster query. tags is an array of strings in Article, and Article::EXPERT_TAGS is another array of strings. I want to find the intersection of the arrays, and get the resulting record count.
EDIT: Article::EXPERT_TAGS and article.tags are defined as Mongo arrays. These arrays hold strings, and I believe they are serialized strings. For example: Article.first.tags = ["Guest Writer", "News Article", "Press Release"]. Unfortunately this is not set up properly as a separate table of Tags.
2nd EDIT: I'm using MongoDB, so actually it is using a MongoWrapper like MongoMapper or mongoid, not ActiveRecord. This is an error on my part, sorry! Because of this error, it screws up the analysis of this question. Thanks PinnyM for pointing out the error!
Since you are using MongoDB, you could also consider a MongoDB-specific solution (aggregation framework) for the array intersection, so that you could get the database to do all the work before fetching the final result.
See this SO thread How to check if an array field is a part of another array in MongoDB?
Assuming that the entire tags list is stored in a single database field and that you want to keep it that way, I don't see much scope of improvement, since you need to get all the data into Ruby for processing.
However, there is one problem with your database query
Article.where(status: 'Finished')
# This translates into the following query
SELECT * FROM articles WHERE status = 'Finished'
Essentially, you are fetching all the columns whereas you only need the tags column for your process. So, you can use pluck like this:
Article.where(status: 'Finished').pluck(:tags)
# This translates into the following query
SELECT tags FROM articles WHERE status = 'Finished'
I answered a question regarding general intersection like queries in ActiveRecord here.
Extracted below:
The following is a general approach I use for constructing intersection like queries in ActiveRecord:
class Service < ActiveRecord::Base
belongs_to :person
def self.with_types(*types)
where(service_type: types)
end
end
class City < ActiveRecord::Base
has_and_belongs_to_many :services
has_many :people, inverse_of: :city
end
class Person < ActiveRecord::Base
belongs_to :city, inverse_of: :people
def self.with_cities(cities)
where(city_id: cities)
end
# intersection like query
def self.with_all_service_types(*types)
types.map { |t|
joins(:services).merge(Service.with_types t).select(:id)
}.reduce(scoped) { |scope, subquery|
scope.where(id: subquery)
}
end
end
Person.with_all_service_types(1, 2)
Person.with_all_service_types(1, 2).with_cities(City.where(name: 'Gold Coast'))
It will generate SQL of the form:
SELECT "people".*
FROM "people"
WHERE "people"."id" in (SELECT "people"."id" FROM ...)
AND "people"."id" in (SELECT ...)
AND ...
You can create as many subqueries as required with the above approach based on any conditions/joins etc so long as each subquery returns the id of a matching person in its result set.
Each subquery result set will be AND'ed together thus restricting the matching set to the intersection of all of the subqueries.

how to query a limited set of records with ActiveRecord

This has been driving me crazy for the last couple of hours as I'm sure there must be a simple solution. Let's say I have the following models:
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
end
And the Comment model has an attribute called Flagged. Assume the post has ten comments and the first two and last two have been marked as flagged.
I want to get a count of how many of the first 5 comments of a post have been flagged. In this case I would want to return 2. So at first I tried:
post.comments.limit(5).where(comments: { flagged: true }).count
But this returns 4 which makes sense because it's finding the first 5 records where flagged is true. My question is, how can I do the count on only the limited resultset? I tried:
first_five_comments = post.comments.limit(5)
first_five_comments.where(flagged: true).count
This also returns 4 as it's just chaining the relations together and executing the same query as above.
I know I could do this with a straight SQL statement, but it just seems like there should be a more Rails way to do it. Do I have to add a .all to the above statement and then do the count within the returned array? Obviously this doesn't work:
first_five_comments = post.comments.limit(5).all
first_five_comments.where(flagged: true).count
because I can't use "where" on an array. If I do have to do it like this, how would I search within the array the get the count?
Any help is appreciated!
You need to filter the array and then count it's elements.
post.comments.limit(5).select{ |comment| comment.flagged? }.size
Or shorter:
post.comments.limit(5).select(&:flagged?).size
Note: select is a method of Array, it does not have anything to do with SQL Select statement.

Rails - find_by_sql - Querying with multiple values for one field

I'm having trouble joining the values for querying multiple values to one column. Here's what I got so far:
def self.showcars(cars)
to_query = []
if !cars.empty?
to_query.push cars
end
return self.find_by_sql(["SELECT * FROM cars WHERE car IN ( ? )"])
end
That makes the query into:
SELECT * FROM cars WHERE car IN (--- \n- \"honda\"\n- \"toyota\"\n')
It seems find_by_sql sql_injection protection adds the extra characters. How do I get this to work?
Do you really need find_by_sql? Since you're performing a SELECT *, and assuming your method resides on the Car model, a better way would be:
class Car < ActiveRecord::Base
def self.showcars(*cars)
where('car in :cars', :cars => cars)
# or
where(:car => cars)
end
end
Note the * right after the parameter name... Use it and you won't need to write code to make a single parameter into an array.
If you really need find_by_sql, try to write it this way:
def self.showcars(*cars)
find_by_sql(['SELECT * FROM cars where car in (?)', cars])
end
Try joining the to_query array into a comma separated string with all values in single quotes, and then passing this string as a parameter "?".
Problem resolve.
def self.average_time(time_init, time_end)
query = <<-SQL
SELECT COUNT(*) FROM crawler_twitters AS twitter WHERE CAST(twitter.publish AS TIME) BETWEEN '#{time_init}' AND '#{time_end}'
GROUP BY user) AS total_tweets_time;
SQL
self.find_by_sql(sanitize_sql(query))
end

Resources