Compare the last element in Rails active record - sql

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.

Related

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)

Query using condition within an array

I have 2 models, user and centre, which have a many to many relationship.
class User < ActiveRecord::Base
attr_accessible :name
has_and_belongs_to_many :centres
end
class Centre < ActiveRecord::Base
attr_accessible :name, :centre_id, :city_id, :state_id
has_and_belongs_to_many :users
end
Now I have an user with multiple centres, and I want to retrieve all the centres that have the same "state_id" as that user.
This is what I am doing now
state_id_array = []
user.centres.each do |centre|
state_id_array << centre.state_id
end
return Centre.where("state_id IN (?)", state_id_array).uniq
It works, but it's very ugly. Is there a better way for achieving this? Ideally a one line query.
UPDATE
Now I have
Centre.where('centres.state_id IN (?)', Centre.select('state_id').joins(:user).where('users.id=(?)', user))
The subquery work by itself, but when I tried to execute the entire query, I get NULL for the inner query.
Centre.select('state_id').joins(:user).where('users.id=(?)', user)
will generate
SELECT state_id FROM "centres" INNER JOIN "centres_users" ON "centres_users"."centre_id" = "centres"."id" INNER JOIN "users" ON "users"."id" = "centres_users"."user_id" WHERE (users.id = (5))
Which return 'SA', 'VIC', 'VIC'
but the whole query will generate
SELECT DISTINCT "centres".* FROM "centres" WHERE (centres.state_id IN (NULL,NULL,NULL))
Does user also has state_id column if yes then try this,
User.joins("LEFT OUTER JOIN users ON users.state_id = centers.state_id")
else
try User.joins(:center)
Solved.
.select(:state_id)
will retrieve a model with only the state_id column populated. To retrieve a field, use
.pluck(:state_id)
Below is the final query I had
Centre.where('centres.state_id IN (?)', Centre.joins(:user).where('users.id=(?)', user).pluck('state_id').uniq)

How to select only unique entries from a join table

I would like to make my block of code more efficient. I have two models and a join table for them. They both have a has_many :through relationship. Some parts belong to multiple groups, some only belong to one. I need to get the records that belong to only one group and in the most efficient manner as there are thousands of parts. Here are my models:
part.rb
class Part < ActiveRecord::Base
attr_accessible :name,
:group_ids
has_many :part_groups, dependent: :destroy
has_many :groups, through: :part_groups, select: 'groups.*, part_groups.*'
end
group.rb
class Group < ActiveRecord::Base
attr_accessible :name,
:part_ids
has_many :part_groups, dependent: :destroy
has_many :parts, through: :part_groups, select: 'parts.*, part_groups.*'
end
part_group.rb
class PartGroup < ActiveRecord::Base
attr_accessible :part_id,
:group_id
belongs_to :part
belongs_to :group
end
What I would like to be able to do is get all the parts that belong only to Group A and only to Group B, but not ones that belong to both A & B. After struggling with this for hours and getting nowhere I'm using this as a stop gap:
#groupA = []
#groupB = []
Part.all.each do |part|
if part.group_ids.length == 1
if part.group_ids.first == 1
#groupA.push(part)
elsif part.group_ids.first == 2
#groupB.push(part)
end
end
end
This obviously isn't scalable as there will be many groups. I've tried various methods of join and include that I've been googling but so far nothing has worked.
I am also new to rails , So as far i understand this is the structure of your tables.
parts
Id | Name
groups
Id | Name
part_groups
Id | part_id | group_id
So you can do the following,
Group.find(1).parts // Parts belong to group A
Group.find(2).parts // Parts belong to group B
so this may give parts that belong to other groups also.
Objective is to get parts that belongs only to group A and only to group B
Try for
Group.find(1).parts.collect{|row| row if row.groups.count==1}.flatten
I think this is better approach than yours , because am traversing only those parts which belong to group1.
The raw sql for this could look like
select parts.* from parts
inner join part_groups on parts.id = part_groups.part_id
left outer join part_groups as group_b on group_b.part_id = parts.id and group_b.group_id = 456
where group_b.id is null and part_groups.group_id = 123
Assuming that group a had id 123 and group b had id 456.
What this does is try to join the part_groups table twice (so an alias needs to be used the second time), once where group_id matches group A and once against group B. The use of the left join allows us to require that the second join (against B) produces no rows.
Activerecord doesn't provide much assistance for this, other than allowing you to pass arbitrary sql fragments to joins, so you end up with something like
Part.select("parts.*").
.joins(:part_groups).
.joins("left outer join part_groups as group_b on group_b.group_id = #{groupb.id} and group_b.part_id = parts.id").
.where(:part_groups => {:group_id => groupa.id}).where("group_b.id is null")
Arel (which underpins the query generation part of active record) can generate this sort of query but this isn't exposed directly.

How to add conditional where clauses in rails

I am a rails newbie and am trying to perform a search on a table with rails, and i'm just using my sql knowledge to do this. But this just doesn't seems like rails or ruby even...
Is there any better way to do what i'm doing below? (basically, only pass date arguments to sql if they are filled)
def search(begin_date=nil, end_date=nil)
subject = " and created_at "
if !(begin_date.nil? || end_date.nil?)
where_part = subject + "BETWEEN :begin_date AND :end_date"
else if (begin_date.nil? && end_date.nil?)
where_part = ""
else if(begin_date.nil?)
where_part = subject + " <= :end_date"
else if (end_date.nil?)
where_part = subject + " >= :begin_date"
end
end
end
end
User.joins(places: {containers: {label: :user}}).where("users.id= :user_id "+where_part, user_id: self.id, begin_date:begin_date, end_date:end_date).group(...).select(...)
end
EDIT
user.rb
has_many :containers
has_many :user_places
has_many :places, through: :user_places
has_many :labels
place.rb
has_many :containers
has_many :user_places
has_many :users, through: :user_places
container.rb
belongs_to :label
belongs_to :place
belongs_to :user
label.rb
belongs_to :user
has_many :containers
Basically, i want to get a count of the number of containers within a given user's labels or with a direct relationship, per location, and want to be able to filter it by begin and end dates.
Either of this dates may be nil, and so i would need to address this in my "query".
My question is : How can i do this the rails way? I took a look at http://guides.rubyonrails.org/active_record_querying.html and perhaps i could use the except command here somewhere...but this relationship model just seems a bit complex to do this with ActiveRecord...how may I?, i really think i should use ActiveRecord, but how?
Thank you
You can apply multiple where calls to a query so you can build your base query:
query = User.joins(...)
.group(...)
.select(...)
.where('users.id = :user_id', :user_id => self.id)
and then add another where call depending on your date interval:
if(begin_date && end_date)
query = query.where(:created_at => begin_date .. end_date)
# or where('created_at between :begin_date and :end_date', :begin_date => begin_date, :end_date => end_date)
elsif(begin_date)
query = query.where('created_at >= :begin_date', :begin_date => begin_date)
elsif(end_date)
query = query.where('created_at <= :end_date', :end_date => end_date)
end
Each where call adds another piece to your overall WHERE clause using AND so something like:
q = M.where(a).where(b).where(c)
is the same as saying WHERE a AND b AND c.
I cant think of a great reason why you would actually want to generate SQL in your code. Active record seems like a much more efficient solution for your needs, unless there is a reason why you cant use that.
Link explaining how to join tables with active record

Rails 3 Order Records By Grand-child Count

I'm trying to do some fairly complicated record sorting that I was having a bit of trouble with. I have three models:
class User < ActiveRecord::Base
has_many :registers
has_many :results, :through => :registers
#Find all the Users that exist as registrants for a tournament
scope :with_tournament_entrees, :include => :registers, :conditions => "registers.id IS NOT NULL"
end
Register
class Register < ActiveRecord::Base
belongs_to :user
has_many :results
end
Result
class Result < ActiveRecord::Base
belongs_to :register
end
Now on a Tournament result page I list all users by their total wins (wins is calculated through the results table). First thing first I find all users who have entered a tournament with the query:
User.with_tournament_entrees
With this I can simply loop through the returned users and query each individual record with the following to retrieve each users "Total Wins":
user.results.where("win = true").count()
However I would also like to take this a step further and order all of the users by their "Total Wins", and this is the best I could come up with:
User.with_tournament_entrees.select('SELECT *,
(SELECT count(*)
FROM results
INNER JOIN "registers"
ON "results"."register_id" = "registers"."id"
WHERE "registers"."user_id" = "users.id"
AND (win = true)
) AS total_wins
FROM users ORDER BY total_wins DESC')
I think it's close, but it doesn't actually order by the total_wins in descending order as I instruct it to. I'm using a PostgreSQL database.
Edit:
There's actually three selects taking place, the first occurs on User.with_tournament_entries which just performs a quick filter on the User table. If I ignore that and try
SELECT *, (SELECT count(*) FROM results INNER JOIN "registers" ON "results"."register_id" = "registers"."id" WHERE "registers"."user_id" = "users.id" AND (win = true)) AS total_wins FROM users ORDER BY total_wins DESC;
it fails in both PSQL and the ERB console. I get the error message:
PGError: ERROR: column "users.id" does not exist
I think this happens because the inner-select occurs before the outer-select so it doesn't have access to the user id before hand. Not sure how to give it access to all user ids before than inner select occurs but this isn't an issue when I do User.with_tournament_entires followed by the query.
In your SQL, "users.id" is quoted wrong -- it's telling Postgres to look for a column named, literally, "users.id".
It should be "users"."id", or, just users.id (you only need to quote it if you have a table/column name that conflicts with a postgres keyword, or have punctuation or something else unusual).