This chapter is relatively straight forward and is more of a refresher than anything else.
It’s a reminder that spending time on determining the correct associations early will help save a lot of time in the future.
If you haven’t setup your Rails project you can find useful information in this post:
There’s also some great official documentation available, for example here.
Let’s jump straight into it. I will use generic model/table names here which are relevant to the sample project, but not the same, just for relative security purposes.
Below, I will demonstrate how to do a simple many to many relationship with Rails.
Understanding the above table diagram
Essentially you have 1 post which can have many keywords. A certain keyword, can belong to many posts.
We can map this out with a join table, in this case post_keywords
table. This table simply contains ID of the post
and ID of the keyword
it links.
If you just had a one to one or one to many relationship, simply having a foreign key on one of the tables is sufficient.
You first need to generate a model in Rails which will represent the table that you want to create, there’s 3 tables, so we’d generate 3 models. You can generate the models using commands below:
rails g model post
rails g model keyword
rails g model keywords_posts
Running these commands, you will see multiple things being generated, the model file, a migration file and some test files.
We will first focus on the migrations files;
create_posts.rb
migration:
class CreatePosts < ActiveRecord::Migration[6.0]
def change
create_table :posts do |t|
t.string :title
t.string :content
t.timestamps
end
end
end
create_keywords.rb
migration:
class CreateKeywords < ActiveRecord::Migration[6.0]
def change
create_table :keywords do |t|
t.string :content
t.timestamps
end
end
end
create_keywords_posts.rb
migration:
class CreatePostKeywords < ActiveRecord::Migration[6.0]
def change
create_join_table :posts, :keywords
end
end
With these migrations, we will have a post
model with title
and content
fields. keywords
will have a content
field. The keywords_posts
will join the two tables.
Note that the join table model should match the name of the table itself. If not, you’d have to specify the model manually on the belongs_to
or has_many
associations, like: has_many :posts_keywords, class_name: <ClassName>
We will now add the following content to the models in order to associate them in ActiveRecord.
post.rb
content:
class Post < ApplicationRecord
has_many :keywords_posts
has_many :keyword, through: :keywords_posts
end
keyword.rb
content:
class Keyword < ApplicationRecord
has_many :keywords_posts
has_many :post, through: :keywords_posts
end
post_keyword.rb
content:
class PostKeyword < ApplicationRecord
belongs_to :post
belongs_to :keyword
end
With all of these in place, you have the groundwork to create the associations as you need. Just make sure the spelling of the associations are correct.
You can start testing it with rails console
.
rails c
2.7.1 :001 > post = Post.create
(0.5ms) BEGIN
Post Create (0.7ms) INSERT INTO "posts" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id" [["created_at", "2020-08-02 13:45:19.013163"], ["updated_at", "2020-08-02 13:45:19.013163"]]
(0.5ms) COMMIT
2.7.1 :002 > kw = Keyword.create(content: :kw1)
(0.2ms) BEGIN
Keyword Create (0.7ms) INSERT INTO "keywords" ("content", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["content", "kw1"], ["created_at", "2020-08-02 13:45:33.369817"], ["updated_at", "2020-08-02 13:45:33.369817"]]
(6.3ms) COMMIT
2.7.1 :003 > kw2 = Keyword.create(content: :kw2)
(0.2ms) BEGIN
Keyword Create (0.4ms) INSERT INTO "keywords" ("content", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["content", "kw2"], ["created_at", "2020-08-02 13:45:43.420804"], ["updated_at", "2020-08-02 13:45:43.420804"]]
(6.6ms) COMMIT
2.7.1 :004 > post.keyword << kw
(0.2ms) BEGIN
KeywordsPost Create (0.5ms) INSERT INTO "keywords_posts" ("post_id", "keyword_id") VALUES ($1, $2) [["post_id", 11], ["keyword_id", 11]]
(0.4ms) COMMIT
Keyword Load (1.1ms) SELECT "keywords".* FROM "keywords" INNER JOIN "keywords_posts" ON "keywords"."id" = "keywords_posts"."keyword_id" WHERE "keywords_posts"."post_id" = $1 LIMIT $2 [["post_id", 11], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Keyword id: 11, content: "kw1", created_at: "2020-08-02 13:45:33", updated_at: "2020-08-02 13:45:33">]>
2.7.1 :005 > post.keyword << kw2
(2.7ms) BEGIN
KeywordsPost Create (0.5ms) INSERT INTO "keywords_posts" ("post_id", "keyword_id") VALUES ($1, $2) [["post_id", 11], ["keyword_id", 12]]
(6.8ms) COMMIT
Keyword Load (0.4ms) SELECT "keywords".* FROM "keywords" INNER JOIN "keywords_posts" ON "keywords"."id" = "keywords_posts"."keyword_id" WHERE "keywords_posts"."post_id" = $1 LIMIT $2 [["post_id", 11], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Keyword id: 11, content: "kw1", created_at: "2020-08-02 13:45:33", updated_at: "2020-08-02 13:45:33">, #<Keyword id: 12, content: "kw2", created_at: "2020-08-02 13:45:43", updated_at: "2020-08-02 13:45:43">]>
2.7.1 :008 > kw.post
Post Load (0.4ms) SELECT "posts".* FROM "posts" INNER JOIN "keywords_posts" ON "posts"."id" = "keywords_posts"."post_id" WHERE "keywords_posts"."keyword_id" = $1 LIMIT $2 [["keyword_id", 11], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Post id: 11, title: nil, content: nil, created_at: "2020-08-02 13:45:19", updated_at: "2020-08-02 13:45:19">]>
Now we have the associations between both the post and the keywords.