
TechCampの短期集中プログラムを受講し、未経験からエンジニア転職を目指しているごんにんごんです。
冷蔵庫の食材を共有できるオリジナルアプリケーションをごりごり作ってます。前回は、ペルソナの定義〜DB設計まで行いました。詳しくは▼をご覧ください。
少しずつ進んでいて、以下機能まで実装できました。
- ユーザー管理機能の実装(ログイン/ログアウト)と単体テスト
- トップページの整備
- アプリケーションのロゴを設置(奥さんに描いてもらいました!!)
- 冷蔵庫のタグ付け機能
今回は、このタグ付け機能が少しややこしかったので、備忘録としてまとめたいと思います。
Contents
タグ付け機能の概要
何をやったのか??
- こちらのQiitaを参考に、ユーザーの冷蔵庫にタグ付けできる機能を実装した。
- 自身の理解のため、タグ付けできる便利なgemは使用しなかった。
- 完成形イメージ
- ①ユーザー登録画面から、任意のタグを入力
- ②トップページで、タグを1つずつ表示させる

モデル設計とアソシエーション

①Boxモデル
- 冷蔵庫の情報を取り扱うモデル
- 冷蔵庫のタグ”boxtag”とは、以下の関係から、多対多の関係性
- 1つの冷蔵庫は複数のタグを持つ
- 1つのタグは、複数の冷蔵庫に付与される
- 中間テーブル(box_boxtag_relation)を作り、お互いのテーブルを管理する。
class Box < ApplicationRecord
belongs_to :user
has_many :box_boxtag_relations
has_many :boxtags, through: :box_boxtag_relations
has_one_attached :image
end
②Boxtagモデル
- ユーザーが定義したタグの情報を取り扱うモデル
- 冷蔵庫の情報”box”とは、上記同様に多対多の関係性。
class Boxtag < ApplicationRecord
has_many :box_boxtag_relations
has_many :boxes, through: :box_boxtag_relations
validates :tag_name, uniqueness: true
end
③Box_boxtag_relationモデル
- “冷蔵庫テーブル”と”タグテーブル”を管理する中間テーブル
- それぞれの外部キー(box_idとboxtag_id)のみ保有する。
class BoxBoxtagRelation < ApplicationRecord
belongs_to :box
belongs_to :boxtag
end
タグ付け機能の実装
下準備
まずは、モデル設計とアソシエーション(以下README)に従って、下準備する。
- モデルとテーブルの生成
- Validationの設定
- アソシエーションの定義
# Boxes テーブル
| Column | Type | Options |
| ---------------- | ---------- | ------------------------------ |
| box_title | string | null: false |
| description | text | null: false |
| user | references | null: false, foreign_key: true |
### Association :Boxes
- belongs_to :user
- has_many :tags, through box_tags
- has_one :food
## Box-tags テーブル
| Column | Type | Options |
| ---------------- | ---------- | ------------------------------ |
| box | references | null: false, foreign_key: true |
| tag | references | null: false, foreign_key: true |
### Association :Box-tags
- belongs_to :box
- belongs_to :tag
## Boxtagsテーブル
| Column | Type | Options |
| ---------------- | ---------- | ----------- |
| tag_name | string | null: false |
### Association :Tags
- has_many :boxes, through box_tags
Boxesコントローラー部分
次に、冷蔵庫機能を管理するboxesコントローラーへ、タグ付けするための記述をしていきます。まずタグ付け機能のプロセスを箇条書きにすると、、、
- ユーザーが冷蔵庫登録フォームにて、任意のタグを入力する : boxesコントローラー newアクション
- createアクションにて、フォーム入力されたparamsの中から、タグの情報を取得する
- 現状、タグ情報はまとめて1つの文字列として格納されているので、タグを1つずつ分解する。
- 分解した状態にしてから、データベースへ保存するsaveメソッドへ送る。
※補足 : 本アプリケーションの冷蔵庫登録フォームでは、1つのフォームから「タグ情報」と「食材情報」を取得し複数のテーブルに保存する仕様のため、Formオブジェクトを用いています。そのため、後ほどFormオブジェクトのsaveメソッド部分への記載をおこなっていきます。
といった感じになりました。これを元に実装すると、以下のようになります。
def create
@box_form = BoxForm.new(box_form_params)
tag_list = params[:box_form][:tag_name].split(",") # ①
if @box_form.valid?
@box_form.save(tag_list) # ②
redirect_to root_path
else
render :new
end
end
private
def box_form_params
params.require(:box_form).permit(
:box_title,
:description,
:tag_name,
:image
).merge(
user_id: current_user.id
)
end
コード内のコメントアウトで表した部分について、それぞれ説明していきますね。
①タグを1つずつ分解する
# box_formというインスタンスに、フォームの入力値を格納している
@box_form = BoxForm.new(box_form_params)
tag_list = params[:box_form][:tag_name].split(",")
フォームから入力された情報は、paramsに配列形式で格納されています。格納のイメージは、、、
params[:box_form][:nick_name, :box_title, :description, :tag_name]
といった2次元ハッシュのような形でしょうか。つまり、params[:box_form][:tag_name]であれば、フォームのタグ名部分を指定することになります。
その後、splitメソッドを用いています。文字列.split(分解するルール)のように記載すると、指定されたルールに従って文字列を分解し、配列として戻してくれる便利なメソッドです。
今回の場合は、「タグはカンマで区切って入力」されているので.split(“,”)として、カンマごとに分解してもらっています。
splitによってタグが1つずつ分解された状態で、配列に格納できました。
@box_form = BoxForm.new(box_form_params)
# この時点のparams[:box_form][:tag_name] => "20代, 2人夫婦, 質実剛健"
tag_list = params[:box_form][:tag_name].split(",")
# この時点のparams[:box_form][:tag_name] => ["20代", "2人夫婦", "質実剛健"]
②タグの配列をデータベースに保存する
@box_form.save(tag_list)
①の処理で、バラバラにしたタグをtag_listという配列に格納しました。こちらを引数として、後述するsaveメソッドに渡しています。
saveメソッドの定義
こちらも先ほどのBoxesコントローラーと同じように、先ずやりたいことを箇条書きにしてから、実装しました。前提として、「1つのフォームで複数のテーブルに値を保存したい」ので、Formオブジェクトの中で、それぞれのテーブルに保存するようにしました。
- フォームのBoxテーブルに関する部分を保存し、インスタンスに格納しておく。
- タグが重複して保存されないよう、入力されたタグが登録済か判断。タグが、、、
- 登録済 -> 該当するタグのIDを引っ張ってくる。
- 未登録 -> 新たにインスタンスを生成する。
- 入力されたタグを一つずつデータベースに登録する。
- 中間テーブルも合わせて登録する。
# 冷蔵庫とタグのFormオブジェクトモデル
# saveメソッド定義
def save(tag_list)
box = Box.create(box_title: box_title, description: description, image: image, user_id: user_id)
i = 0
tag_list.each do |t| #①
relative_tag = Boxtag.where(tag_name: tag_list[i]).first_or_initialize
relative_tag.save
BoxBoxtagRelation.create(box_id: box.id, boxtag_id: relative_tag.id)
i += 1
end
end
first_or_initializeメソッド
入力されたtagが登録済かどうか判断するため、first_or_initializeメソッドを用いてます。
relative_tag = Boxtag.where(tag_name: tag_list[i]).first_or_initialize
relative_tag.save
上記のように、レコードを検索するActiveRecordのwhere文とセットで使用します。
もし、whereの条件式に、、、
- 一致 -> 該当するレコードのインスタンスを戻す。
- 不一致 ->新たにインスタンスを生成する。
という働きをします。
タグを表示する
これで、タグがテーブルに保存されたので、viewの実装に移ります。
viewでは、登録された冷蔵庫と紐付くタグを表示させます。まずは、例によってやりたいことを箇条書きにします。
- コントローラーでviewに渡すインスタンス(全ての投稿と全てのタグ)を定義する。
- 投稿された冷蔵庫を1つずつ(each do)表示させる。
- 2.のeach doの中で、その投稿に紐付けられているタグを割り出す。登録済タグとその投稿のタグのIDを比較し、一致したとき以下処理を行う。
- その投稿に紐づけられたタグということで、タグを表示する。
- その投稿にタグが1つ以上紐づけられたことを示すため、count upする。
- もし3.で、countがゼロ = タグ紐付けが無かったら、「タグ未登録」と表示する。
やりたいことに沿って、実装した結果が以下の通りです。
# Boxesコントローラー
def index
@boxes = Box.includes(:user).order("created_at DESC")
@box_tag_list = BoxBoxtagRelation.all
end
# Boxes#index
<ul class='item-lists'>
<% @boxes.each do |box| %>
<li class='list'>
<%= link_to "#" do %>
<div class='item-info'>
<% count = 0%>
<% @box_tag_list.each do |relation| %>
<% if box.id == relation.box_id %>
<div class='item-tag'>
<%= relation.boxtag.tag_name %>
<% count = count + 1 %>
</div>
<% end %>
<% end %>
<% if count == 0 %>
<div class='item-tag'>未登録</div>
<% end %>
</div>
<% end %>
</li>
<% end %>
</ul>
タグ未登録の場合は、「未登録」と表示されるようになってます。

まとめ
タグ機能の実装を一つずつ紐解いてみました。
機能実装するとき、「まずやりたいことを箇条書きにして、それに沿って実装していく」という基本的な進め方が身についてきたような気がします。
また、今回振り返ってみたことで、不必要だった記述が見つかったり、よくわかってなかったとこが明らかになったり、非常にタメになりました。
今後は、タグの編集や検索機能もつけていきたいと思います。