Pocket

TechCampの短期集中プログラムを受講し、未経験からエンジニア転職を目指しているごんにんごんです。

冷蔵庫の食材を共有できるオリジナルアプリケーションをごりごり作ってます。前回は、ペルソナの定義〜DB設計まで行いました。詳しくは▼をご覧ください。

少しずつ進んでいて、以下機能まで実装できました。

  • ユーザー管理機能の実装(ログイン/ログアウト)と単体テスト
  • トップページの整備
  • アプリケーションのロゴを設置(奥さんに描いてもらいました!!)
  • 冷蔵庫のタグ付け機能

今回は、このタグ付け機能が少しややこしかったので、備忘録としてまとめたいと思います。

タグ付け機能の概要

何をやったのか??

  • こちらのQiitaを参考に、ユーザーの冷蔵庫にタグ付けできる機能を実装した。
  • 自身の理解のため、タグ付けできる便利なgemは使用しなかった。
  • 完成形イメージ
    • ①ユーザー登録画面から、任意のタグを入力
    • ②トップページで、タグを1つずつ表示させる
Image from Gyazo

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

①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モデル

  1. ユーザーが定義したタグの情報を取り扱うモデル
  2. 冷蔵庫の情報”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コントローラーへ、タグ付けするための記述をしていきます。まずタグ付け機能のプロセスを箇条書きにすると、、、

  1. ユーザーが冷蔵庫登録フォームにて、任意のタグを入力する : boxesコントローラー newアクション
  2. createアクションにて、フォーム入力されたparamsの中から、タグの情報を取得する
  3. 現状、タグ情報はまとめて1つの文字列として格納されているので、タグを1つずつ分解する。
  4. 分解した状態にしてから、データベースへ保存する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オブジェクトの中で、それぞれのテーブルに保存するようにしました。

  1. フォームのBoxテーブルに関する部分を保存し、インスタンスに格納しておく。
  2. タグが重複して保存されないよう、入力されたタグが登録済か判断。タグが、、、
    • 登録済 -> 該当するタグのIDを引っ張ってくる。
    • 未登録 -> 新たにインスタンスを生成する。
  3. 入力されたタグを一つずつデータベースに登録する。
  4. 中間テーブルも合わせて登録する。
# 冷蔵庫とタグの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では、登録された冷蔵庫と紐付くタグを表示させます。まずは、例によってやりたいことを箇条書きにします。

  1. コントローラーでviewに渡すインスタンス(全ての投稿と全てのタグ)を定義する。
  2. 投稿された冷蔵庫を1つずつ(each do)表示させる。
  3. 2.のeach doの中で、その投稿に紐付けられているタグを割り出す。登録済タグその投稿のタグのIDを比較し、一致したとき以下処理を行う。
    • その投稿に紐づけられたタグということで、タグを表示する。
    • その投稿にタグが1つ以上紐づけられたことを示すため、count upする。
  4. もし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>

タグ未登録の場合は、「未登録」と表示されるようになってます。

まとめ

タグ機能の実装を一つずつ紐解いてみました。

機能実装するとき、「まずやりたいことを箇条書きにして、それに沿って実装していく」という基本的な進め方が身についてきたような気がします。

また、今回振り返ってみたことで、不必要だった記述が見つかったり、よくわかってなかったとこが明らかになったり、非常にタメになりました。

今後は、タグの編集や検索機能もつけていきたいと思います。

投稿者

waco@jp

30代未経験でエンジニア転職を目指しています。前職はメーカーで営業と製品開発を担当。22年3月からTechCampを受講し、5月に卒業。現在、就活とオリジナルアプリケーション制作の両輪で活動しています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です