Self joins in rails 5.0 using postgresql - ruby-on-rails

I am trying to create self join in rails.
class ItemGroup < ApplicationRecord
has_many :children, class_name: "ItemGroup", foreign_key: "parent_id"
belongs_to :parent, class_name: "ItemGroup"
end
This is my code but when ever i try to add new record it doesn't saves it. So what is the problem in this code can any one help me or give me idea about self join.
class CreateItemGroups < ActiveRecord::Migration[5.0]
def change
create_table :item_groups do |t|
t.string :name
t.integer :parent_id
t.text :description
t.references :parent, index: true
t.timestamps
end
end
end
And this is my migration file.

Rails 5 makes belongs_to association required by default. To disable it you need to add optional: true to belongs_to
belongs_to :parent, class_name: "ItemGroup", optional: true

Related

How to write associations for a model with two foreign keys for the same relationship?

I've been coming back to a problem I haven't solved on my own. I've got a shipments table with two columns for addresses, namely to_address_id and from_address_id.
After looking at a related question (Same Model for Two belongs_to Associations migration), I am still confused. Here is my work so far. I haven't run the migration. I'm trying to resolve this in my mind before getting worked up about it and waste an hour or two. I really thank you for your help.
class AddAddressFromAndAddressToToAddress < ActiveRecord::Migration[6.0]
def change
add_reference :address, :address_from, null: false
add_reference :address, :address_to, null: false
add_foreign_key :address, :shipments, column: :to_address_id
add_foreign_key :address, :shipments, column: :from_address_id
end
en
If i understand correctly you want add origin and destiny address to shipping, correctly?
In this case:
If you want create a migration to create a relationship
class AddOriginAndDestinyAddressToShipment < ActiveRecord::Migration[6.0]
def change
add_reference :shipments, :from_address, foreign_key: { to_table: :addresses }
add_reference :shipments, :to_address, foreign_key: { to_table: :addresses }
end
end
If you want create a model with a relationship
class CreateShipments < ActiveRecord::Migration[6.0]
def change
create_table :shipments do |t|
t.datetime :deadline
t.references :from_address, null: false, foreign_key: { to_table: :addresses }
t.references :to_address, null: false, foreign_key: { to_table: :addresses }
t.timestamps
end
end
end
And model Shipment must be something like this
class Shipment < ApplicationRecord
belongs_to :to_address, class_name: 'Address'
belongs_to :from_address, class_name: 'Address'
end
I did not need the migration, since the foreign keys were already in place. The one I tried didn't work, and the strong_migrations gem was complaining. I only had to wire it up right.
I wanted to show the way I've saved some confusion, so I can type shipment.to_address or shipment.address_to and get the same result. Thank you.
class Shipment < ApplicationRecord
belongs_to :address_from, class_name: 'Address', foreign_key: :from_address_id
belongs_to :from_address, class_name: 'Address', foreign_key: :from_address_id
belongs_to :address_to, class_name: 'Address', foreign_key: :to_address_id
belongs_to :to_address, class_name: 'Address', foreign_key: :to_address_id
end
class Address < ApplicationRecord
has_many :shipments
end

Rails ActiveRecord two fields each referencing the same table

I have a Player class that I want to have high_school_team and club_team properties. So then I figure Player will have high_school_team_id and club_team_id properties that point to the corresponding team. I try to do this in the following migration, but it doesn't work.
class CreatePlayers < ActiveRecord::Migration[6.0]
def change
create_table :players do |t|
t.string :first_name
t.string :middle_name
t.string :last_name
t.decimal :height
t.decimal :weight
t.date :birthday
t.references :team, :high_school_team, foreign_key: true
t.references :team, :club_team, foreign_key: true
t.decimal :gpa
t.string :class_year
t.string :intended_major
t.string :email
t.string :phone_number
t.text :notes
t.timestamps
end
end
end
It gives the following error:
code/scout-db [master●] » rails db:migrate
== 20191218003854 CreatePlayers: migrating ====================================
-- create_table(:players)
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:
you can't define an already defined column 'team_id'.
/Library/Ruby/Gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/connection_adapters/abstract/schema_definitions.rb:372:in `column'
...
HighSchoolTeam and ClubTeam are models that do single table inheritance with Team.
I don't see why I'm getting the error. The docs seem to say that the first argument for t.referenes is table_name and the second ref_name. :team is the name of the table and I want the references to be high_school_team_id and club_team_id.
When I switch the order of the arguments to t.references, it still doesn't work. Somehow it gives the same error: you can't define an already defined column 'team_id'..
It looks you you are confusing SchemaStatement#add_reference and TableDefinition#references which have completely different signatures.
If you want to setup a foreign key column where the table can't be derived from the name of the column (the first argument) you just pass foreign_key: { to_table: :teams}.
class CreatePlayers < ActiveRecord::Migration[6.0]
def change
create_table :players do |t|
t.references :high_school_team, foreign_key: { to_table: :teams}
t.references :club_team, foreign_key: { to_table: :teams}
end
end
end
t.references :high_school_team, index: true as recommended by the other answers is NOT equivilent. That just adds a index to the column but no foreign key constraint.
You can then setup the assocations on Player as:
class Player < ApplicationRecord
belongs_to :high_school_team, class_name: 'Team'
belongs_to :club_team, class_name: 'Team'
end
You can't use a single has_many :players assocation on the other end though as the foreign key column can be either players.high_school_team_id or players.club_team_id.
class Team
has_many :high_school_team_players,
foreign_key: :high_school_team_id,
class_name: 'Player'
has_many :club_team_players,
foreign_key: :club_team_id,
class_name: 'Player'
end
But really a better alternative in the first place would have been to setup a join table:
class Player
has_many :placements
has_one :high_school_team,
through: :placements,
source: :team
class_name: 'Team'
has_one :club_team,
through: :placements,
source: :team
class_name: 'Team'
end
class Placement
belongs_to :player
belongs_to :team
end
class Team
has_many :placements
has_many :players, through: :placements
end
The doc you mentioned talks about the case when you need to add reference to an existing table.
For adding a refernence to a new table:
t.references :team, :high_school_team, foreign_key: true
This piece of code is wrong. Instead, it should be
t.references :high_school_team, foreign_key: {to_table: :teams}
to_table is needed to add database referential integrity
So your migration will be like this:
class CreatePlayers < ActiveRecord::Migration[6.0]
def change
create_table :players do |t|
....
t.references :high_school_team, foreign_key: {to_table: :teams}
t.references :club_team, foreign_key: {to_table: :teams}
....
end
end
end

Does anybody know another way to create join table migration and specify a specific name of references columns in join table?

I have these two models:
module Studying
class Student < ApplicationRecord
has_and_belongs_to_many :instructors,
class_name: 'Studying::Instructor',
foreign_key: 'studying_student_id',
association_foreign_key: 'studying_instructor_id'
end
end
module Studying
class Instructor < ApplicationRecord
has_and_belongs_to_many :students,
class_name: 'Studying::Student',
foreign_key: 'studying_instructor_id',
association_foreign_key: 'studying_student_id'
end
end
And for join_table I have generated migration:
def change
create_table :studying_instructors_students, id: false do |t|
t.belongs_to :studying_instructor, index: { name: 'index_instructors_students_on_studying_instructor_id' }
t.belongs_to :studying_student, index: { name: 'index_instructors_students_on_studying_student_id' }
end
end
So all is working fine, but the point is, that my senior comrade tells me that I should not use in models things such as:
foreign_key: 'studying_instructor_id',
and
association_foreign_key: 'studying_student_id'
but instead of these I should use:
foreign_key: 'instructor_id',
and
association_foreign_key: 'student_id'
and in the same way in the first model because this is against the convention.
I do not know how I can do this in this models and in tables (how you already understand tables names in db: studying_instructors and studying_students).
Any advice please ?
You can create relation table this way
def change
create_table :studying_instructors_students, id: false do |t|
t.integer :instructor_id, index: true
t.integer :student_id, index: true
end
end
and then you can use foreign_key: 'instructor_id', and association_foreign_key: 'student_id'
If I get this wrong way, please, feel free to address additional questions.
I would do something like this:
class Studying::Student < ApplicationRecord
has_many :student_instructors, class_name: 'Studying::StudentInstructor'
has_many :instructors, through: :student_instructors, class_name: 'Studying::Instructor'
end
class Studying::Instructor < ApplicationRecord
has_many :student_instructors, class_name: 'Studying::StudentInstructor'
has_many :students, through: :student_instructors, class_name: 'Studying::Instructor'
end
class Studying::StudentInstructor < ApplicationRecord
belongs_to :student, class_name: 'Studying::Student'
belongs_to :instructor, class_name: 'Studying::Instructor'
end
I have three tables here. One for students, second for instructors and third one is a junction table (student_instructors).
Migration for studnt_instructor will look something like this
def change
create_table :student_instuctors, id: false do |t|
t.integer :instructor_id, index: true
t.integer :student_id, index: true
end
end
I have not verified the syntax but this is more of logic explanation.
I hope this helps.

Rails - No proper access to self join model

I have model called Section, and I want section to be able to have many Sections - so a Self Joins seems like a place to start.
I setup my code like this:
Model file
class Section < ApplicationRecord
belongs_to :offer
has_many :offer_items
# Self joins:
has_many :child_sections, class_name: "Section", foreign_key: "parent_id"
belongs_to :parent_section, class_name: "Section", optional: true
end
Migration file
class CreateSections < ActiveRecord::Migration[5.0]
def change
create_table :sections do |t|
t.string :name
t.references :offer, foreign_key: true
t.references :parent_section, foreign_key: true
t.timestamps
end
end
end
As you can see I set belongs_to :parent_section as optional, as no every section should has its parent.
When I print attribute_names of my Section model it says:
=> ["id", "name", "offer_id", "parent_section_id", "created_at", "updated_at"]
Trying to retrieve child_sections gets me an error:
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: sections.parent_id: SELECT "sections".* FROM "sections" WHERE "sections"."parent_id" = ?
Where did I make my mistake?
You do not have parent_id in your sections
change this:
has_many :child_sections, class_name: 'Section', foreign_key: 'parent_id'
on this:
has_many :child_sections, class_name: 'Section', foreign_key: 'parent_section_id'

Rails Joining multiple models on a single table

New to rails (using 4.1), making a small project management tool as a learning project. I have run into a slightly more complex model association, and I want to make sure I am on the right track here, and ask how to extend it a bit.
Consider this situation:
Models:
class Website < ActiveRecord::Base
has_many :website_user
has_many :users, through: :website_user
has_many :tasks, through: :website_user
end
class User < ActiveRecord::Base
has_many :websites, through: :website_user
has_many :website_user
end
class WebsiteUser < ActiveRecord::Base
belongs_to :website
belongs_to :user
belongs_to :role
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :website_user
has_one :website, through: :website_user
end
class Role < ActiveRecord::Base
has_many :website_user
end
DB:
create_table "roles", force: true do |t|
t.string "name"
end
create_table "tasks", force: true do |t|
t.text "description"
t.string "title"
t.integer "website_user_id"
end
create_table "users", force: true do |t|
t.string "name"
t.string "email"
t.string "password"
t.string "password_hash"
t.string "password_salt"
end
create_table "website_users", force: true do |t|
t.integer "website_id"
t.integer "user_id"
t.integer "role_id"
end
create_table "websites", force: true do |t|
t.string "name"
t.string "url"
end
What I have going on here is basically Websites get users (team members working on sites) associated though the website_user table. That table belongs to roles, so that a team member would have a specific job on this website, and finally, tasks belong to the website_user association, so that you could swap out a user, but the task would stay associated with the role and website.
I am looking into extending it more, so that the task would be associated on website_user twice, once for the assigner, once for the assigned user of the task. However, at this point, it feels like I will have an awful lot of things attached to a big join table in the middle, and without a ton of experience under my belt, it is starting to smell like there might be a better way.
If this all looks good, how would you join the tasks to the website_user twice, once for assigner, once for assigned? Or alternatively, how would rearrange the model association?
A simple solution that first comes to head is to keep assigner and assignee ids in Task model.
Migration AddAssigneeAssignerToTask
class AddAssigneeAssignerToTask < ActiveRecord::Migration
change do
add_reference :task, :assignee, index: true
add_reference :task, :assigner, index: true
end
end
Adding belongs_to into Task model
class Task < ActiveRecord::Base
belongs_to :assignee, class: 'WebsiteUser'
belongs_to :assigner, class: 'WebsiteUser'
has_one :website, through: :assignee
end
Modifying WebsiteUser
class WebsiteUser < ActiveRecord::Base
belongs_to :website
belongs_to :user
belongs_to :role
has_many :assigned_tasks, class_name: 'Task', foreign_key: 'assigner_id'
has_many :received_tasks, class_name: 'Task', foreign_key: 'assignee_id'
end
So afterwards you can use it like this
#website_user.assigned_tasks # => []
#website_user.received_tasks # => [Task1, Task2]
BUT
If you think to add some different functionality to either assigner or assignee, you should consider to use STI or MTI
class Task < ActiveRecord::Base
belongs_to :assignee, class_name: WebsiteUser, foreign_key:website_user_id
belongs_to :assigner, class_name: WebsiteUser, foreign_key:assigner_user_id
end
class WebsiteUser < ActiveRecord::Base
has_many :assigned_tasks, class_name: Task, inverse_of: :assignee, dependent: :destroy, foreign_key: :website_user_id
has_many :tasks_assigned, class_name: Task, inverse_of: assigner, dependent: :destroy, foreign_key: :assigned_user_id
end
You will have to add another foreign key in your tasks table..
just a starting point but this should get you going..
I can not advice you in the database design, but you can assign users twice using an option called class_name. You can read more here: http://guides.rubyonrails.org/association_basics.html#belongs-to-association-reference
But you will have to add additional foreign_key to your Tasks model as well.
And I also advice you to read following chapter of M. Hartle book, as it have really good explanation between relationships of models: https://www.railstutorial.org/book/following_users#cha-following_users

Resources