RoR – Simple way to setup you database, models and associations

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.

Rails – many to many relationship with posts – keywords

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.