custom attributes in rails - ruby-on-rails

I have a Student model and a method that does some calculations and returns a value
class Student < ActiveRecord::Base
def total_result
#some calculations
return result
end
end
Now in my students controller I would like to do the following
Student.where("total_result > ?", params[:result])
but this brings a PG::UndefinedColumn: ERROR. I am using postgres. How do I achieve this?

You could use:
Student.select { |student| student.total_result > params[:result] }
A word of warning: This will load all students from the database and calculate the value for each of them. This will be slow depending on the number of students in the table.
If you need this more frequently then it would make sense to store/cache the result of the calculation in the database.

Related

Count total bookings by category - Rails

My BookingGroup has_many Booking. Booking contains column category where the data can be "adult" or "child_infant" or child_normal.
Now I want to count all total %child% and display it in my index view table
I was'nt sure whether this could be done in one line or I have to use a scope, this is where I stucked.
BookingGroup model
def search_by_category
bookings.visible.map(&:category).inject(:+)
end
Assuming category is a string column, you should be able to count it like that :
bookings.visible.where("category LIKE ?", "child%").count
bookings.visible.where(category: ["child_infant", "child_normal"]).count
We can use LIKE just as in SQL with active record
In your BookingGroup model
def search_by_category
bookings.visible.where('category LIKE ?', '%child%').size
end
But, if you do so for many booking_groups, your code will have N+1 queries issue. You can use eager load in your controller
#booking_groups = BookingGroup.joins(:bookings).select('booking_groups.*', 'count(*) as total_bookings').where('bookings.category LIKE ?', '%child%').group(:id)
Then you can
#booking_groups.first.total_bookings

Are .select and or .where responsible for causing N+1 queries in rails?

I have two methods here, distinct_question_ids and #correct_on_first attempt. The goal is to show a user how many distinct multiple choice questions have been answered that are correct.
The second one will let me know how many of these distinct MCQs have been answered correctly on the first attempt. (A user can attempt a MCQ many times)
Now, when a user answers thousands of questions and has thousands of user answers, the page to show their performance is taking 30 seconds to a minute to load. And I believe it's due to the .select method, but I don't know how to replace .select without using .select, since it loops just like .each
Is there any method that doesn't cause N+1?
distinct_question_ids = #user.user_answers.includes(:multiple_choice_question).
where(is_correct_answer: true).
distinct.pluck(:multiple_choice_question_id)
#correct_on_first_attempt = distinct_question_ids.select { |qid|
#user.user_answers.
where(multiple_choice_question_id: qid).first.is_correct_answer
}.count
.pluck returns an Array of values, not an ActiveRecord::Relation.
So when you do distinct_question_ids.select you're not calling ActiveRecord's select, but Array's select. Within that select, you're issuing a fresh new query against #user for every id you just plucked -- including ones that get rejected in the select.
You could create a query named distinct_questions that returns a relation (no pluck!), and then build correct_on_first_attempt off of that, and I think you'll avoid the N+1 queries.
Something along these lines:
class UserAnswer < ActiveRecord::Base
scope :distinct_correct, -> { includes(:multiple_choice_question)
.where(is_correct_answer: true).distinct }
scope :first_attempt_correct, -> { distinct_correct
.first.is_correct_answer }
end
class User < ActiveRecord::Base
def good_guess_count
#correct_on_first_attempt = #user.user_answers.distinct_correct.first_attempt_correct.count
end
end
You'll need to ensure that .first is actually getting their first attempt, probably by sorting by id or created_at.
As an aside, if you track the attempt number explicitly in UserAnswer, you can really tighten this up:
class UserAnswer < ActiveRecord::Base
scope :correct, -> { where(is_correct_answer: true) }
scope :first_attempt, -> { where(attempt: 1) }
end
class User < ActiveRecord::Base
def lucky_guess_count
#correct_on_first_attempt = #user.user_answers.includes(:multiple_choice_question)
.correct.first_attempt.count
end
end
If you don't have an attempt number in your schema, you could .order and .group to get something similar. But...it seems that some of your project requirements depend on that sequence number, so I'd recommend adding it if you don't have it already.
ps. For fighting N+1 queries, use gem bullet. It is on-point.

Problems with displaying the correct number of items in an has_and_belongs_to_many association

I have a model UseCases (about 6.000 rows) and EducationalObjectives (about 4.000 rows) associated with has_and_belongs_to_many(EducationalObjectivesUseCases with about 8.000 rows). Some of the EducationalObjectives belong to subjectA (about 4.500 rows in EducationalObjectivesUseCases) and some to subjectB (about 3.500 rows in EducationalObjectivesUseCases).
Now I want to display a list of all UseCases which are tied to the EducationalObjectives of the subjectA which should be about 3.500 rows but I get about 4.500 rows (you've guessed it: the number of associations within EducationalObjectivesUseCases) since duplicate entries (UseCases with many EducationalObjectives on subjectA) are displayed the number of times of entries.
My thinking was that I only can tell through the HABTM association that I need the UseCases for subjectA but don't know how the avoid duplicate entries.
class UseCase < ApplicationRecord
has_and_belongs_to_many :educational_objectives
end
class EducationalObjective < ApplicationRecord
has_and_belongs_to_many :use_cases
end
class EducationalObjectivesUseCase < ApplicationRecord
belongs_to :educational_objective
belongs_to :use_case
end
class UseCasesController < ApplicationController
def index
#use_cases = UseCase.all.
order(:use_case).
joins(:educational_objectives).
where('educational_objectives.subject_id = ?',2)
end
end
How do I get Rails to display only the used UseCases for subjectA once (only 3.500 rows)? Where is my mistake?
Thanks in advance!
The quickest way to solve this is to call #distinct on the where-chain. Since the select is automatically set to use_cases.* this will work and filter out duplicated records.
def index
#use_cases = UseCase.joins(:educational_objectives)
.where(educational_objectives: {subject_id: 2})
.order(:use_case)
.distinct
end
Alternatively this can be solved using a sub-query.
def index
educational_objectives = EducationalObjective.where(subject_id: 2)
use_case_ids = EducationalObjectivesUseCase
.where(educational_objective_id: educational_objectives)
.select(:use_case_id)
#use_cases = UseCase.where(id: use_case_ids).order(:use_case)
end
edit
The sub-query code will execute 1 SQL query, just like the code for the distinct version. When executed on the console suffix each statement with ;nil to prevent execution by the #inspect method (used to show you the result). If you don't do this the console will try to show the result and trigger the query before we are ready executing it. It will still work, but it looks like it are multiple queries.

Rails sql query with dynamic attribute

Say I have an ActiveRecord object that contains a quantity and a price stored in the database.
I have defined a accessor for the total_price:
def total_price
quantity * price
end
Now what if I want to use this dynamic "attribute" in multiple ActiveRecord query contexts? I might to sum on it, compute average, for multiple scope, etc.
What would be the best practices so that I don't have to repeat this quantity * price with ActiveRecord and if I don't want to denormalize by writing it in DB?
Thanks!
Well we wanted to get caption (from join model) to appear on our associated image model (I.E if you called #user.images, you'd be able to call image.caption (even though caption was in the join model)
So we looked at this RailsCast (you'll benefit from around 6:40) which gave us some information about how you can use join to create more dynamic queries. We ended up using this:
has_many :images, -> { select("#{Image.table_name}.*, #{ImageMessage.table_name}.caption AS caption") }
I'm thinking you could use something similar for your request (include some SQL to create the pseudo column in the object). Since it's the origin model, I'm thinking about a scope like this:
default_scope select("(table.quantity * table.price) as total_price")
I assume price is stored in the database. Is quantity stored in the database? If both are stored, why not make total_price a database column as well? You can update total_price whenever you update the record.'
class Order < AR::Base
before_update :update_total_price
def update_total_price
self[:total_price] = quantity * price
end
end
Obviously you can do anything you would with an ordinary column, like Order.where("total_price > 1.0") and what-not.

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

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.

Resources