query include methods rails - sql

I have a Student model and that model has_many subscriptions. I need to return all students who have subscriptions. this object must contain only the student's name, email, phone and status, in addition to the subscription id and recurrence_code. I thought something more or less like this but I can't succeed in the query:
students = Student.all.includes(:subscriptions)
students.each do |student|
student.select(:id, :name, :email, :phone, :status, subscription: {
methods: [:id, :recurrency_code]}
end

This is a classic inner join scenario
class Student < ApplicationRecord
has_many :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :student
end
I find it helpful to break these problems into steps:
"Only Students where a Subscription record is present" is a standard inner join:
Student.joins(:subscriptions).uniq
"object must contain only the student's name, email, phone and status"
Student.joins(:subscriptions).select(:name, :email, :phone, :status).uniq
"in addition to the subscription id and recurrence_code"
students = Student.joins(:subscriptions)
.select(
'students.name, students.email,'\
'students.phone, students.status, '\
'subscriptions.id as subscription_id,'\
'subscriptions.recurrence_code as subscription_recurrence_code'
)
A few notes:
1. Using select with joins
#vee's SO Answer here points out:
If the column in select is not one of the attributes of the model on which the select is called on then those columns are not displayed. All of these attributes are still contained in the objects within AR::Relation and are accessible as any other public instance attributes.
This means if you load an individual record (e.g. students.first), you will only see the Student attributes by default. But you can still access the Subscription attributes by the as name you set in the query. E.g.:
students.first.subscription_recurrence_code
2. Use .uniq to eliminate duplicates.
Because of the has_many relationship, the query Student.joins(:subscriptions) will return a record for each subscription, which means each student will appear in the result the same number of times as they have subscriptions. Calling .uniq (short for unique) will remove duplicates from the result.

I'm agree with the Chiperific response, but I disagree to use the uniq method because it doesn't call the 'DISTINCT' in the SQL query.
Rails: uniq vs. distinct
For me it's better to use distinct. So the query could be as this:
Student.joins(:subscriptions).distinct.select(
:name, :email, :phone, :status,
'subscriptions.id AS subscription_id',
'subscriptions.recurrence_code'
)

Related

Rails: Query with inner join, ordering by association and no duplicate records

So I'm trying to create the ability to search appointments in my app and am having trouble with duplicate records and ordering.
To summarize:
An appointment is between two users with one being the booker and one being the bookee being booked. An appointment also has_many :time_options that a user can suggest, and then when the bookee selects and confirms one of these suggested time_options the start_time is applied to the similar start_time attribute on appointments. The models are structured as follows:
#appointment.rb
belongs_to :booker, foreign_key: :booker_id,
class_name: 'Profile'
belongs_to :bookee, foreign_key: :bookee_id,
class_name: 'Profile'
has_many :time_options, dependent: :destroy
scope :order_by_start_time, -> {
joins(:time_options)
.order("COALESCE(appointments.start_time, time_options.start_time) DESC")
}
def self.search(search)
if search
search_term = "%#{search}%"
joins(
"INNER JOIN profiles ON profiles.id IN (appointments.booker_id,
appointments.bookee_id)"
)
.where(
"CONCAT_WS(' ', profiles.first_name, profiles.last_name) ILIKE :term
OR appointments.service_title ILIKE :term",
term: search_term
)
else
where(nil)
end
end
#time_option.rb
belongs_to :appointment, touch: true
belongs_to :profile
#profile.rb
belongs_to :user, touch: true
has_many :booker_appointments, foreign_key: :booker_id,
class_name: 'Appointment',
dependent: :nullify
has_many :bookee_appointments, foreign_key: :bookee_id,
class_name: 'Appointment',
dependent: :nullify
I'm trying to let users search appointments by their appointment's service_title, or the other user's name (whether they are the booker or the bookee). I then want to order those search results by start_time. Either the start_time of the Appointment itself (which is only set when an appointment is confirmed), or if the appointment hasn't been confirmed yet and it's start_time is nil, then to instead use the earliest start_time of the associated time_options.
So in the controller, I'm doing something like:
def index
#appointments = Appointment.all_profile_appointments(#profile)
if params[:search]
#upcoming_appointments = #appointments.search(params[:search])
.order_by_start_time
.page(params[:page])
.per(12)
else
#upcoming_appointments = #appointments.order_by_start_time
.page(params[:page])
.per(12)
end
end
However, my search method returns duplicates, I believe because of the INNER JOIN. And if I add .distinct before or after the order_by_start_time method, I get the wonderful error: PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list
So I'm pretty sure I just need to write a more complex, precise SQL statement; but, between needing to join the profiles table to search by name, needing join/coalesce the time_options table to order by start_time I'm not quite sure where to go next. For background, I'm using postgresql and Rails 4.
TL;DR: I'm trying to search for Appointment records by the name of the Profile who either booked or was booked, for the appointment. I'm then looking to order that query by appointments.start_time, or if no appointment start_time has been confirmed, then by the earliest time_options.start_time associated with the appointment. And I'm trying to do this without a pile of duplicate records.

Rails select by number of associated records

I have following models in my rails app:
class Student < ApplicationRecord
has_many :tickets, dependent: :destroy
has_and_belongs_to_many :articles, dependent: :destroy
class Article < ApplicationRecord
has_and_belongs_to_many :students, dependent: :destroy
class Ticket < ApplicationRecord
belongs_to :student, touch: true
I need to extract all Students who has less than articles and I need to extract all Students who's last ticket title is 'Something'.
Everything I tried so far takes a lot of time. I tried mapping and looping through all Students. But I guess what I need is a joined request. I am looking for the most efficient way to do it, as database I am working with is quite large.
go with #MCI's answer for your first question. But a filter/select/find_all or whatever (although I havn't heared about filter method in ruby) through students record takes n queries where n is the number of student records (called an n+1 query).
studs = Student.find_by_sql(%{select tmp.id from (
select student_id as id from tickets where name='Something' order by tickets.created_at desc
) tmp group by tmp.id})
You asked
"I need to extract all Students who has less than articles". I'll presume you meant "I need to extract all Students who have less than X articles". In that case, you want group and having https://guides.rubyonrails.org/active_record_querying.html#group.
For example, Article.group(:student_id).having('count(articles.id) > X').pluck(:student_id).
To address your second question, you can use eager loading https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations to speed up your code.
result = students.filter do |student|
students.tickets.last.name == 'Something'
end
Here association is HABTM so below query should work
x = 10
Student.joins(:articles).group("articles_students.student_id").having("count(articles.id) < ?",x)

Compare the last element in Rails active record

I have two models Student and StudentRecord. Student Record has start_date, end_date and class_course_id among its attributes and it belongs to Student
scope = Student.eager_load(:student_record)
Now, I want to get the students whose latest(according to start_date) student_record's class_course_id is the same as a given class_course_id. something like:
scope = scope.where("student_records.order(start_date asc).last.class_course_id = ?", params[:class_course_id])
Obviously, the above statement is not correct but I hope it describes what I would like to get.
The below should do
Student.eager_load(:student_record).where("student_records.class_course_id = ?", params[:class_course_id]).order('student_records.start_date asc').last
Use Order by clause and descend to get the latest dates .order("student_records.start_date DESC") and in the where clause, the records will be filtered out .where("student_records.class_course_id = ?", params[:class_course_id]). The where will come first then, the order by desc will sort it correctly.
scope.where("student_records.class_course_id = ?", params[:class_course_id]).order("student_records.start_date DESC")
And you can do .limit(5) to get the first 5 records that are the latest start_dates.
If you want all students, then this is a bit non-trivial in active record.
Identifying the last Student Record sounds like an important thing that would benefit from a scope:
def self.latest_for_student
where("not exists (select null from student_records sr2 where sr2.student_id = student_records.student_id and sr2.start_date > student_records.start_date)")
end
... which means "return rows for which there does not exist another row in student_records for the same student_id, and a greater start_date"
Or...
def self.latest_for_student
where("student_records.start_date = (select max(start_date) from student_records sr2 where sr2.student_id = student_records.student_id)")
end
... which means "return rows for which the start date is equal to the maximum start date for that student id"
Then you can:
class Student
has_one :last_student_record, -> {merge(StudentRecord.latest_for_student)}, :class_name => "StudentRecord"
has_one :last_class_course, :through => :last_student_record, :source => :class_course
end
class ClassCourse
has_many :last_records_for_student, -> {merge(StudentRecord.latest_for_student)}, :class_name => "StudentRecord"
has_many :students_having_as_last_course, :through => : last_records_for_student, :source => :student
end
Then you should be able to:
#course.students_having_as_last_course
Bit complex ... could be syntax errors ... let me know if there are.

Selecting models through Rails relations?

I have these (simplified) models:
Address
public (Boolean)
has_one :group_member
Group
has_many :Group_Members
belongs_to :User
Group_Member
belongs_to :group
belongs_to :address
User
has_many :groups
I want to select all addresses where public is true and all the addresses which User can access through Groups.
I assume it's something along the lines of:
Address.where(public: TRUE).joins(:group_member)
Am I somewhere close?
I'm using Rails 4 and PostgreSQL as my database if it helps anyone.
I think this will work:
Address.joins(group_member: :group).where(is_public: true, groups: {user_id: 12345})
Let's break this down.
First we are calling on the Address model, b/c that is what we want to return.
.joins(:group_member) will join addresses to group_members via the Address has_one :group_member relation.
However, we actually want to go further, and join the group connected with the group_member, so we use a nested join, which is why it looks like this joins(group_member: :group) to indicate that we join address -> group_member, then group_member -> group.
Next the where clause. There are 2 conditions, we want:
Public addresses only, which is indicated as a column on the address, so we add this:
.where(is_public: true).
We want only where a particular user is connected with a group (and so its group_members and addresses). For this we need to add a nested where clause, like so groups: {user_id: 12345}.
The result of combining these is:
where(is_public: true, groups: {user_id: 12345})
So all together, the line of code above should get what you want.
Try the following -- it is not a single query but I think it is better than a single query with big join table:
public_addresses = Address.where(is_public: true)
# => get all public addresses
user_addresses = current_user.groups.includes(:group_members => :address).
# includes is to eager load records to avoid N+1 queries
flat_map{|g| g.group_members.map(&:address)}
# inner block returns array of addresses for each group
# flat_map converts the array of arrays to single level
# => get all addresses associated with the user
all_addresses = (public_addresses + user_addresses).uniq
# => remove duplicates
To speed up the query add indices for slower queries. For e.g
add_index :groups, :user_id
# this speeds up finding groups for a given user
add_index :group_members, :group_id
# this speeds up finding group_members for a given group
add_index :addresses, :group_member_id
# this speeds up finding addresses for a given group_member
Other Options is to get user_addresses using join tables
user_addresses = Address.joins(group_member: group).where(groups: {user_id: current_user.id} )

Complex Query with Has many and belongs to for RAILS 3

I am trying to do a QUERY in my controller to get a list of suppliers with a category ID.
I have my models set up like this.
class Supplier < ActiveRecord::Base
has_and_belongs_to_many :sub_categories
end
class Category < ActiveRecord::Base
has_many :sub_categories
end
class SubCategory < ActiveRecord::Base
belongs_to :category
has_and_belongs_to_many :suppliers
end
A supplier can have Many sub_categories that are under one single category. So i can grab the category of a supplier by doing this.
#supplier.sub_categories.first.category.name
This returns the category that the supplier comes under because they have to have at least 1 sub category which is then linked to a category.
What i am trying to do is by passing a category_id i wish to return all suppliers that come under that category.
I had it written like this but it doesnt seem to be working.
#category = Category.find(params[:category_id])
#suppliers = Supplier.where('sub_category.first.category.id = ?', #category.id)
i get the following sql error
Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '.id = 20)' at line 1: SELECT `suppliers`.* FROM `suppliers` WHERE (sub_category.first.category.id = 20)
Well, that's certainly an sql error. The stuff inside the where() call gets translated directly to SQL, and that's not sq;l. :)
You need to join tables together. I'm assuming there's a sub_category_suppliers table that completes the habtm association. (BTW, I much prefer to use has_many :through exclusively)
I think it would be something like this:
Supplier.joins(:sub_category_suppliers => :sub_categories).
where('sub_categories.category_id =?', #category.id).
group('suppliers.id')
As Caley Woods suggested, this should be placed in the Supplier model as a scope:
scope :by_category, lambda { |category_id|
joins(:sub_category_suppliers => :sub_categories).
where('sub_categories.category_id =?', category_id).
group('suppliers.id')
}
and then called as Supplier.by_category(#category.id)