HABTM join table with different primary keys - ruby-on-rails

I have a two models 1. category primary key is cust_number , 2. product primary key is id . Now i want to make a HABTM relationship between category and product. Here the problem is how to create join table with cust_number and product_id as a keys?

For this use case you want to use has_many through: and not has_and_belongs_to_many. They both acheive the same goal (a m2m association) but has_and_belongs_to_many is very limited.
AFAIK HABTM only takes the foreign_key option which is used to set the foreign key on the join table, there does not seem to be a way to tell it what PK that FK points to.
Run the generator to create the join model:
rails g model category_product product:references
Open up the migration and change the foreign key so that it points to your custom primary key:
create_table :category_products do |t|
t.references :product, foreign_key: true
t.references :category, foreign_key: { primary_key: "cust_number" }
end
Change the foreign key in the assocation:
class CategoryProduct < ApplicationRecord
belongs_to :category, foreign_key: "cust_number"
belongs_to :product
end
Then add the has_many through: assocations to each end:
class Category < ApplicationRecord
has_many :category_products
has_many :products, through: :category_products
end
class Product < ApplicationRecord
has_many :category_products
has_many :categories, through: :category_products
end

Related

How can I use two records from the same Rails model as foreign keys in a different Rails model?

I have two models: person.rb and relationship.rb
I need my :relationships table to reference two rows from the :people table as foreign keys.
Here are the migrations for both tables:
class CreatePeople < ActiveRecord::Migration[5.1]
def change
create_table :people do |t|
t.string :first_name
t.string :second_name
t.integer :age
t.string :gender
t.timestamps
end
end
end
class CreateRelationships < ActiveRecord::Migration[5.1]
def change
create_table :relationships do |t|
t.references :person_a
t.references :person_b
t.string :status
t.timestamps
end
end
end
The idea is the :person_a and :person_b fields will both be individual records from the :people table referenced as foreign keys, while the :status field will just be a description of their relationship ("Married", "Friends", "Separated", etc.)
I'm trying to find out:
1) What is the additional code I have to write in the CreateRelationships migration above in order to set :person_a and :person_b up as foreign keys from the :people table?
2) What code do I need to write in the model files (person.rb and relationship.rb) for both tables below to define the relationship structure I'm talking about?
class Person < ApplicationRecord
end
class Relationship < ApplicationRecord
end
I've found one other question on here that deals with this issue, but the answers given were conflicting, some incomplete, and others working with older versions of Rails. None of them have worked for me.
I'm using Rails 5.1.4
You have defined you migration correctly just add the following in your model to define the relationship between the model.
class Person < ApplicationRecord::Base
has_many :relationships, dependent: :destroy
end
class Relationship < ApplicationRecord::Base
belongs_to :person_a, :class_name => 'Person'
belongs_to :person_b, :class_name => 'Person'
end
This allows you to access the Person that a Relationship belongs to like this:
#relationship.person_a #user assigned as the first person.
#relationship.person_b #user assigned as the second person.
Hope this works.
EDIT: Apologies for a rushed and wrong answer.
Initially, I thought that simple has_many/belongs_to association is possible and sufficient.
class Person < ApplicationRecord
has_many :relationships #dependent: :destroy
end
class Relationship < ApplicationRecord
belongs_to :person_a, class_name: "Person"
belongs_to :person_b, class_name: "Person"
enum status: [:married, :friends, :separated]
end
As #engineersmnky pointed out, has_many association can't work here because there is no person_id column in relationships table. Since we can declare only one custom foreign key in has_many association, it's not possible to declare it here this way. belongs_to will work, but I don't think that's enough.
One way is to skip declaring has_many and stick to custom method for querying relationships:
class Person < ApplicationRecord
def relationships
Relationship.where("person_a_id = ? OR person_b_id = ?", id, id)
end
end
It will give you an ActiveRecord::Relation to work with, containing exactly the records you need. The drawbacks of this solution are numerous - depending on your needs, you will probably need more code for inserting data, starting with a setter method to assign relationships to people...
What could be a real solution, is to have a composite primary key in Relationship model - composed of :person_a_id and :person_b_id. ActiveRecord doesn't support composite primary keys, but this gem seems to fill the gap. Apparently it allows to declare such key and use it as a foreign key in a has_many association. The catch is that your person_a/person_b pairs would have to be unique across relationships table.
I had to do the same for a chat module, this is an example to how you can do it:
class Conversations < ApplicationRecord
belongs_to :sender, foreign_key: :sender_id, class_name: 'User'
belongs_to :recipient, foreign_key: :recipient_id, class_name: 'User'
end
class User < ApplicationRecord
...
has_many :conversations
has_many :senders, through: :conversations, dependent: :destroy
has_many :recipients, through: :conversations, dependent: :destroy
...
end
More explanations at complex-has-many-through
Hope it helps,
You can do like this
In relationship model write
belongs_to : xyz, class_name: "Person", foreign_key: "person_a"
belongs_to : abc, class_name: "Person", foreign_key: "person_b"
In Person model write
has_many :relationships, dependent: :destroy
hope it will help

Ruby on Rails Use custom id for join table

I'm trying to get a join table working with a custom id in the schema. I want to be able to get all the relevant posts for a user through the join table using the custom_id on the join table as recipient_id
Here is my current setup:
class User > ActiveRecord::Base
has_many :user_posts
has_many :received_posts, through: :user_posts, source: :recipient
end
class UserPost < ActiveRecord::Base
belongs_to :recipient, class_name: "User"
belongs_to :post
end
class Post < ActiveRecord::Base
has_many :user_posts
has_many :recipients, through: :user_posts, source: :recipient
end
Here's the schema of my join table:
UserPost
recipient_id: integer
post_id: integer
The error I get is:
PG::UndefinedColumn: ERROR: column user_posts.user_id does not exist
I've tried adding the source like above but to no avail. It makes me think that it's looking for the user_id and not recipient_id on the join table. I'm not sure how to correctly specify the custom id as the field to use. I've tried foreign keys and other answers on here but to no luck. Could be that I'm implementing foreign keys or source wrong.
Any help is much appreciated, Thanks.
In User model, add foreigh_key to :user_posts
class User > ActiveRecord::Base
has_many :user_posts, foreign_key: :recipient_id
has_many :received_posts, through: :user_posts, source: :recipient
end

Using foreign_key in rails associations has_many

Could somebody explain me is my code correct.
I'm trying to get foreign_key option in rails associations.
I have 2 models:
Book and Author
Book db schema:
name
user_id
Author db schema:
name
My models:
class Author < ApplicationRecord
has_many :books, foreign_key: :user_id
end
class Book < ApplicationRecord
belongs_to :author, foreign_key: :user_id
end
Here I don't understand why we should define foreign_key in both models. Is it necessarily?
If you have used the table and column names that Rails expects, then you do not need to explicitly define the foreign_key. In your case, if the foreign key column was named author_id, then you could get by quite simply:
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :author
end
However, in your case, the foreign key column is not named according to what Rails expects, so you have needed to explicitly define the foreign key column name. That's fine, but it does make a little more work for you.
In cases where you have explicitly defined the foreign key, you should define it for both associations. Your has_many association will not work without it.
In addition, you should define the inverse association:
class Author < ApplicationRecord
has_many :books, foreign_key: :user_id, inverse_of: :author
end
class Book < ApplicationRecord
belongs_to :author, foreign_key: :user_id, inverse_of: :books
end
Defining the inverse_of can cause ActiveRecord to make fewer queries, and gets rid of a few surprise behaviors. For an explanation of inverse_of, see Exploring the :inverse_of Option on Rails Model Associations by Ryan Stenberg

many to many polymorphic association

I'm not sure how to create this, I'd like to create a many-to-many polymorphic association.
I have a question model, which belongs to a company.
Now the question can has_many users, groups, or company. Depending on how you assign it.
I'd like to be able to assign the question to one / several users, or one / several groups, or the company it belongs to.
How do I go about setting this up?
In this case I would add a Assignment model which acts as an intersection between questions and the entities which are assigned to it.
Create the table
Lets run a generator to create the needed files:
rails g model assignment question:belongs_to assignee_id:integer assignee_type:string
Then let's open up the created migration file (db/migrations/...__create_assignments.rb):
class CreateAssignments < ActiveRecord::Migration
def change
create_table :assignments do |t|
t.integer :assignee_id
t.string :assignee_type
t.belongs_to :question, index: true, foreign_key: true
t.index [:assignee_id, :assignee_type]
t.timestamps null: false
end
end
end
If you're paying attention here you can see that we add a foreign key for question_id but not assignee_id. That's because the database does not know which table assignee_id points to and cannot enforce referential integrity*. We also add a compound index for [:assignee_id, :assignee_type] as they always will be queried together.
Setting up the relationship
class Assignment < ActiveRecord::Base
belongs_to :question
belongs_to :assignee, polymorphic: true
end
The polymorpic: true option tells ActiveRecord to look at the assignee_type column to decide which table to load assignee from.
class User < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
class Group < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
class Company < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
Unfortunately one of the caveats of polymorphic relationships is that you cannot eager load the polymorphic assignee relationship. Or declare a has_many :assignees, though: :assignments.
One workaround is:
class Group < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
def assignees
assignments.map(&:assignee)
end
end
But this can result in very inefficient SQL queries since each assignee will be loaded in a query!
Instead you can do something like this:
class Question < ActiveRecord::Base
has_many :assignments
# creates a relationship for each assignee type
['Company', 'Group', 'User'].each do |type|
has_many "#{type.downcase}_assignees".to_sym,
through: :assignments,
source: :assignee,
source_type: type
end
def assignees
(company_assignees + group_assignees + user_assignees)
end
end
Which will only cause one query per assignee type which is a big improvement.

how to create a 3 table join migration in ruby on rails

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

Resources