I have two models in my rails 4.2.4 project that I'd like to join together and I'm struggling with creating the correct relationship between the two.
The domain in question is rugby and the code is all available at https://github.com/rgbycch/rgbycch-rest/tree/rails. The models are:
Player (https://github.com/rgbycch/rgbycch-rest/blob/rails/app/models/player.rb)
PlayerPosition (https://github.com/rgbycch/rgbycch-rest/blob/rails/app/models/player_position.rb)
I'd like to be able to create a relationship whereby a Player can have multiple favoured PlayingPositions. I believe my db structure would need to look something like this:
create_table :favoured_positions do |t|
t.references :player, index: true, foreign_key: true # fk to player table
t.references :player_position, index: true, foreign_key: true # fk to player_position table
t.integer :num_favs # additional data for a favored playing position
t.timestamps null: false
end
I can generate a new FavoredPosition model like this I guess:
bundle exec bin/rails generate model FavouredPosition player:references player_position:references num_favs:integer
which generates a class that looks like:
class FavouredPosition < ActiveRecord::Base
belongs_to :player
belongs_to :player_position
end
I'm not sure how to alter my Player and PlayerPosition models to reflect this new relationship. Is the following correct:
class Player < ActiveRecord::Base
has_many :favored_positions, :through => :favoured_positions
end
class PlayerPositions < ActiveRecord::Base
has_many :favored_playing_positions, :through => :favoured_positions
end
Would you advise me to add indexes to either of these tables too?
Thanks,
Sean
You would need to set up the associations like this:
class Player < ActiveRecord::Base
has_many :favoured_positions
has_many :player_positions, through: :favoured_positions
end
class PlayerPosition < ActiveRecord::Base
has_many :favoured_positions
has_many :players, through: :favoured_positions
end
See: http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
Firstly, change PlayerPosition to Position, it's far simpler and works with the DRY (Don't Repeat Yourself) principle.
Secondly, what you're referring to is an ActiveRecord association. These are basically how the ActiveRecord ORM allows you to associate objects in Rails.
--
has_many :through
The association I feel you'd best using is either going to be has_many :through or has_and_belongs_to_many. Since you've comitted to has_many :through, here's what you need to do:
#app/models/player.rb
class Player < ActiveRecord::Base
#columns id | etc | etc | created_at | updated_at
has_many :favoured_positions
has_many :positions, through: :favoured_positions
end
#app/models/position.rb
class Position < ActiveRecord::Base
#columns id | etc | etc | created_at | updated_at
has_many :favoured_positions
has_many :players, through: :favoured_positions
end
#app/models/favoured_position.rb
class FavouredPosition < ActiveRecord::Base
#columns id | position_id | player_id | any | other | column | created_at | updated_at
belongs_to :position
belongs_to :player
end
--
has_and_belongs_to_many
An important caveat about the difference between has_many :through and has_and_belongs_to_many. If you didn't have any extra columns in your favoured_positions join model, I would recommend you use has_and_belongs_to_many. It's far simpler for what you need:
#app/models/player.rb
class Player < ActiveRecord::Base
has_and_belongs_to_many :positions
end
#app/models/position.rb
class Position < ActiveRecord::Base
has_and_belongs_to_many :players
end
You'd then make a join table as follows:
#players_positions
player_id | position_id
Related
I have these associations in my Rails 4.2 app. I don't understand how to setup the proper references/foreign_keys between instructors and courses.
So far both instructors and courses tables have a local_id (local reference).
Local is a training center they both belong to.
class Local < ActiveRecord::Base
has_many :instructors
has_many :courses
end
class Instructor < ActiveRecord::Base
belongs_to :local
has_many :courses, through: :locals
end
class Course < ActiveRecord::Base
belongs_to :local
has_many :instructors, through: :locals
end
Do I add a foreign_key to the courses table? Like this:
add_foreign_key :courses, :instructors
I read something about when having many to many associations we need a "join table" cause we need to store many ids. I guess Local is just that in this case.
Or do I need another table(Model) that belongs_to :instructor, :course?
Here is how I would set it up for maximum flexibility.
Instructors and Courses
Lets set up a many to many relationship with a join model which we call Employment.
We can generate the model with:
rails g model employment instructor:belongs_to course:belongs_to
Which will give us this:
class Employment < ActiveRecord::Base
belongs_to :instructor
belongs_to :course
end
employments will have the instructor_id and course_id foreign keys. This lets us assign any number of instructors to a course and vice versa.
So let's put the join model to use:
class Instructor < ActiveRecord::Base
has_many :employments
has_many :courses, through: :employments
end
class Course < ActiveRecord::Base
has_many :employments
has_many :instructors, through: :employments
end
Venues and Courses
I would recommend you rename your Local model Venue as it's the common english term for the place where a course or event is held. Locale is awkward since it collides with a separate concept in web applications and may cause name clashes.
And we should probably set it up as many to many to account for the complexities of real life.
So again:
rails g model booking venue:belongs_to course:belongs_to
class Booking < ActiveRecord::Base
belongs_to :venue
belongs_to :course
end
bookings will have the venue_id and course_id foreign keys.
class Venue < ActiveRecord::Base # was Local
has_many :bookings
has_many :courses, through: :bookings
end
class Course < ActiveRecord::Base
# ...
has_many :bookings
has_many :venues, through: :bookings
end
More reading:
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
http://blog.flatironschool.com/why-you-dont-need-has-and-belongs-to-many/
I tried to set up a basic association between users and projects:
user.rb
class User < ActiveRecord::Base
has_many :projects, foreign_key: 'owner_id'
has_many :project_members, through: :project_members
end
project.rb
class Project < ActiveRecord::Base
has_many :project_members, dependent: :destroy
has_many :users, through: :project_members
end
project_member.rb
class ProjectMember < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
my project_members table:
+----+------------+---------+
| id | project_id | user_id |
+----+------------+---------+
| 1 | 1 | 1 |
| 2 | 2 | 1 |
+----+------------+---------+
and my project table:
+----+-------+----------+
| id | name | owner_id |
+----+-------+----------+
| 1 | test1 | 1 |
| 2 | test2 | 2 |
+----+-------+----------+
why I get with
current_user.projects
only the projects where the projects.owner_id = current_user.id and not the projects where the user is member?
But I think it's the wrong side I tried to get the data I want.
I'm on /projects where the projects controller gets the data.
I think I should use something like that:
class ProjectsController < ApplicationController
# GET /projects
def index
#projects = Project.all
end
end
but how I can get only the projects where current_user.id is member of?
I think you should set that up a tiny bit different:
class User < ActiveRecord::Base
has_many :owned_projects, class_name: "Project", foreign_key: 'owner_id', dependent: :restrict_with_error
has_many :project_members, dependent: :destroy
has_many :projects, through: :project_members
end
class Project < ActiveRecord::Base
has_many :project_members, dependent: :destroy
has_many :users, through: :project_members
belongs_to :owner, class_name: "User"
end
In order to get all the projects the current_user is a member of you can do:
current_user.projects
In order to get all the projects the current_user owns you can do:
current_user.owned_projects
As an addition to Kasper Johansen's answer, you may also wish to look into has_and_belongs_to_many:
The main reason you'd use this is because you're not using any extra attributes in your join model (which is your current setup).
If you want to keep it that way, you can do away with your join model by using the has_and_belongs_to_many association in place of has_many :through:
#app/models/user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :projects
end
#join table - projects_users
#app/models/project.rb
class Project < ActiveRecord::Base
has_and_belongs_to_many :users
end
From the docs:
The simplest rule of thumb is that you should set up a has_many
:through relationship if you need to work with the relationship model
as an independent entity. If you don't need to do anything with the
relationship model, it may be simpler to set up a
has_and_belongs_to_many relationship (though you'll need to remember
to create the joining table in the database).
In regards your original problem, you have to also remember that in order to get the data back from an associative model, you have to set up the associations that will be populated with that data.
You mention...
why I get with current_user.projects only the projects where the projects.owner_id = current_user.id
... because you've set the association of has_many :projects, foreign_key: 'owner_id'
Rails is not magic - it has to take what associations you give it and populate the data in those methods accordingly:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :owned_projects, class_name: "Project", foreign_key: :owner_id
has_and_belongs_to_many :projects
end
#app/models/project.rb
class Project < ActiveRecord::Base
has_and_belongs_to_many :users
end
Hopefully gives you some more context to add to Kasper's answer.
I have 3 tables, students messages and coaches.
now i want to create a join migration table with message_id coach_id and student_id
please help to create the migration query
any help is highly appreciated
getting this error when i tried the below code
== 20150924072052 AssociationTable: migrating
=================================
-- create_table(:associations)
rake aborted!
StandardError: An error has occurred, all later migrations canceled:
Mysql2::Error: Key column 'student_id' doesn't exist in table: ALTER TABLE `associations` ADD CONSTRAINT `fk_rails_122f0db022` FOREIGN KEY (`student_id`) REFERENCES `student` (`id`)`
It will be something like this:
$ rails g migration AssociationTable
... which will create a file such as the following:
#db/migrate/association_table_[timestamp].rb
class AssociationTable < ActiveRecord::Migration
def change
create_table :associations do |t|
t.references :student
t.references :message
t.references :coache
t.timestamps null: false
end
end
end
This will create a table with the following columns:
id
student_id
message_id
coach_id
created_at
updated_at
This will be used in a has_many :through relationship, which requires you to have a join model:
#app/models/association.rb
class Association < ActiveRecord::Base
belongs_to :student
belongs_to :message
belongs_to :coach
end
--
To update you regarding the choice between has_many :through and has_and_belongs_to_many, here is what you need to know:
The main difference between the two is the fact that has_many :through utilizes a join model. Join models are basically a model through which ActiveRecord will populate dependent associative data. In short, it "joins" two models together.
Although the join model is the big difference between HABTM and HMT, there is another technical reason why you'd choose between them - HMT permits you to have extra attributes in the join model.
This is important for something like this:
#app/models/doctor.rb
class Doctor < ActiveRecord::Base
has_many :appointments
has_many :patients, through: :appointments
end
#app/models/appointment.rb
class Appointment < ActiveRecord::Base
#columns id | doctor_id | patient_id | time | created_at | updated_at
belongs_to :doctor
belongs_to :patient
end
#app/models/patient.rb
class Patient < ActiveRecord::Base
has_many :appointments
has_many :doctors, through: :appointments
end
The join model (appointment) will therefore be able to have specific data that you'll be able to use with the other models:
#doctor = Doctor.find 1
#appointments = #doctor.appointments.where(time: ____)
#patients = #appointments.patients
Lots of queries. I hope you get the idea.
--
has_and_belongs_to_many is a lot simpler, although I'm not sure if it works for 3 tables (don't see why it shouldn't).
This removes the need for a join model, and in the process prevents you from being able to use extra attributes in the association (notice how the join table has no id or timestamp attributes?). The naming convention for HABTM tables is albabetical_plurals - in your case recipient_messages
Again, I don't know if this will work for a 3-way join, but here's how you'd do it:
#app/models/student.rb
class Student < ActiveRecord::Base
has_and_belongs_to_many :messages, join_table: "recipients_messages", foreign_key: "recipient_id", association_foreign_key: "message_id"
end
#app/models/message.rb
class Message < ActiveRecord::Base
has_and_belongs_to_many :recipients, join_table: "recipients_messages", foreign_key: "message_id", association_foreign_key: "recipient_id"
end
Thinking about your request specifically, I'd say that you'd be better with has_many :through.
Reason being that if you're sending messages, you need a way to know to whom the message was sent, its content and whether it's been read.
I would use messages as the join model:
#app/models/message.rb
class Message < ActiveRecord::Base
#columns id | student_id | coach_id | message | read | created_at | updated_at
belongs_to :student
belongs_to :coach
end
#app/models/coach.rb
class Coach < ActiveRecord::Base
has_many :messages
end
#app/models/student.rb
class Student < ActiveRecord::Base
has_many :messages
end
Let's say you have two models Venue and Photo. Each Venue can have many Photos, but it can only have one FeaturedPhoto. Each Photo can be belong to many Venues and can be the FeaturedPhoto of more than one Venue too.
I already have the general has_and_belongs_to_many relationship set up and working through a join table with these models:
class Photo < ActiveRecord::Base
mount_uploader :image, PhotoUploader
has_and_belongs_to_many :venues
end
class Venue < ActiveRecord::Base
has_and_belongs_to_many :photos
end
To add the FeaturedPhoto, it seems like I would need to add a column called featured_photo_id to the Venue model and then set up a has_many, belongs_to association between the two. But I'm not sure where to add the foreign_key info. Is this right?
class Photo < ActiveRecord::Base
mount_uploader :image, PhotoUploader
has_and_belongs_to_many :venues
has_many :venues
end
class Venue < ActiveRecord::Base
has_and_belongs_to_many :photos
belongs_to :photo, foreign_key: "featured_photo_id", class_name: "Photo"
end
Or do I have to add foreign_key info to both models?
This can be achieved if you add a model for intermediate table PhotosVenue and add a boolean column is_featured in this intermediate table.
I created a demo app to do exactly what you want. Check this github repo
Hope this helps :)
has_many :through
I concur with SiriusROR - you need to use a has_many :through model, with an extra boolean attribute called featured:
#app/models/photo.rb
Class Photo < ActiveRecord::Base
has_many :photo_venues
has_many :venues, through: :venue_photos
end
#app/models/venue.rb
Class Venue < ActiveRecord::Base
has_many :photo_venues
has_many :photos, through: :venue_photos do
def featured
find_by featured: true
end
end
end
#app/models/photo_venue.rb
Class VenuePhoto < ActiveRecord::Base
belongs_to :photo
belongs_to :venue
end
--
Your schema for the :through model should be set up like this:
#photo_venues
id | photo_id | venue_id | featured | created_at | updated_at
--
This will allow you to call:
#venue.photos.featured # -> should bring back the single ActiveRecord object for the first featured photo in the collection
Right. This simply refuses to work. Been at this for hours.
album model
class Album < ActiveRecord::Base
has_many :features, through: :join_table1
end
features model
class Feature < ActiveRecord::Base
has_many :albums, through: :join_table1
end
join_table1 model
class JoinTable1 < ActiveRecord::Base
belongs_to :features
belongs_to :albums
end
join_table1 schema
album_id | feature_id
albums schema
id | title | release_date | genre | artist_id | created_at | updated_at | price | image_path
features schema
id | feature | created_at | updated_at
Upon raking the test database, and running this integration test:
require 'test_helper'
class DataFlowTest < ActionDispatch::IntegrationTest
test "create new user" do
album = albums(:one)
feature = features(:one)
album.features
end
end
I get
ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association :join_table1 in model Album
Why is this?
You need to add has_many :album_features both to Album and Feature models (given that you rename JoinTable1 model to more meaningful AlbumFeature, and corresponding table would be album_features), as :through references association - your error is exactly about it.
Or you can use has_and_belongs_to_many so there will be no need to define special link model. But in that case you must name your table albums_features.
Just define the models as follow
album model
class Album < ActiveRecord::Base
has_many :join_table1
has_many :features, through: :join_table1
end
features model
class Feature < ActiveRecord::Base
has_many :join_table1
has_many :albums, through: :join_table1
end
join_table1 model
class JoinTable1 < ActiveRecord::Base
belongs_to :features
belongs_to :albums
end
happened to me as well.
made it work by adding the join table as has_many to both models.
like this:
connection model:
module Alerts
class AlertIncidentConnection < ActiveRecord::Base
belongs_to :incident
belongs_to :alert
end
end
alert model:
module Alerts
class Alert < ActiveRecord::Base
has_many :alert_incident_connections, class_name: 'Alerts::AlertIncidentConnection'
has_many :incidents, through: :alert_incident_connections,class_name: 'Alerts::Incident', dependent: :destroy
end
end
incident model:
module Alerts
class Incident < ActiveRecord::Base
has_many :alert_incident_connections, class_name: 'Alerts::AlertIncidentConnection'
has_many :alerts, through: :alert_incident_connections,class_name: 'Alerts::Alert' ,dependent: :destroy
end
end
migration file:
class CreateTableAlertIncidentConnections < ActiveRecord::Migration
def change
create_table :alert_incident_connections do |t|
t.references :alert, null: false, index: true
t.references :incident, null: false, index: true
t.timestamps
end
end
end
usage:
alert.incidents << incident
alert.save!
Similarly like #mad_raz answered, but join table needs to have singulars for belongs_to, like this:
class JoinTable1 < ActiveRecord::Base
belongs_to :feature
belongs_to :album
end
Complete associations tutorial https://kolosek.com/rails-join-table/