Rails many to many associations and has_many, through: - ruby-on-rails

I created my models a week ago, but I didn't know many things that I know now so it is time to create it from scratch.
What I want to accomplish is to create:
Lab model that can have many offers
Offer model that can have many labs.
#MIGRATION FILES BELOW:
class CreateLabs < ActiveRecord::Migration[5.0]
def change
create_table :labs do |t|
t.string :name
...
t.timestamps
end
end
end
class CreateOffers < ActiveRecord::Migration[5.0]
def change
create_table :offers do |t|
t.string :name
...
t.timestamps
end
end
end
# Join table:
class CreateLabChain < ActiveRecord::Migration[5.0]
def change
create_table :lab_chain do |t|
t.references :lab, foreign_key: true
t.references :offer, foreign_key: true
t.timestamps
end
end
end
And here is how the model files look like:
class Lab < ApplicationRecord
has_many :offers, through: :lab_chain
has_many :lab_chains
end
class Offer < ApplicationRecord
has_many :labs, through: :lab_chain
has_many :lab_chains
end
class LabChain < ApplicationRecord
belongs_to :lab
belongs_to :offer
end
I just want to know if I wrote it all correctly as I am not sure about all those tutorials I have watched and read.
Bonus question is what if I want my Offer to have many sections, and section to have many offer_items? Should I just add:
to Offer:
has_many :sections
has_many :offer_items, through: :section
and then to Section:
has_many :offer_items
belongs_to :offer
and the to OfferItem:
belongs_to :section
?
As I mentioned before, I volunteered as a guy that would make a website for our school project as I was the only one that had something to do with code (different language). It is harder than I thought.
EDIT
How would I also correctly add an self join in Section, so a section can have a subsection and so on?
Self joins addded to Section model
has_many :child_sections, class_name: "Section", foreign_key: "section_id"
belongs_to :parent_section, class_name: "Section"
Added to migration file
t.references :parent_section, foreign_key: "section_id"

That does look like a correct many to many association. You second also looks correct.
As an aside, it helps to draw a diagram of any database with more than 3 tables and if you plan on doing this as a job, it's well worth it to really grasp the whole table relationships fully as it's core to writing good model code.

Related

Ruby on Rails 6 ActiveRecord associations, one model with multiple references to another model

I am building a Ruby on Rails 6 application where I have a model Archive, and a another model ArchiveAgencies. In the Archive model I must have a sender agency and a receiver agency, which should represent ArchiveAgencies.
After going through the Rails docs, and some StackOverflow QA:
How do I add migration with multiple references to the same model in one table? Ruby/Rails
Defining two references to the same column in another table
Ruby on Rails: two references with different name to the same model
I came up with this approach:
Models
class Archive < ActiveRecord::Base
belongs_to :sender_agency, class_name: ArchiveAgencies, foreign_key: "sender_agency_id"
belongs_to :receiver_agency, class_name: ArchiveAgencies, foreign_key: "receiver_agency_id"
end
class ArchiveAgency < ActiveRecord::Base
has_many :archives, inverse_of: 'sender_agency'
has_many :archives, inverse_of: 'receiver_agency'
end
Migration
class CreateArchiveAgencies < ActiveRecord::Migration[6.0]
def change
create_table :archives do |t|
t.string :name
t.timestamps
end
end
end
class CreateArchives < ActiveRecord::Migration[6.0]
def change
create_table :archives do |t|
t.references :sender_agency, foreign_key: { to_table: :archive_agencies }
t.references :receiver_agency, foreign_key: { to_table: :archive_agencies }
t.timestamps
end
end
end
Is this the best approach for this case?
Would, having two inverse_of statements in the model ArchiveAgency work?
If you declare multiple associations with the same name the last one will overwrite the others.
Instead you need to use unique names for each association and configure them properly:
class ArchiveAgency < ActiveRecord::Base
has_many :archives_as_sender,
inverse_of: :sender_agency,
foreign_key: :sender_agency_id,
class_name: 'Archive'
has_many :archives_as_receiver,
inverse_of: :receiver_agency,
foreign_key: :receiver_agency_id,
class_name: 'Archive'
end
class Archive < ActiveRecord::Base
belongs_to :sender_agency, # foreign_key can be derived from the name
class_name: 'ArchiveAgency', # needs to be a string - not the class itself
inverse_of: :archives_as_sender
belongs_to :receiver_agency,
class_name: 'ArchiveAgency'
inverse_of: :archives_as_receiver
end

Ruby on Rails - Polymorphic associations + joint table + reference the same model twice?

I am having a hard time using polymorphic associations in a joint table where one option could lead to the association of two instances of the same table. I am relatively new to programming so I hope my question makes sense.
Basically, I have 3 models where my aim is to make associations between different products:
Product
ExtrenalProduct
Integration (polymorphic)/joint table
The Integration table will link 2 Products or 1 Product and 1 ExternalProduct
Here is my migration file:
class CreateIntegrations < ActiveRecord::Migration[5.1]
def change
create_table :integrations do |t|
t.references :main_product
t.belongs_to :integratable, polymorphic: true
t.timestamps
end
add_index :integrations, [:integratable_id, :integratable_type]
add_foreign_key :integrations, :products, column: :main_product_id, primary_key: :id
end
end
Here is my Integration Model:
class Integration < ApplicationRecord
belongs_to :integratable, polymorphic: true
belongs_to :main_product, class_name: 'Product'
end
Here is my ExternalProduct Model:
class ExternalProduct < ApplicationRecord
has_many :integrations, as: :integratable
end
Here is my Product Model:
has_many :integrations, class_name: 'Integration', foreign_key: 'main_product_id', dependent: :destroy
has_many :integrations, as: :integratable
My question is regarding the way I can query all Integrations from #product. As for now, I have to build a custom method:
def all_integrations
Integration.where(main_product_id: id).or(Integration.where(integratable_type: 'Product', integratable_id: id))
end
I would like to be able to do a: #product.integrations (which currently retrieves an empty array)
Any clue on what I am doing wrong or how I could make this code DRY? I feel I am missing something.
Thanks for reading!

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

How to implement playlist concept in Ruby on Rails

In my Ruby on Rails project I have a User model and a Content model.
A User has_many :contents and a Content belongs_to :user.
Now, I want to create the idea of playlist. There will be more than one playlists, and each one will have some contents in some order. At this moment, it doesn't matter if a user owns a playlist or not, they'll be general.
A playlist doesn't have any kind of association with a User. They will be general, owned by the system.
I think the solution will be something like having a model Playlist and another table with these attributes: playlist_id:integer content_id:integer order:integer. But do I really need to create all the MVC parts to this new relationship?
As I looked into Rails associations, I got confused and I don't know how to do this, if using the through property, using has_and_belongs_to_many in both Content and Playlist or even how to create this new relationship.
I'd be glad if someone could help me, as you can see, I'm a bit confused.
The solution for you is use has_many through
class User < ActiveRecord::Base
... user code in here with no association
end
class Playlist < ActiveRecord::Base
has_many :content_playlists
has_many :contents, through: :content_playlists
end
class Content < ActiveRecord::Base
has_many :content_playlists
has_many :playlists, through: :content_playlists
end
class ContentPlaylist < ActiveRecord::Base
belongs_to :content
belongs_to :playlist
end
The migration:
class CreateAll < ActiveRecord::Migration
def change
create_table :contents do |t|
t.string :name
t.timestamps
end
create_table :playlists do |t|
t.string :name
t.timestamps
end
create_table :content_playlists do |t|
t.belongs_to :content
t.belongs_to :playlist
t.integer :order
t.timestamps
end
add_index(:content_playlists, :content_id)
add_index(:content_playlists, :playlist_id)
end
end
Now, you can assign a order integer on content_playlists, and in the future you can reorder your playlist changing the value on contents_playlists.
To add a new content_playlist:
c = Content.create(name: "Song 2")
p = Playlist.create(name: "My Playlists2)
ContentPlaylist.create(content: c, playlist: p, order: 1)
Reference:
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
You can see (fork, clone, do whatever you want) here:
https://github.com/bdfantini/hmt_example
I'm guessing something like this is what you want:
class User < ActiveRecord::Base
...
has_many :contents
has_many :playlists
has_many :playlisted_contents, :through => :playlists
...
end
class Playlist < ActiveRecord::Base
...
has_many :contents
...
end
class Content < ActiveRecord::Base
...
belongs_to :user
belongs_to :playlist
...
end
I'd start there, and write some tests to see if it's behaving as you want. If your design has other constraints, we might need to adjust this some.

Resources