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))
Related
How can I access my related records?
class Post < ActiveRecord::Base
has_many :post_categories
has_many :categories, through: :post_categories
class Categories < ActiveRecord::Base
has_many :post_categories
has_many :post, through: :post_categories
class PostCategories < ActiveRecord::Base
belongs_to :post
belongs_to :category
PostCategories table has id, posts_id, and categories_id columns.
id | posts_id | categories_id
1. | 2 | 3
2. | 2 | 4
What I want is: to get posts related to a category. like: all Posts where in x category.
Yep, this is an easy one.
one_or_more_categories = # Category.find... or Category.where...
posts = Post.joins(:categories).where(category: one_or_more_categories)
Rails is clever enough to take either a model or a query that would find some data and turn that into an efficient appropriate query, that might be a subquery. Trying things out in the Rails console (bundle exec rails c) is a good way to see the generated SQL and better understand what's going on.
(EDIT: As another answer points out, if you've already retrieved a specific Category instance then you can just reference category.posts and work with that relationship directly, including chaining in .order, .limit and so-on).
Another way to write it 'lower level' would be:
Post.joins(:categories).where(category: {id: one_or_more_category_ids})
...which is in essence what Rails will be doing under the hood when given an ActiveRecord model instance or an ActiveRecord::Relation. If you already knew the e.g. category "name", or some other indexed text column that you could search on, then you'd adjust the above accordingly:
Post.joins(:categories).where(category: {name: name_of_category})
The pattern of joins and where taking a Hash where the join table name is used as a key with values nested under there can be taken as deep as you like (e.g. if categories had-many subcategories) and you can find more about that in Rails Guides or appropriate web searches. The only gotcha is the tortuous singular/plural stuff, which Rails uses to try and make things more "English-y" but sometimes - as in this case - just creates an additional cognitive burden of needing to remember which parts should be singular and which plural.
Not sure if this answers it but in ActiveRecord your Post will have direct access to your Category model and vice versa. So you could identify the category you want the posts from in a variable or an instance variable, and query #specific_category.posts. If you are doing this in your controller, you could even do it in before_action filter. If you are using it in serializers its not much different.
You could also create a scope in your Post model and use either active record or raw SQL to query specific parameters.
You also have an error in your Category model. Has many is always plural so it would be has_many :posts, through: :post_categories
Get the category object and you can directly fetch the related posts. Please see the following
category = Category.find(id)
posts = category.posts
Since you have already configured the has_many_through relation, rails will fetch post records related the category.
So I have been trying to create a dummy application to try and learn Rails. The app I thought I could create is a coffee ordering app for a group of people in work.
So the website will have many users.
A user can create a coffee_order.
A coffee order contains orders for other individual users.
Each user can have one or more coffee_shop_items (e.g. latte,
cappuccino,danish, muffin, etc)
A coffee order also has an assignee, this is the person who is tasked
with going and getting the order.
So as a user, I create a coffee order, select an assignee, add users to the order, and add one or more coffee shop items to each user,
I am really struggling with how the database should be, and what the associations need to be, along with any join tables?
I am also trying to use nested attributes for the form entry.
Thanks in advance for help.
Update with some code I have tried to create a coffee order:
#coffee_order = CoffeeOrder.new(coffee_order_params)
params[:coffee_order][:user_coffee_orders_attributes].each do |user_order|
order = #coffee_order.user_coffee_orders.new(user_id: user_order[1][:user_id].to_i)
user_order[1][:coffee_shop_items].each do |item|
coffee_shop_item = CoffeeShopItems.find(item) if item != ""
# this line fails! see error below
#coffee_order.user_coffee_orders.coffee_shop_items << coffee_shop_item if coffee_shop_item != nil
end
end
error:
NoMethodError (undefined method `coffee_shop_items' for #<UserCoffeeOrder::ActiveRecord_Associations_CollectionProxy:0x42c6180>):
The coffee_shop_items belong to the order, not the user. After all, a user could probably create another order another day? You should probably also check out the rails documentation, which, IIRC actually contains a walk-through of a shopping cart application.
User has_many :coffes_orders
User has_many :coffee_orders_he_needs_to_get, class_name: "CoffeeOrder", foreign_key: "assignee_id"
CoffeeOrder belongs_to :user
CoffeeOrder belongs_to :assignee, class_name: "User"
CoffeeOrder has_and_belongs_to_many :coffee_shop_items
Coffee_shop_items has_and_belongs_to_many :coffee_orders
So many tutorials on how to set up a has_many :through but not enough on how to actually do it!
I have a Inventories and Requests table joined by Bookings. Example: there could be 3 lenders who have tents in inventory, each of which is requested by 3 other borrowers. What I want to do is for each of the 3 tents in inventory, show that lender the list of 3 borrowers who requested the tent. Then the lender can pick who s/he wants to be the ultimate borrower.
I have thoughts on how this should work, but no idea if it's right, so please give advice on the below! The action is driven all by the Requests controller. Let's run through an example where the Inventories table already has 3 tents, ids [1, 2, 3]. Let's say Borrower Pat submits a Request_ID 1 for a tent.
Am I then supposed to create 3 new Bookings all with Request_ID 1 and then Inventory_ID [1, 2, 3] to get all the conceivable combinations? Something like
Inventory.where(name: "tent").each { |inventory| #request.bookings.create(inventory_id: inventory.id) }
And then is it right to use the Bookings primary key as the foreign key in both the Request and Inventory? Which means that after Borrower Pat submits his request, the bookings_id will be blank until say Lender 2 accepts, at which point bookings_id equals the id that matches the combination of Request_ID 1 and Inventory_ID 2
Now let's say when a Request is posted and a Bookings is made, I email the lender. However, I realized I don't want to bother Lender Taylor if 3 borrowers want her tent over the same time period. I'll just email her the first time, and then the subsequent ones she'll find out about when she logs in to say yes or no. In this situation is it OK to just query the Bookings table in the create action, something like (expanding off above)
-
Inventory.where(name: "tent").each do |inventory|
if !Bookings.find_by_inventory_id(inventory.id).exists?
# If there are no other bookings for this inventory, then create the booking and send an email
#request.bookings.create(inventory_id: inventory.id)
AlertMail.mail_to_lender(inventory).deliver
else
# If there are other bookings for this inventory, do any of those bookings have a request ID where the requested time overlaps with this new request's requested time? If so then just create a booking, don't bother with another email
if Bookings.where(inventory_id: inventory.id).select { |bookings_id| Bookings.find_by_id(bookings_id).request.time overlaps_with current_request.time }.count > 0
#request.bookings.create(inventory_id: inventory.id)
# If there are other bookings for this inventory but without overlapping times, go ahead and send an new email
else
#request.bookings.create(inventory_id: inventory.id)
AlertMail.mail_to_lender(inventory).deliver
end
end
end
Code above is probably flawed, I just want to know the theory of how this is supposed to be working.
Join Table
Firstly, has_many :through works by using a join table - a central table used to identify two different foreign_keys for your other tables. This is what provides the through functionality:
Some trivia for you:
has_and_belongs_to_many tables are called [plural_model_1]_[plural_model_2] and the models need to be in alphabetical order (entries_users)
has_many :through join tables can be called anything, but are typically called [alphabetical_model_1_singular]_[alphabetical_model_2_plural]
--
Models
The has_many :through models are generally constructed as such:
#app/models/inventory.rb
Class Inventory < ActiveRecord::Base
has_many :bookings
has_many :requests, through: :bookings
end
#app/models/booking.rb
Class Booking < ActiveRecord::Base
belongs_to :inventory
belongs_to :request
end
#app/models/request.rb
Class Request < ActiveRecord::Base
has_many :bookings
has_many :requests, through: :bookings
end
--
Code
Your code is really quite bloated - you'll be much better doing something like this:
#app/controllers/inventories_controller.rb
Class InventoriesController < ApplicationController
def action
#tents = Inventory.where name: "tent"
#tents.each do |tent|
booking = Booking.find_or_create_by inventory_id: tend.id
AlertMail.mail_to_lender(tent).deliver if booking.is_past_due?
end
end
end
#app/models/booking.rb
Class Booking < ActiveRecord::Base
def is_past_due?
...logic here for instance method
end
end
Used find_or_create_by
You should only be referencing things once - it's called DRY (don't repeat yourself)
I did a poor job of asking this question. What I wanted to know was how to create the actual associations once everything is set up in the DB and Model files.
If you want to create a record of B that is in a many-to-many relationship with an existing record of A, it's the same syntax of A.Bs.create. What was more important for me, was how to link an A and B that already existed, in which case the answer was A.B_ids += B_id.
Two other things:
More obvious: if you created/ linked something one way, was the other way automatic? And yes, of course. In a many-to-many relationship, if you've done say A.B_ids += B_id, you no longer have to do 'B.A_ids += A_id`.
Less obvious: if A and B are joined by table AB, the primary key of table AB doesn't need to be added to A or B. Rails wants you to worry about the AB table as less as possible, so searches, builds, etc. can all be done by A.B or B.A instead of A.AB.B or B.AB.A
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]})
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