ActiveRecord, polymorphic has_many, :through, :as - ruby-on-rails

I am new to Rails and I'm having a superb deal of difficulty wrapping my head around what seems to be a very simple database structure, but I'm thrown by the idea that objects must belong to other objects.
In a site that I am creating, a User may create many Posts.
A Post may fit into any number of many different Topics.
So what Rails would like is that Posts belong to both Topics and to Users, while Topics also belong to Posts (many-to-many?). This makes some sense in my head, but then I can't imagine how to create a Topic independent of a Post (which is reasonable to the site's function).
Is this possible? Any help would be greatly appreciated - this is giving me a headache!

You can use has_and_belongs_to_many (HABTM) for this kind of relationship:
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
has_and_belongs_to_many :topics
end
class Topic < ActiveRecord::Base
has_and_belongs_to_many :posts
end
in addition to the tables for users, posts and topics, you will need to create a migration for the HABTM relationship:
rails g migration create_posts_users post:references user:references
Note that the model names appear in alphabetical order in the table name

I don't think you are using polymorhpic relations here, just a regular many-to-many relation. Your model relations should look something like:
class User
has_many :posts
end
class Post
has_many :post_topics
has_many :topics, through: :post_topics
belongs_to :user
end
class Topic
has_many :post_topics
has_many :posts, through: :post_topics
end
class PostTopic
belongs_to :post
belongs_to :topic
end

This scenario is perfectly fine, a Topic can have may Posts, and a Post can have many Topics, which is a many-to-many relationship. In rails that would generally translate to has_and_belongs_to_many. Therefore you can define your models as follows:
class Post < ActiveRecord::Base
has_and_belongs_to_many :topics
end
class Topic < ActiveRecord::Base
has_and_belongs_to_many :posts
end
The corresponding generated Migration and database tables will look like this:
class CreatePotsAndTopics < ActiveRecord::Migration
def change
create_table :posts do |t|
t.string :title
t.timestamps
end
create_table :topics do |t|
t.string :name
t.timestamps
end
create_table :posts_topics, id: false do |t|
t.belongs_to :post
t.belongs_to :topic
end
end
end
As you can see both Topic and Post tables are standalone tables with no reference to any other table, which means they can be treated on their own. they way they get linked is through the posts_topics join table. Which enables you to do have access to #post.topics and #topic.posts
If you are uncomfortable with such a scenarios pick up a good rails book or do an online tutorial or course which walk you through creating a full rails application.

Related

Tricky Rails migration and modeling involving a self-join ":users" table and a ":bands" table: simultaneous one-to-many and many-to-many relationships

My :users table is successfully self-joined with all the necessary confusing (to this newbie) code and tables necessary to do that. :users's two groups are :teachers and :students.
I need to make the group :teachers join one-to-many with the :bands table (a band may have only one teacher) while at the same time joining :students many-to-many with the :bands table (a band may have many students and vice versa).
What's tripping me up is :students and :teachers are both :users. Therefore, if for a moment I pretend that there's only one kind of user and go for a one-to-many (teacher) relationship, then the Band model belongs_to :user and the User model has_many :bands
But if I go for the many-to-many (student) relationship instead, the Band model has_many :users, through :user_bands join table and the User model has_many :bands, through :user_bands join table. (UserBands model has belongs_to :user and belongs_to :band in this case)
But I need both relationships at the same time. I haven't actually tried putting has_many :bands in the User model while simultaneously having has_many :users (through join table) and belongs_to :users in the Bands model because, unless Rails is more magic than I give it credit for, it won't differentiate that teachers get the single-to-many while the students get the many-to-many.
I have not attempted my best guess (below) because I'm admittedly skittish: my database already has a sprawling number of many-to-many relations that are intact and functioning properly. The one time I attempted to make a complicated alteration to it earlier in the process, it thoroughly messed things up so badly that rolling back and undoing model alterations didn't get me back to where I'd started somehow, so I had to go back to rebuilding everything from scratch after pulling out enough hair for a tonsure. I do have github this time, so I should be able to revert the project if it blows up like before, but git is its own fussy minefield.
So if some folks could take a look at my guess first, I'd deeply appreciate it. Does it look right? Do I need to make changes before updating the database schema?:
In User model, add has_many :bands.
In Band model, add has_many :students, through :user_bands ; add belongs_to :teacher
In the create_bands migration, add t.belongs_to :teacher
In UserBands model, add belongs_to :teacher and add t.belongs_to :teacher in the create_user_bands migration.
The needed associations are not self-joining. Self-joins are primarily used to build tree like hierarchies from a single table - see the guides for a good example.
You just need multiple associations between two tables - the key thing here to remember is that you must use unique names for each association. If you declare multiple associations with the same name the later just overwrites the former without error.
Teachers
class AddTeacherIdToBands < ActiveRecord::Migration[5.2]
def change
add_reference :bands, :teacher, foreign_key: { to_table: :users }
end
end
class User < ApplicationRecord
has_many :bands_as_teacher,
class_name: 'Band',
foreign_key: 'teacher_id'
end
class Band < ApplicationRecord
belongs_to :teacher,
class_name: 'User'
end
We name the association bands_as_teacher to avoid conflict and confusion. This requires us to explicitly set the class_name and foreign_key options as they cannot be deduced from the name.
This is kind of where you tripped up and overcomplicated your solution. If the association is one to many you don't need to involve a join table.
Students
To create the association between a band and its members you need a m2m association through a join table:
class CreateBandMemberships < ActiveRecord::Migration[5.2]
def change
create_table :band_memberships do |t|
t.references :band, foreign_key: true
t.references :user, foreign_key: true
t.timestamps
end
end
end
class Band < ApplicationRecord
# ...
has_many :band_memberships
has_many :users, through: :band_memberships
end
class BandMembership < ApplicationRecord
belongs_to :band
belongs_to :user
end
class User < ApplicationRecord
# ...
has_many :band_memberships
has_many :bands, through: :band_memberships
end
You can improve the naming by providing the source option which tells Rails which association to use on the model its joining through.
class Band < ApplicationRecord
# ...
has_many :band_memberships
has_many :members, through: :band_memberships, source: :user
end
class User < ApplicationRecord
# ...
has_many :band_memberships
has_many :bands_as_member, through: :band_memberships, source: :band
end

Categories with comments and posts

I dont understand how to create this is relationships and routes.
I want to create comments for Category, and for Posts, It's must creating in Category#index and post#index pages.
Here is my code
category.rb
has_many :posts
has_many :comments, through: :posts
comment.rb
belongs_to :category
belongs_to :post
post.rb
belongs_to :category
has_many :comments, through: :category
My migration looks like
create_posts.rb
class CreatePosts < ActiveRecord::Migration[5.1]
def change
create_table :posts do |t|
t.integer :category_id
t.string :name
t.string :content
t.string :file
t.timestamps
end
end
end
And i dont understand how to create routes... for showing comments on post and on categories...
Here is my route
route.rb
resources :categories do
resources :posts
How to create a good relationships, good routes? I dont understand... how to create this model to model relationships and what the fields i must create in migrations...
Do you want it so that a comment can be created either for a post or for a category? In which case, it sounds like a job for a polymorphic association (http://guides.rubyonrails.org/association_basics.html#polymorphic-associations). Your associations would look something like this:
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
class Post < ActiveRecord::Base
has_many :comments, as: :commentable
end
class Category < ActiveRecord::Base
has_many :comments, as: :commentable
end
This way a post and category can both have many comments, and a comment itself is associated with only one of them (the database columns for commentable therefore has to store not only a reference id, commentable_id, but the type of model it's referencing, commentable_type - the docs link above has more on the data migration side).
If posts are associated with categories (a category having many posts, and a post having many categories), and you want to instead show comments for a category based on the posts within a category, then your code instead would look a little like this:
class Comment < ActiveRecord::Base
belongs_to :post
end
class Post < ActiveRecord::Base
has_many :comments
has_and_belongs_to_many :categories
end
class Category < ActiveRecord::Base
has_and_belongs_to_many :posts
has_many :comments, through: :posts
end
For this, you'll also need the database migration for a table called :categories_posts which will map posts and categories together, without the need for a model between the two. More on that at http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association.
Hope that helps!
According to your query you need to add category_id and post_id in comments table. The right way to do this by adding polymorphic association to the comments table. Go-rails posted similar tutorial on rails comments with polymorphic association

Having different associations between two models Rails

I am building an event app that lets users post events, and other users can 'register' themselves as attending that event. I currently have two models, 'User' and 'Article', where article is the event that a user can post. An article belongs_to :user and a user has_many :articles, dependent: :destroy.
I am trying to set up a new database to store which users plan to attend an event, in the form of a 'Registration', where it stores a user and the event he plans to attend. This means i need a many to many association between Users and Articles (as a user can attend multiple events, and an event can have multiple attendees). But will this not conflict with the original setting that states an article belongs to a single user?
How do i set this up so my associations dont interfere?
You could try either a has_many through: or a has_and_belongs_to_many relationship. Personally, I think I would use a HABTM for this, but the advantage of a HM Through is that there is an intermediate model, which can be used for additional information (such as whether an "attendee" is going or merely interested, etc): http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
As for having multiple different associations between the same two models, you can name the association anything you like but specify the class_name of the model you are pointing to: http://guides.rubyonrails.org/association_basics.html#has-and-belongs-to-many-association-reference
For example:
class Article < ActiveRecord::Base
belongs_to :user
has_and_belongs_to_many :attendees, class_name: "User", join_table: "articles_attendees", foreign_key: "attended_event_id", association_foreign_key: "attendee_id"
...
end
And for your User model:
class User < ActiveRecord::Base
has_many :articles
has_and_belongs_to_many :attended_events, class_name: "Article", join_table: "articles_attendees", foreign_key: "attendee_id", association_foreign_key: "attended_event_id"
...
end
That way you're able to name your association whatever you like, just be sure to keep your singulars singular and your plurals plural, and generally everything readable. class_name should be the name of the model to which you are defining the relationship. foreign_key is the database column name containing the ID of the models in which the relationship is defined. For example, in your User model, foreign_key should be the user ID. The association_foreign_key is the column containing the ID of the model to which you are linking.
Also don't forget to create your migration. Something like this example:
class CreateArticlesAttendees < ActiveRecord::Migration
def self.up
create_table :articles_attendees, :id => false do |t|
t.references :attended_event
t.references :attendee
end
add_index :articles_attendees, [:attended_event_id, :attendee_id]
add_index :articles_attendees, :attendee_id
end
def self.down
drop_table :articles_attendees
end
end

How to migrate a existing one-to-many relationship to many-to-many in Rails and ActiveRecord

I have a model A,
Class A < ActiveRecord::Base
has_many: names, class_name: 'B'
and a model B
class B < ActiveRecord::Base
belongs to :A
and there are already a bunch of data in database.
How do I write a migration to migrate them from one-to-many to many-to-many relationship? I prefer to use
has_many: through
if possible.
It's not hard to write the db migration, but what do I do to migrate the data in it?
This scenario comes up quite often in Rails projects and I'm surprised there still aren't a lot of how-tos out there as its a straightforward data evolution but requires some delicacy when dealing with already deployed systems.
I'm not sure if you're interested in polymorphic behavior for the many-to-many but I'm throwing that in as I find it useful for many many-to-many scenarios (pun intended! :-).
I had this before I started:
class Tag < ActiveRecord::Base
has_many :posts, inverse_of: :tag
class Post < ActiveRecord::Base
belongs_to :tag, inverse_of: :posts
I know, I know, why only one Tag for a Post? Turns out, I wanted my Posts to have multiple Tags after all. And then I thought, wait a minute, I want other things to have Tags as well, such as some kind of Thing.
You could use :has_and_belongs_to_many for each of Posts-Tags and Things-Tags but then that makes for 2 join tables and we'll probably want to Tag more entities as they get added right? The has_many :through is a great option here for one side of our associations and avoids having multiple join tables.
We are going to do this in 2 STEPS involving 2 deploys:
Step 1 - No change to existing associations. A new Taggable model/migration that will be polymorphic with respect to Posts and Things. DEPLOY.
Step 2 - Update associations. New migration to drop the old :tag_id foreign_key from Posts. DEPLOY.
The two steps are necessary to be able to execute your migration in Step 1 using your previous association definitions, otherwise your new associations will not work.
I think two steps is the simplest approach, recommended if your traffic is low enough that the risk of additional Tags being created on Posts/Things in between the two steps is low enough. If your traffic is very high, you can combine these two steps into one but you'll need to use different association names and then go back to delete the old unused ones after a working rollout. I'll leave the 1 step approach as an exercise for the reader :-)
Step 1
Create a model migration for a new polymorphic join table.
rails g model Taggable tag_id:integer tagged_id:integer tagged_type:string --timestamps=false
Edit the resulting migration to revert to using #up and #down (instead of #change) and add the data migration:
class CreateTaggables < ActiveRecord::Migration
def up
create_table :taggables do |t|
t.integer :tag_id
t.integer :tagged_id
t.string :tagged_type
end
# we pull Posts here as they have the foreign_key to tags...
Posts.all.each do |p|
Taggable.create(tag_id: p.tag_id, tagged_id: p.id, tagged_type: "Post")
end
end
def down
drop_table :taggables
end
end
Edit your new model:
class Taggable < ActiveRecord::Base
belongs_to :tag
belongs_to :tagged, polymorphic: true
end
At this point, DEPLOY your new model and migration. Great.
Step 2
Now we're going to update our class definitions:
class Tag < ActiveRecord::Base
has_many :taggables
has_many :posts, through: :taggables, source: :tagged, source_type: "Post"
has_many :things, through: :taggables, source: :tagged, source_type: "Thing"
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags, join_table: 'taggables', foreign_key: :tagged_id
class Thing < ActiveRecord::Base
has_and_belongs_to_many :tags, join_table: 'taggables', foreign_key: :tagged_id
You should be able to add dependent: :destroy on has_many :posts and has_many :things as :tag is a belongs_to on Taggable.
Don't forget to drop your old foreign_key:
class RemoveTagIdFromPosts < ActiveRecord::Migration
def up
remove_column :posts, :tag_id
end
def down
add_column :posts, :tag_id, :integer
end
end
Update your specs!
DEPLOY!

A Ruby on Rails has_many relationship.

I'm new to RoR and working on my first project. The basic concept behind the idea is to connect "Users" that have chosen a set a "Skills" with other Users that that have submitted a "Help Request" that deals specifically with those chosen skills. An app that connects Skilled Users with Users that need help if you will. My question has to do with the relationship between the Users, Skills, and Help_Request Models. It feels like a "has_many :through association" or maybe "polymorphic association" might be in order for this kind of three way relationship? Really not sure?
Any thoughts or suggestions would be greatly appreciated.
A polymorphic association is when a model should belong to another model. Let's say when you a comment model. You can comment on a post and a comment itself. That's when you would use a polymorphic.
In your case a simple has_many through would do.
It should look like this
class User < ActiveRecord::Base
has_many :skills
has_many :help_requests, through: :skills
end
class Skill < ActiveRecord::Base
belongs_to :user
belongs_to :helpRequest
end
class HelpRequest < ActiveRecord::Base
has_many :skills
has_many :users, through :skills
end
For more information the docs
I'm afraid ShivamD's answer will not suffice. That would require duplicate Skills, and queries the relevant User.help_requests via relationship, where instead you need an intersection.
Not just that, Users who create HelpRequests will already have a has_many relationship. That makes sense.
I won't include schema for the HABTM I'm about to suggest (see here), but I will cover the models. Essentially what you want:
skill = Skill.create(name: "My Skill")
user.skills << skill
help_request.skills << skill
user.matched_help_requests
#> [help_request]
Which can be achieved as follows:
class User
has_and_belongs_to_many :skills
def matched_help_requests
HelpRequest.joins(:skills).where("skills.id IN(?)", skills.pluck(:id))
end
end
class HelpRequest
has_and_belongs_to_many :skills
end
class Skill
has_and_belongs_to_many :users
has_and_belongs_to_many :help_requests
end
EDIT: here's the schmea for the HABTM:
rails g migration add_skills_join_tables
within migration
def change
create_table :skills_users, id: false do |t|
t.references :skill
t.references :user
end
create_table :help_requests_skills, id: false do |t|
t.references :skill
t.references :help_request
end
add_index :skills_users, [:skill_id, :user_id]
add_index :help_request_skills, [:skill_id, :help_request_id]
end
One hay is to create a has_and_belongs_to_manyrelation between the Users and Helprequests and Skills, so you end up with this:
class User < ActiveRecord::Base
has_and_belongs_to_many :skills
end
class Help_request < ActiveRecord::Base
has_and_belongs_to_many :skills
end
class Skills < ActiveRecord::Base
has_and_belongs_to_many :users
has_and_belongs_to_many :help_requests
end
Then you need to create the tables
rails g migration add_skills_users_table
rails g migration add_help_requests_skills_table
In the end run rake db:migrate
You can then search for it using User.first.skills

Resources