RailsでGemを使わずにタグ機能を実装する

なぜGemを使わずに自分で実装するのか

Railsで開発した事がある人なら、このacts-as-taggable-onというGemを知っている人は多いと思います。 タグ機能を開発するなら、このGemを使えばいいんじゃないの?と思われるかも知れません。 このGemを使えば、基本的なDBの構造も作ってくれるし、楽に実装できます。

他のGemも同様ですが、欠点もあります。

  • 変更するのが簡単じゃない
  • Gemの独自ルールの理解に時間がかかる

どこを変更するのか、変更した際にどこまで影響が出るのか、このあたりを把握するのに少し時間がかかります。

多言語対応できるタグ

今回、タグに3言語の名前を持たせられるようなものを作る必要がありました。 例えば、タグの名前が英語、日本語、中国語に対応するものです。

acts-as-taggable-onのgemを使っても出来るだろうとは思うものの、その学習コストを考えると自分で作った方が簡単で早いのではないかと思っています。

自分で作ったものなら、中身が把握できているので後の変更も楽です。

もちろんGemを使う良い点もたくさんありますが、そのメリット・デメリットを比較して使うべきだと思っています。

多言語対応できるタグ機能の実装

1. Tagモデルの実装

英語、日本語、中国語の3つの名前カラムを持つタグを実装していきます。 (name_en, name_ja, name_zh_cn)

$ rails g model Tag name_en:string name_ja:string name_zh_cn:string
class CreateTags < ActiveRecord::Migration
  def change
    create_table :tags do |t|
      t.string :name_en
      t.string :name_ja
      t.string :name_zh_cn

      t.timestamps null: false
    end
    add_index :tags, :name_en, unique: true
    add_index :tags, :name_ja, unique: true
    add_index :tags, :name_zh_cn, unique: true
  end
end
# app/models/tag.rb
class Tag < ActiveRecord::Base
end

2. Taggingモデルの実装

TagPostのリレーションを保持するモデルです。(many-to-many 多対多の中間テーブル)

重複を避けるため、tag_idpost_id にはユニークインデックスを作成します。

$ rails g model Tagging tag_id:integer post_id:integer
class CreateTaggings < ActiveRecord::Migration
  def change
    create_table :taggings do |t|
      t.integer :tag_id
      t.integer :post_id

      t.timestamps null: false
    end
    add_index :taggings, :tag_id
    add_index :taggings, [:tag_id, :post-id], unique: true
  end
end
# app/models/tagging.rb
class Tagging < ActiveRecord::Base
end

3. 各モデル間のリレーションの設定

典型的なmany-to-many(多対多)のリレーションです。

# app/models/post.rb
class Post < ActiveRecord::Base
  has_many :tags, through: :taggings
  has_many :taggings, dependent: :destroy
# app/models/tag.rb
class Tag < ActiveRecord::Base
  has_many :taggings, dependent: :destroy
  has_many :posts, through: :taggings
# app/models/tagging.rb
class Tagging < ActiveRecord::Base
  belongs_to :post
  belongs_to :tag

これで、以下のようにメソッドを呼べます。

> post = Post.find 1
> post.tags
=> returns the all related tags
>
> tag = Tag.find 1
> tag.posts
=> returns the all related posts
>
s> post.taggings.new(tag_id: 1)
=> #<Tagging:0x007ff9482e9e58 id: nil, tag_id: 1, post_id: 1, created_at: nil, updated_at: nil>

4. 多言語のタグ名を簡単に取得するメソッド

nameメソッドを追加して、簡単に多言語のタグ名を取得できるようにします。(該当する名前がnilの場合は、name_enにふぉーるばっくします。)

# app/models/tag.rb
class Tag < ActiveRecord::Base
  def name
    name = eval("self.name_#{I18n.locale}")
    name.present? ? name : name_en
  end
> tag = Tag.find 1
> I18n.locale = :en
> tag.name
=> 'Tag Name'
> I18n.locale = :ja
> tag.name
=> 'タグの名前'

That’s it!

シンプル・イズ・ベスト

Gemを使うことで、良いアプリケーションを作る事ができます。 ただ、大きすぎるGemや中身を理解しないままブラックボックスのようになってしまうと良くありません。 (ソースコードを呼んである程度理解すればブラックボックスにはなりませんが、そのために時間がかかります)

Gemを使うなというつもりは全然ありませんが、Gemを使う前に、本当にこのGemを使うべきかどうか自分自身に 問いかけるのが良いと思います。

Thanks!

@takp