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


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


# 仕組みを理解する。

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

```:gem無しの場合

親コテゴリー - 中間カテゴリ-（親_子） - 子カテゴリー - 中間カテゴリー（子_孫) - 孫カテゴリー - 中間テービル（孫_商品） - 商品
```
gemがないと上記のような仕組みのため、非常にめんどくさいです。

```:gem'ancestry'の場合

商品 - 中間テーブル - カテゴリー
```
上記だけで完結します。だから非常に楽ですね
ではどうして、カテゴリーテーブル一つで３階層可能なのか？疑問になりますよね
一緒に学習しましょう！

## タグの３階層の仕組み
DBを見るとわかりやすいので、見ていきましょう！
![スクリーンショット 2020-02-23 3.58.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455403/fe42bbeb-8ad2-0152-524a-a1bc02505b89.png)

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

一番上の親カテゴリは祖先がないので、
ancestryカラム： NULL
![スクリーンショット 2020-02-23 4.01.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455403/0bc22a98-e555-dbf5-9fea-f363dcada694.png)


ID:14番目のレコードですが、ancestry: 1になってます。
祖先はID:1のレコードという意味ですね。つまりレディースが親カテゴリということです。
![スクリーンショット 2020-02-23 4.03.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455403/2a173050-594a-6365-a753-88ca28182c43.png)


では、さらにトップスよりも下の階層（３階層目）はどうなのか？
![スクリーンショット 2020-02-23 4.04.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455403/39cde333-9dfd-5448-9787-cd2bb9e6d0e9.png)
つまり、祖先はID:1番目の レディースになるということです。ancestry: 1/14となっています。
つまり、ID:1の子である,ID:14番目の子という表示


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

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

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

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

#### product_categoriesテーブル
|Column|Type|Options|
|------|----|-------|
|product_id|references|null: false|
|category_id|references|null: false|

実際にDBを見て理解していきましょう！！
[![Image from Gyazo](https://i.gyazo.com/3dc6309eb9a8235114eb8a0c3efbffc8.png)](https://gyazo.com/3dc6309eb9a8235114eb8a0c3efbffc8)
この例だと
productのID:1番目とcategory_id:７番目が紐づいてます。

![スクリーンショット 2020-02-23 4.30.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455403/ec56b065-7a4d-93ac-84c4-2f542713b4be.png)
categoryの7番目は、コスメ・香水・美容ですから、
商品は「コスメ・香水・美容」カテゴリに紐づいているとわかります。

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

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

## まとめ

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

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

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

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





# 実装をしていく。
それでは実装していきましょう！！

## インストール

```ruby: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


```ruby: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ファイルを記述していきます。

```ruby: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
```


#### アソシエーション

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

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

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

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

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

```ruby: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)

```ruby:seeds.rb
require './db/seeds/category.rb'
```

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

```ruby: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で追加表示

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


```ruby: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
```

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

```ruby:
@children = Category.find(params[:parent_id]).children
```
上記の.childenって何？ってなると思いますが、
親カテゴリから子カテゴリを取得するためのメソッドです。

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

```
親 ＞ 子：親カテゴリ.chidren ＞ 孫: 子カテゴリ.children
``` 
他にもいろんなメソッドが用意されているので、一度Githubを見ていただければと思います。
[Github:ancestroy](https://github.com/stefankroes/ancestry)

## Ajaxを導入する（JQuery）

### route.rb

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

```ruby: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対応のコントローラーにする。

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

```ruby: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に持っていきたい。

```ruby:children.jbuilder
json.array! @grandchildren do |child|
  json.id child.id
  json.name child.name
end
```

```ruby:grandchildren.jbuilder
json.array! @grandchildren do |grandchild|
  json.id grandchild.id
  json.name grandchild.name
end
```

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

```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](https://github.com/stefankroes/ancestry)
[Railsでタグ機能をgemを使わずに実装した際のメモ](https://qiita.com/tobita0000/items/daaf015fb98fb918b6b8)
[Railsのgem"ancestry"による多階層構造の実現](https://atora1992.hatenablog.com/entry/2019/07/08/215444?_ga=2.238805312.1127791881.1563800013-1192814443.1557397135)