How associations can be set with non id foreign key? - ruby-on-rails

Currently I have following migrations:
class CreateDevices < ActiveRecord::Migration[5.0]
def change
create_table :devices do |t|
t.string :name
t.string :abbr
t.timestamps
end
end
end
class CreateVendors < ActiveRecord::Migration[5.0]
def change
create_table :vendors do |t|
t.string :name
t.string :abbr
t.timestamps
end
end
end
class CreateDeviceVendors < ActiveRecord::Migration[5.0]
def change
create_table :device_vendors do |t|
t.string :device
t.string :vendor
t.timestamps
end
end
end
There is many to many relationship between device and vendor, so DeviceVendors table is getting used for that. Both tables abbr column (which is unique) is getting saved in this table as device and vendor respectively.
I am using this kind of table structure so that I can seed the data and don't have to check for ids in the primary tables.
How can I set the association in all three models so that I can access in better way. Something like this:
class Device < ApplicationRecord
has_many :device_vendors
has_many :vendors, through: device_vendors
end
class Vendor < ApplicationRecord
has_many :device_vendors
has_many :devices, through: device_vendors
end
class DeviceVendor < ApplicationRecord
belongs_to :device
belongs_to :vendor
end
I know I have to apply foreign_key: :abbr to belongs_to in models but not sure in which ones. Also whether I need to change/add the migration for this?

The foreign_key, as you point out, is on the belongs_to table, but you need to specify both primary_key and foreign_key (since none is the default id) in all associations:
class Device < ApplicationRecord
has_many :device_vendors, primary_key: "abbr", foreign_key: "device"
has_many :vendors, through: device_vendors
end
class Vendor < ApplicationRecord
has_many :device_vendors, primary_key: "abbr", foreign_key: "vendor"
has_many :devices, through: device_vendors
end
class DeviceVendor < ApplicationRecord
belongs_to :device, primary_key: "abbr", foreign_key: "device"
belongs_to :vendor, primary_key: "abbr", foreign_key: "vendor"
end
Also notice that the foreign key is not abbr, that's the primary key in both device and vendor; the foreign key is the one in the table with belongs_to (i.e. device and vendor in device_vendors).

Related

rails - can't save data in associated models in RoR

I have 2 Models with association has_many along with cascade property between them.
class ServicesBrandDetail < ApplicationRecord
has_many :services_brands, foreign_key: "brand_id", dependent: :delete_all
end
class ServicesBrand < ApplicationRecord
belongs_to :services_brand_details, foreign_key: "brand_id",
end
Migration for both files
class CreateServicesBrandDetails < ActiveRecord::Migration[6.1]
def change
create_table :services_brand_details do |t|
t.string :brand
t.string :mail_list
t.string :cc_list
t.timestamps
end
end
end
class CreateServicesBrands < ActiveRecord::Migration[6.1]
def change
create_table :services_brands do |t|
t.string :warehouse
t.references :brand, null: false, foreign_key: {to_table: :services_brand_details}
t.timestamps
end
end
end
Now I was able to create and save data in from ServicesBrandDetails model. but the Problem is when i create record from ServiceBrand It created record perfectly but i was not able to store data in DB.
record = ServicesBrandDetail.create(:brand => "a", :mail_list => 'abc#mail.com', :cc_list => 'def#mail.com')
record.save
Record successfully stored in DB.
child = record.services_brands.new(:warehouse => "in") <-- record was created successfully.
child.save
it give me error
C:/Ruby30-x64/lib/ruby/gems/3.0.0/gems/activerecord-6.1.5/lib/active_record/inheritance.rb:237:in `compute_type': uninitialized constant ServicesBrand::ServicesBrandDetails (NameError)
Please follow proper Naming convention
This article might help - https://www.bigbinary.com/learn-rubyonrails-book/summarizing-rails-naming-conventions
In ServiceBrand Model
class ServiceBrand < ApplicationRecord
belongs_to :brand, class_name: 'ServiceBrandDetail'
end
belongs_to should be foreign key name i.e brand in your case
You can delete existing models and tables from your codebase and try below one. (I've tested)
class ServiceBrandDetail < ApplicationRecord
has_many :service_brands, foreign_key: :brand_id, dependent: :delete_all
end
class ServiceBrand < ApplicationRecord
belongs_to :brand, class_name: 'ServiceBrandDetail'
end
Migration for both files
class CreateServiceBrandDetails < ActiveRecord::Migration[6.1]
def change
create_table :service_brand_details do |t|
t.string :brand
t.string :mail_list
t.string :cc_list
t.timestamps
end
end
end
class CreateServiceBrands < ActiveRecord::Migration[6.1]
def change
create_table :service_brands do |t|
t.string :warehouse
t.references :brand, null: false, foreign_key: {to_table: :service_brand_details}
t.timestamps
end
end
end
Then try to create model objects which you tried in your question. It will work 👍🏽
In your model ServicesBrand you have to use singular association name for belongs_to
Change this belongs_to :services_brand_details to this belongs_to :services_brand_detail
class ServicesBrand < ApplicationRecord
belongs_to :services_brand_detail, foreign_key: "brand_id"
end

has_one and belong_to associations with the same table

I wonder is it ok to have like 2 associations with the same table. For example:
class CreateTeams < ActiveRecord::Migration[6.0]
def change
create_table :teams do |t|
t.string :name, null: false
t.references :manager, foreign_key: { to_table: 'users' }
end
end
end
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name, null: false
t.references :team
end
end
end
class Team < ApplicationRecord
has_many :users
belongs_to :manager, class_name: 'User', foreign_key: 'manager_id'
end
class User < ApplicationRecord
belongs_to :team
has_one :child_team, class_name: 'Team' # bad name, but just for example
end
Or it would be better to create a join table with team_id, user_id, and member_type?
Ruby/Rails versions do not matter but let's assume Ruby is 2.7.0 and Rails is 6.0.0
From a technical point of view - that's perfectly fine, but be careful with possible foreign key loop in the future.
This is more a question of architecture and your predictions of how system will evolve. A many-to-many relation with a explicit join model is more flexible. For example:
does manager always belong to the team? with a join table it's easier to fetch "all users from the team, no matter the role" or "all the teams a person has relation to, also no matter the role"
if there will be other roles or multiple people at same position - join table will also come handy
What you have seems fine.
Though a join table would be more flexible to provide more roles. This also avoids having a circular dependency in setting up teams and users.
class CreateTeams < ActiveRecord::Migration[6.0]
def change
create_table :teams do |t|
t.string :name, null: false
t.timestamps
end
end
end
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name, null: false
t.timestamps
end
end
end
class CreateTeamMembers < ActiveRecord::Migration[6.0]
def change
create_table :team_members do |t|
t.belongs_to :user, null: false, foreign_key: true
t.belongs_to :team, null: false, foreign_key: true
t.integer :role, null: false
t.timestamps
# Enforce one manager per team
t.index [:team_id],
name: :one_manager,
unique: true,
where: "role = 0"
end
end
end
class TeamMember < ApplicationRecord
enum role: { manager: 0, player: 1, fan: 2 }
belongs_to :user
belongs_to :team
end
class Team < ApplicationRecord
has_many :users, through: :team_members
has_many :team_members, dependent: :destroy
has_one :manager, -> { where(role: :manager) }, class_name: "TeamMember"
has_many :players, -> { where(role: :player) }, class_name: "TeamMember"
has_many :fans, -> { where(role: :fan) }, class_name: "TeamMember"
end
class User < ApplicationRecord
has_many :team_memberships, dependent: :destroy, class_name: "TeamMember"
has_many :teams, through: :team_memberships
end
You could even potentially take advantage of single table inheritance to differentiate your users by their role.
This is something you could migrate to later if necessary.

How to associate 2 many_to_many relationship in Rails

How do you associate a double many_to_many relationship? and also, what is it called? I know that there's no "double" many_to_many.
So have these models in rails, a User, Role, UserRole, Menu, RoleMenu.
A user can access menus depending on the roles. On console, I can do this User.first.roles.first.menus. My question is there a way to do like this User.first.menus, so it'll shorten? How do you associate User to Menu? What should I add to my models? what migration should I create?
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, through: :user_roles
end
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :email
t.timestamps null: false
end
end
end
class Role < ActiveRecord::Base
has_many :user_roles
has_many :users, through: :user_roles
has_many :role_menus
has_many :menus, through: :role_menus
end
class CreateRoles < ActiveRecord::Migration
def change
create_table :roles do |t|
t.string :name
t.timestamps null: false
end
end
end
class UserRole < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
class CreateUserRoles < ActiveRecord::Migration
def change
create_table :user_roles do |t|
t.belongs_to :user
t.belongs_to :role
t.timestamps null: false
end
end
end
class Menu < ActiveRecord::Base
has_many :role_menus
has_many :roles, through: :role_menus
end
class CreateMenus < ActiveRecord::Migration
def change
create_table :menus do |t|
t.string :name
t.timestamps null: false
end
end
end
class RoleMenu < ActiveRecord::Base
belongs_to :role
belongs_to :menu
end
class CreateRoleMenus < ActiveRecord::Migration
def change
create_table :role_menus do |t|
t.belongs_to :role
t.belongs_to :menu
t.timestamps null: false
end
end
end
Did you mean User.first.menus instead of User.menus ? Because, latter can't be achieved as you are trying to access menus through User class (which is more of a scope implementation) and not the particular user.
For the first case, as I can see that you are already aware of the has_many, through association. We will use the same to achieve that. Following should work.
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, through: :user_roles
has_many :menus, through: :roles
end
How do you associate a double many_to_many relationship? and also, what is it called? I know that there's no "double" many_to_many.
Well, yes, there's nothing called double many to many association but it is more aptly called multiple or nested many to many relation/association. And as mentioned above, it can be achieved through has_many, through
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, through: :user_roles
has_many :menus, through: :roles
end
adding another has_many.. through should work

polymorphic association and creating a table for it

Say I have this:
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
class Employee < ApplicationRecord
has_many :pictures, as: :imageable
end
class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
Does this require me to define a table exactly in the following way?
class CreatePictures < ActiveRecord::Migration[5.0]
def change
create_table :pictures do |t|
t.string :name
t.integer :imageable_id
t.string :imageable_type
t.timestamps
end
add_index :pictures, [:imageable_type, :imageable_id]
end
end
Or may I define a bit differently, with different columns or types, for example, that is, in a way I see more efficient? Will the polymorphic association remain functioning?
The polymorphic association only relates to the _type and _id pair of columns. Everything else is up to you.
So yes, you can add additional metadata if you like.

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