自作の投稿機能付きアプリを作っている際にタグ機能があったらいいなぁと思ったので、色々調べて導入してみました!
その際にいくつか困ったことが起きたので、備忘録として載せておきます。
タグ機能の実装方法について
大まかに
①自分で実装する
②gemを使う
の2選択肢があって、正直最初は前者で実装しようとしたのですが、手軽に使えるgemでチョチョイと入れてしまおうという悪魔の誘惑に勝てず、後者でいくことにしました。
(これが後に地獄となることに。。。)
参考にした記事
railsでタグ機能を実装する
タグ機能を実現するための便利なデータベース設計を3つ紹介
acts-as-taggable-onに決めた理由
単純にスターの数が多かったのと、導入にあたっての日本語に記事がたくさんあったからですね!
やはり日本語の記事が多いと導入コストが大幅に下がります。
導入
日本語の記事があればいいというものの、まずはgithubのRead.meを読みましょう。
また、日本語の参考にした記事も下記に置いておきます!
動的なタグ生成をするgem「acts-as-taggable-on」を使ってみました
gem のbundle install
Gemfile
gem 'acts-as-taggable-on', '~> 6.0'
ターミナル
bundle install
Mysqlを使っている場合のmigrate
DBをmysqlにしている場合は初期導入で色々問題があるようなのでmigrateを実行する前にrake acts_as_taggable_on_engine:install:migrationsを実行します。
※tagsテーブルのnameカラムは’binary encoded string'(パソコンで扱う2真数の文字列)として読み込まれるので、’utf8_bin’で読まなければいけないらしいです。
ターミナル
rake acts_as_taggable_on_engine:tag_names:collate_bin
rake acts_as_taggable_on_engine:install:migrations
rake db:migrate
今回おきたエラー
ターミナル
rake acts_as_taggable_on_engine:install:migrations
で6つmigrationファイルが作成されたのですが、migrateしようとしたら
Mysql2::Error: Cannot drop index 'index_taggings_on_tag_id': needed in a foreign key constraint: DROP INDEX `index_taggings_on_tag_id` ON `taggings`
/Users/tech-camp/projects/tech_faq/db/migrate/20191124071633_add_missing_unique_indices.acts_as_taggable_on_engine.rb:11:in `up'
とエラーが吐かれて止まりました。
中身をみてみるとindexを付与したり削除したりを繰り返していて一旦うまくいっている範囲だけでイケるだろうと思ってテストしたらNGが出ます。
※その場合のエラーは’tagging_count’がないよ!!って感じです。
結論 不要なadd_indexやremove_indexをコメントアウトする。
結果、remove_indexができない、tagging_countカラムが必要
という2点に絞り、必要なファイルだけ置いておきます。
一旦自分で使う時のファイルだけまとめておきます。
①acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb
②add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb
①acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb
# This migration comes from acts_as_taggable_on_engine (originally 1)
if ActiveRecord.gem_version >= Gem::Version.new('5.0')
class ActsAsTaggableOnMigration < ActiveRecord::Migration[4.2]; end
else
class ActsAsTaggableOnMigration < ActiveRecord::Migration; end
end
ActsAsTaggableOnMigration.class_eval do
def self.up
create_table ActsAsTaggableOn.tags_table do |t|
t.string :name
t.timestamps
end
create_table ActsAsTaggableOn.taggings_table do |t|
t.references :tag, foreign_key: { to_table: ActsAsTaggableOn.tags_table }
# You should make sure that the column created is
# long enough to store the required class names.
t.references :taggable, polymorphic: true
t.references :tagger, polymorphic: true
# Limit is created to prevent MySQL error on index
# length for MyISAM table type: http://bit.ly/vgW2Ql
t.string :context, limit: 128
t.datetime :created_at
end
add_index ActsAsTaggableOn.taggings_table, :tag_id
add_index ActsAsTaggableOn.taggings_table, [:taggable_id, :taggable_type, :context], name: 'taggings_taggable_context_idx'
end
def self.down
drop_table ActsAsTaggableOn.taggings_table
drop_table ActsAsTaggableOn.tags_table
end
end
②add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb
# This migration comes from acts_as_taggable_on_engine (originally 3)
if ActiveRecord.gem_version >= Gem::Version.new('5.0')
class AddTaggingsCounterCacheToTags < ActiveRecord::Migration[4.2]; end
else
class AddTaggingsCounterCacheToTags < ActiveRecord::Migration; end
end
AddTaggingsCounterCacheToTags.class_eval do
def self.up
add_column ActsAsTaggableOn.tags_table, :taggings_count, :integer, default: 0
# ActsAsTaggableOn::Tag.reset_column_information
# ActsAsTaggableOn::Tag.find_each do |tag|
# ActsAsTaggableOn::Tag.reset_counters(tag.id, ActsAsTaggableOn.taggings_table)
# end
end
def self.down
remove_column ActsAsTaggableOn.tags_table, :taggings_count
end
end
といった形でmigrationファイルを微調整しました。
migrate実行しただけで上手く導入できるようにしてもらいたかった・・・笑
モデル追記
今回は記事投稿機能だったので、articleモデルに下記を追加
class article < ActiveRecord::Base
< 省略 >
acts_as_taggable
# acts_as_taggable_on :skills, :interests これは使わない
end
投稿用ページにtag_listを送れるように追記
タグそのものはtag_listとして配列に格納されるようなので、tag_listをコントローラーに送れるようにします。
<%= form_for @article, url: {controller: "post", action: "create" } do |f| %>
<%# 省略 %>
<div class="tags">
<%= f.label :tag_list %>
<%= text_field_tag 'article[tag_list]', @article.tag_list.join(",") %>
</div>
<%= f.submit "送信する" %>
<% end %>
controllerでストロングパラメーターをセット
※初心者向けに
form_forに@articleを渡しているので、articleをrequireしています。
private
def article_params
params.require("article").permit(:tag_list)
end
表示画面にタグを出す
tag_listは配列なのでeach文で取り出します!!
<% article.tag_list.each do |tag| %>
<%= tag %>
<% end %>
終わりに
作業量は少なかったけど、gemは自分で実装していない分どのような形で実装するのか分からないのでエラーが出たらストレスが溜まりますね笑
一旦実装して落ち着いたら中間テーブルを使って自分で実装してみます!