Help us understand the problem. What is going on with this article?

【Ruby on Rails】 Railsのgem"ancestry"によるタグ機能実装(多階層構造)

今回はタグ機能を実装するために、 Railsのgem"ancestry"を利用していきます。
これに関しては今回初めてなので、学習しながらメモしていきたいと思います

仕組みを理解する。

タグの階層とどうやって紐づけるか理解していきましょう!!

gem無しの場合
親コテゴリー - 中間カテゴリ-(親_子) - 子カテゴリー - 中間カテゴリー(子_孫) - 孫カテゴリー - 中間テービル(孫_商品) - 商品

gemがないと上記のような仕組みのため、非常にめんどくさいです。

gem'ancestry'の場合
商品 - 中間テーブル - カテゴリー

上記だけで完結します。だから非常に楽ですね
ではどうして、カテゴリーテーブル一つで3階層可能なのか?疑問になりますよね
一緒に学習しましょう!

タグの3階層の仕組み

DBを見るとわかりやすいので、見ていきましょう!
スクリーンショット 2020-02-23 3.58.31.png

3階層までタグが作成可能で、

一番上の親カテゴリは祖先がないので、
ancestryカラム: NULL
スクリーンショット 2020-02-23 4.01.30.png

ID:14番目のレコードですが、ancestry: 1になってます。
祖先はID:1のレコードという意味ですね。つまりレディースが親カテゴリということです。
スクリーンショット 2020-02-23 4.03.05.png

では、さらにトップスよりも下の階層(3階層目)はどうなのか?
スクリーンショット 2020-02-23 4.04.03.png
つまり、祖先はID:1番目の レディースになるということです。ancestry: 1/14となっています。
つまり、ID:1の子である,ID:14番目の子という表示

カテゴリータグの仕組み
1階層目: null
2階層目: 1階層目のID
3階層目: 1階層目のID/2階層目のID

3階層目のancestryカラムを取得すれば、2階層目、1階層目と辿れる仕組みです。3階層目までしか構造上できないようです(もしもできる場合は、コメントで教えていただければ幸いです。)4階層目を実装するには、別のテーブルが必要となります。

どうやって商品と紐づけるのか?

タグIDと商品IDを紐づける中間テーブルをおきます。

product_categoriesテーブル

Column Type Options
product_id references null: false
category_id references null: false

実際にDBを見て理解していきましょう!!
Image from Gyazo
この例だと
productのID:1番目とcategory_id:7番目が紐づいてます。

スクリーンショット 2020-02-23 4.30.25.png
categoryの7番目は、コスメ・香水・美容ですから、
商品は「コスメ・香水・美容」カテゴリに紐づいているとわかります。

このように、中間テーブルを利用して商品とカテゴリーを紐づけていきます。

モデル
商品 - 中間テーブル - カテゴリー

まとめ

カテゴリータグの仕組み
1階層目: null
2階層目: 1階層目のID
3階層目: 1階層目のID/2階層目のID
モデル
商品 - 中間テーブル - カテゴリー
表示する仕組み
1. htmlより、親カテゴリを並べる
2. 親カテゴリを選択されたら、コントローラーで子カテゴリを取得、JS(ajax)で追加表示
3. 子カテゴリが選択されたら、コントローラーで孫kてゴリを取得、JS(ajax)で追加表示

親 > 子 > 孫
親 > 子: 親.children > 孫: 子.children #ここは後述します。

実装をしていく。

それでは実装していきましょう!!

インストール

Gemfile
gem 'ancestry'

を追加して

ターミナル
 $ bundle instal

インストールをしたら、再起動させたいので、

ターミナル
 $ rails s

モデルの作成

商品モデルはすでに作成してる前提で、作成方法を記述しません。

カテゴリーモデルを作成します。

ターミナル
$ rails g model category

モデルを作成したら、migrateファイルを記述していきましょう!!

categoriesテーブル

Column Type Options
name string null: false, index: true
ancestry string index: true

Association

  • has_many :products
  • has_ancestry
migateファイル
class CreateCategories < ActiveRecord::Migration[5.2]
  def change
    create_table :categories do |t|
      t.string :name,     index: true, null: false
      t.string :ancestry, index: true
      t.timestamps
    end
  end
end
ターミナル
$ rake db:migrate

中間テーブル(product_categories)を作成

ターミナル
$ rails g model product_category

product_categoriesテーブル

Column Type Options
product_id references null: false
category_id references null: false

アソシエーション

  • belongs_to :product
  • belongs_to :category

上記のテーブルになるようにmigrateファイルを記述していきます。

migrateファイル
class CreateProductCategories < ActiveRecord::Migration[5.2]
  def change
    create_table :product_categories do |t|
      t.references :product, null:false
      t.references :category, null:false
      t.timestamps
    end
  end
end
ターミナル
rake db:migrate

アソシエーション

カテゴリーモデルは下記になります

category.rb
class Category < ApplicationRecord
  has_ancestry
  has_many :product_categories, dependent: :destroy
  has_many :products, through: :product_categories
end

中間テーブルは下記になります。

product.category.rb
class ProductCategory < ApplicationRecord
  belongs_to :product
  belongs_to :category
end

商品モデルは下記になります

product.rb
class Product < ApplicationRecord
  has_many :product_categories, dependent: :destroy
  has_many :categories, through: :product_categories
end

はい!これでモデルは完了ですね。
では次に進みましょう!!!

DB → モデル → コントローラー

seeds.rbでカテゴリを生成

今回はモデル別にseedファイルを作成するやり方をします
[通常方法](https://www.sejuku.net/blog/28395)

seeds.rb
require './db/seeds/category.rb'

カテゴリー用のseedファイルを作成しましょう
db > seedsのフォルダを作成 > category.rbを作成

seeds/category.rb
#親カテゴリ
lady = Category.create(name: "レディース")

#子カテゴリー
lady_1 = lady.children.create(name: "トップス")

#孫カテゴリー
lady_1.children.create([{name: "Tシャツ/カットソー(半袖/袖なし)"},{name: "Tシャツ/カットソー(七分/長袖)"},{name: "シャツ/ブラウス(半袖/袖なし)"},{name: "シャツ/ブラウス(七分/長袖)"},{name: "ポロシャツ"},{name: "キャミソール"},{name: "タンクトップ"},{name: "ホルターネック"},{name: "ニット/セーター"},{name: "チュニック"},{name: "カーディガン/ボレロ"},{name: "アンサンブル"},{name: "ベスト/ジレ"},{name: "パーカー"},{name: "トレーナー/スウェット"},{name: "ベアトップ/チューブトップ"},{name: "ジャージ"},{name: "その他"}])

では生成しましょう

ターミナル
$ rails db:seed

コントローラーの作成

DB → モデル → コントローラーの処理で進むので、コントローラーを作成します。

仕組み
1. htmlより、親カテゴリを並べる
2. 親カテゴリを選択されたら、コントローラーで子カテゴリを取得、JSで追加表示
3. 子カテゴリが選択されたら、コントローラーで孫kてゴリを取得、JSで追加表示

親 > 子 > 孫

この仕組みを動かすための独自メソッドを作成します。

products_controller.rb
class ProductsController < ApplicationController
  def get_category_children
    @children = Category.find(params[:parent_id]).children
  end

  def get_category_grandchildren
    @grandchildren = Category.find("#{params[:child_id]}").children
  end
end

と上記のアクションを作成します。

@children = Category.find(params[:parent_id]).children

上記の.childenって何?ってなると思いますが、
親カテゴリから子カテゴリを取得するためのメソッドです。

先ほど親>子>孫の順番で取得すると説明しましたが、.childrenを使うので下記のようになります。

親 > 子:親カテゴリ.chidren > 孫: 子カテゴリ.children

他にもいろんなメソッドが用意されているので、一度Githubを見ていただければと思います。
Github:ancestroy

Ajaxを導入する(JQuery)

route.rb

Ajaxに対応したルーティングにします。

route.rb
  resources :products do
    collection do # 新規用(new) usr:products/newのため
      get 'get_category_children', defaults: { format: 'json' }
      get 'get_category_grandchildren', defaults: { format: 'json' }
    end
    member do # 編集(edit用) usl: products/id/editのため
      get 'get_category_children', defaults: { format: 'json' }
      get 'get_category_grandchildren', defaults: { format: 'json' }
    end
  end 

Ajax対応のコントローラーにする。

先ほどコントローラーを作成していましたが、改良します

products_conroller.rb
  def get_category_children
    respond_to do |format| 
      format.html
      format.json do
        @children = Category.find(params[:parent_id]).children
      end
    end
  end
  def get_category_grandchildren
    respond_to do |format| 
      format.html
      format.json do
        @grandchildren = Category.find("#{params[:child_id]}").children
      end
    end
  end

これでAjax対応のコントローラーになりました。

jbuilder作成

コントローラーとAjax用jsとで情報の架け橋となるjbuilderを作成
子カテゴリを追加するために、idと名前をjsに持っていきたい。

children.jbuilder
json.array! @grandchildren do |child|
  json.id child.id
  json.name child.name
end
grandchildren.jbuilder
json.array! @grandchildren do |grandchild|
  json.id grandchild.id
  json.name grandchild.name
end

Ajax用のjs

Ajaxで要素を追加するjsを記述します。

category-ajax.js
$(document).on('turbolinks:load', function(){
  // カテゴリーの選択肢が入ったdiv
  var categoryBox = $('.form-details__form-box__category')
  // 親カテゴリー
  function appendOption(category) {
    var html = `<option value="${category.id}" data-category="${category.id}">${category.name}</option>`
  }

  // 子カテゴリー
  function appendChildBox(insertHTML) {
    var childSelectHtml = '';
    childSelectHtml = `<div class='form-select' id="child-category">
                        <select class= 'select-default' name="product[category_ids][]">
                            <option value>---</option>
                            ${insertHTML}
                          </select>
                          <i class='fa fa-angle-down icon-angle-down'></i>
                      </div>`
    categoryBox.append(childSelectHtml);
  }

  // カテゴリーボックスで親カテゴリが変わった場合
  categoryBox.on("change", "#parent-category", function(){
    var parentCategory = $("parent-category").value;
    if(parentCategory !== "") {
      $.ajax ({
        url: '/products/get_category_children',
        type: "GET",
        data: {
          parent_id: parentCategory
        },
        dataType: 'json'
      })
      .done(function(children){
        $('#child-category').remove();
        $('#grandchild-category').remove();
        var insertHTML = '';
        children.forEach(function(grandchild){
          insertHTML += appendOption(grandchild);
        });
        appendGrandchildrenBox(insertHTML);
      })
      .fail(function(){
        alert('カテゴリー取得に失敗しました');
      })
    } else {
      //親カテゴリーが初期値(---)の場合、子カテゴリー以下は非表示にする
      //親カテゴリが未選択の場合、子、孫カテゴリの選択欄は非表示にしたいので、そのように変更
      $('#child-category').remove(); 
      $('#grandchild-category').remove();
      $('#size').remove();
    }
  })
})

学習して作成中

参考になるサイト

Github:ancestroy
Railsでタグ機能をgemを使わずに実装した際のメモ
Railsのgem"ancestry"による多階層構造の実現

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした