My tables are created using rails db:migrate with the following migration files

class CreateAuthors < ActiveRecord::Migration[6.1]
  def change
    create_table :authors do |t|
      t.string :name, null: false

      t.timestamps
    end
  end
end
class CreateAuthorDetails < ActiveRecord::Migration[6.1]
  def change
    create_table :author_details do |t|
      t.text :bio
      t.string :email
      t.string :website
      t.binary :profile_picture
      t.references :author, foreign_key: true, unique: true, on_delete: :cascade

      t.timestamps
    end
  end
end
class CreatePosts < ActiveRecord::Migration[6.1]
  def change
    create_table :posts do |t|
      t.string :title, null: false
      t.string :tag, array: true
      t.text :body, null: false
      t.references :author, foreign_key: true, null: false, on_delete: :cascade

      t.timestamps
    end
  end
end

Latter I would like to limit what could be entered into tag column of posts by creating another table such as

CREATE TABLE tag_details (
    id INTEGER PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    deacription TEXT
);

Then will have one to many from posts, tag filed into tag_details id. So the tag column has to be changed into interger array? How to to it. Thanks.


To link the posts table to the tag_details table and limit the values in the tag column to those available in the tag_details table, you can follow these steps:

  1. Create a migration to add a foreign key tag_detail_id to the posts table:
rails generate migration AddTagDetailToPosts tag_detail:references

This will generate a migration file that looks something like this:

class AddTagDetailToPosts < ActiveRecord::Migration[6.1]
  def change
    add_reference :posts, :tag_detail, null: false, foreign_key: true
  end
end
  1. Run the migration to add the tag_detail_id column to the posts table:
rails db:migrate
  1. Update the posts table to use an integer array for the tag column instead of a string array:
class ChangeTagToStringArray < ActiveRecord::Migration[6.1]
  def change
    remove_column :posts, :tag
    add_column :posts, :tag, :integer, array: true, default: [], null: false
  end
end

Note that we first remove the tag column and then add it back with a different data type (:integer) and the array: true option to allow multiple values. We also set a default value of an empty array ([]) and disallow null values (null: false).

  1. the migration to update the posts table:
rails db:migrate
  1. Create the TagDetail model to represent the tag_details table:
class TagDetail < ApplicationRecord
end
  1. Add a belongs_to association from the Post model to the TagDetail model:
class Post < ApplicationRecord
  belongs_to :tag_detail
end
  1. Update the Post model to use the tag_detail_id foreign key instead of the tag column:
class Post < ApplicationRecord
  belongs_to :tag_detail
  serialize :tag, Array
  before_validation :set_tag_from_name

  private

  def set_tag_from_name
    if tag_name.present?
      self.tag = tag_name.map do |name|
        TagDetail.find_or_create_by(name: name).id
      end
    end
  end
end

Here we use serialize :tag, Array to tell ActiveRecord to store the tag array as a serialized string in the posts table. We also define a private set_tag_from_name method that will be called before validation to convert the tag_name array (which contains the user-entered tag names) into an array of tag IDs from the tag_details table. This will ensure that only valid tags are stored in the tag column.

  1. Add a tag_name virtual attribute to the Post model:
class Post < ApplicationRecord
  belongs_to :tag_detail
  serialize :tag, Array
  before_validation :set_tag_from_name

  attr_accessor :tag_name

  private

  def set_tag_from_name
    if tag_name.present?
      self.tag = tag_name.map do |name|
        TagDetail.find_or_create_by(name: name).id
      end
    end
  end
end

The attr_accessor :tag_name line adds a virtual attribute `


Step 8. Add a tag_name virtual attribute to the Post model is not clear to me, how to add it?

class Post < ApplicationRecord
  # existing code...

  attr_accessor :tag_name

  # existing code...
end

This creates a new attribute tag_name that is not backed by a database column, but can be used to set the tag attribute of a Post instance using tag names rather than tag IDs.

With this attribute, you can then use the tag_name parameter in your controller to set the tag attribute of a new Post instance, like this:

class PostsController < ApplicationController
  def create
    @post = Post.new(post_params)
    # existing code...
  end

  private

  def post_params
    params.require(:post).permit(:title, :body, :author_id, :tag_name => [])
  end
end

This will allow you to create a new Post with one or more tag names instead of tag IDs, like this:

@post = Post.new(title: "My Post", body: "Lorem ipsum...", author_id: 1, tag_name: ["tag1", "tag2"])

I hope this clears up any confusion. Let me know if you have any further questions!

Regenerate response