Adding a JOIN between two tables - ruby-on-rails

Here is the classes I have:
Model Organization
has_many Students
Model Student
has_many Classes
belongs_to Organization
Model Class
a field named : price
belongs_to Student
scope :top_expensive_classes, joins(:students).order('price DESC')
Now I want to list the top 10 expensive classes
At least the first problem I have is that in the params I have the organization_id to filter based on that But I write my controller like this which does NOT work because it thinks it should find organization_id in the Class model but it is in the Student model.
#results = Class.top_expensive_classes.where(organization_id: params[:id]).limit(RESULT_SET_COUNT)
So I was wondering if there is a way to fix this? I think I should introduce a new join somewhere? but couldn't figure it out.

There's a typo in your scope: joins:(:programs) should be joins(:programs)
To fetch based on the organization id in Student you may be able to do this:
#results = Class.top_expensive_classes
.joins(student: :organization)
.where(organization: {id: params[:id]})

Related

Rails Query All Students Inside School

I'm fairly new to rails and I'm trying to make a database app for a school as practice. So here's the ERD that I constructed:
What I want to have is a list of students under a school. I know how to retrieve the teachers under a school and the students under the teacher it's basically just school.teachers and teacher.students but I don't know how to get what school -> students.
You can setup a has many through relation:
class Teacher < ApplicationRecord
belongs_to :school
has_many :students
end
class School < ApplicationRecord
has_many :teachers
has_many :students, through: :teachers
end
And then just do
school = School.find(some_id)
students = school.students
Some notes:
Foreign keys in Rails are usually of the format something_id, so instead of TeacherId you would use teacher_id. Primary keys are usually just called id. Of course you can call them whatever you want, things just require less configuration if you go with the defaults.
Unless this is just to play around with Rails I'd probably change the relation between teacher and students to a many to many. In this case you'd need to distinct the students:
has_many :students, -> { distinct }, through: :teachers
UPDATE:
Many To Many
Yes, you are right. To have a many to many relation you will need a "join table". This is not mandated by Rails though, but the way relational DBs work. Perhaps it's best if you start a new question if you need help with goign that route.
through
:through is an option you can pass to has_many. It is described here
https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association (altough they use a "has and belongs to many" association in the example)
In your example I think of it like: "Hey school, please give me all students that you can reach via the teachers"
And ActiveRecord will create a query similiar to this:
school = School.first
school.students.to_sql
=> "SELECT \"students\".* FROM \"students\" INNER JOIN \"teachers\" ON \"students\".\"teacher_id\" = \"teachers\".\"id\" WHERE \"teachers\".\"school_id\" = 1"
(where 1 is the ID of the school you called the students method on).
If you do the query as suggested by #TTD then it will most likely result in something like this:
school = School.first
Student.where(teacher: Teacher.where(school: s)).to_sql
=> "SELECT \"students\".* FROM \"students\" WHERE \"students\".\"teacher_id\" IN (SELECT \"teachers\".\"id\" FROM \"teachers\" WHERE \"teachers\".\"school_id\" = 1)"
Which is working as well, but uses a nested query to obtain the teacher ids.
There is yet another way that I see used from time to time:
school = School.first
teacher_ids = school.teachers.pluck(:id) # only select the teacher ids
students = Student.where(teacher_id: teacher_ids)
I'd not recommend to do it this way though. It will fire two queries to get the students and transfer back and forth more data:
one to get all the teacher ids
one to get all the students belonging to to those teacher ids
Student.where(teacher: Teacher.where(school: school))

Rails how to access child model attributes in where

Let's say I have two models:
Campaign and Qualification. Campaign has_one :qualification and Qualification belongs_to :campaign.
Let's say that Qualification has a male_gender boolean attribute, what's the most effective way to find all campaigns that have a qualification with male_gender == true
One way would be to loop through qualification with male_gender indexed. Create an array of campaign_ids, and then get campaigns with those ids, but this seems not very effective.
Ideally I'd like to do something like Campaign.where("qualification.male_gender: true")
Is that possible? Is there a better way to do this?
You certainly can do this. Using a join you can reference the other table in your where query:
Campaign.joins(:qualification).where(qualifications: {male_gender: true})
Rich Peck edit
You could also put this into a scope:
#app/models/campaign.rb
class Campaign < ActiveRecord::Base
scope :male_gender, -> { joins(:qualification).where(qualifications: {male_gender: true}) }
end
... which would allow you to call #campaigns = Campaign.male_gender

ActiveRecord: searchable concern

Problem
Suppose I have two models, both of which have a name field:
class Student < ActiveRecord::Base
end
class Teacher < ActiveRecord::Base
end
Now I want to find all the students and teachers who has the name 'Joe'. What I think to do:
name = 'Joe'
Student.where(name: name) + Teacher.where(name: name)
But things get ugly when I add other type of models which has also a name field and I want to search them:
name = 'Joe'
Student.where(name: name) + Teacher.where(name: name) + Manager.where(name: name) + ...
Question
Is there a better way to do this? Ideally what I would like to do, define a module Person and then just:
Person.where(name: name)
Note: Changing the schema is not possible for other reasons.
The solution may be to define your data models in a way that will lend themselves to this type of query. If you truly need to work on all "people" by name perhaps you should have a Person data model with roles or an attribute to define what "type" of person they are.
You're on the right track when you talk about defining a Person module. Look into database normalization to get an idea of how best to define your models.
Since teachers and students are both people, but have different roles, you should store both of them in the same model, but add a field which allows you to select just the teachers or just the students. Something like this (in pseudo-code):
Person
:name (string)
:role (string)
With this, you can write queries like "find all people named John who have the role of teacher", or "find all people named Sue with any role".
With this kind of model, you'd need to enforce values on that role field so that people couldn't type anything they wanted - otherwise, you'd end up with some records that say 'student' and others that say 'classmate', for example. (You could take that a step further and store roles within a separate table, but that's another discussion!)
You have to use inheritance with rails, you do it like this:
class Person < ActiveRecord::Base
# shared person methods
end
class Student < Person
end
class Teacher < Person
end
You have to also have a table named persons with a column named type(string).
After this your students and teachers are going to be in persons table (you can delete original tables).
And you can query all students and persons with Person.where(name: name) and will include all students and teachers. And you can do Student.all to get only students, like a regular model.

Newbie: access attributes of model object

I have two model classes: Cars and Customers,
Model Cars:
class car < ActiveRecord::Base
#car has attribute :town_code
has_many :customers
end
Model Customers:
class customer < ActiveRecord::Base
# customer has attribute :first_name, :last_name
belongs_to :car
end
In my controller, I have the following code:
my_customer = Customer.find_all_by_first_name('John')
p my_customer.last_name
p my_customer.car_id
But I got no attribute 'car_id' error, I also got no attribute 'last_name' error.
---Question 1:---
I checked my database, I do have 'car_id' and 'last_name' columns on my customer table. Why I can not access them in the way how my controller code does?
---Question 2:---
but the code : my_customer.map(&:car_id) is working for accessing car_id, however, I do not quite understand the code .map(&:car_id), what does it do? Can anyone explains to me?
The reason you aren't able to do my_customer.last_name is that my_customer is not a Customer here but an array of Customers, since you did find_all. That's also why my_customer.map(&:car_id) works. What that bit of code means is: For each object in the array my_customer, call the method car_id and insert the results into a new array -- and return that new array.
If customer belongs to car, you need a car_id in the customer table (which corresponds to an id column in the car table). Also, you shouldn't have last_name in the car table, but rather in the customer table.
It sounds like you may need to step back and gain a better understanding of ActiveRecord associations. It's not clear to me why a customer would belong_to a car, anyway.

Ruby on Rails Associations

I am starting to create my sites in Ruby on Rails these days instead of PHP.
I have picked up the language easily but still not 100% confident with associations :)
I have this situation:
User Model
has_and_belongs_to_many :roles
Roles Model
has_and_belongs_to_many :users
Journal Model
has_and_belongs_to_many :roles
So I have a roles_users table and a journals_roles table
I can access the user roles like so:
user = User.find(1)
User.roles
This gives me the roles assigned to the user, I can then access the journal model like so:
journals = user.roles.first.journals
This gets me the journals associated with the user based on the roles. I want to be able to access the journals like so user.journals
In my user model I have tried this:
def journals
self.roles.collect { |role| role.journals }.flatten
end
This gets me the journals in a flatten array but unfortunately I am unable to access anything associated with journals in this case, e.g in the journals model it has:
has_many :items
When I try to access user.journals.items it does not work as it is a flatten array which I am trying to access the has_many association.
Is it possible to get the user.journals another way other than the way I have shown above with the collect method?
Hope you guys understand what I mean, if not let me know and ill try to explain it better.
Cheers
Eef
If you want to have user.journals you should write query by hand. As far as I know Rails does has_many :through associations (habtm is a kind of has_many :through) one level deep. You can use has_many with finder_sql.
user.journals.items in your example doesn't work, becouse journals is an array and it doesn't have items method associated. So, you need to select one journal and then call items:
user.journals.first.items
I would also modify your journals method:
def journals
self.roles(:include => :journals).collect { |role| role.journals }.flatten.uniq
end
uniq removes duplicates and :inlcude => :journals should improve sql queries.
Similar question https://stackoverflow.com/questions/2802539/ruby-on-rails-join-table-associations
You can use Journal.scoped to create scope with conditions you need. As you have many-to-many association for journals-roles, you need to access joining table either with separate query or with inner select:
def journals
Journal.scoped(:conditions => ["journals.id in (Select journal_id from journals_roles where role_id in (?))", role_ids])
end
Then you can use user.journals.all(:include => :items) etc

Resources