メルカリのクローンアプリ作成に際して、商品カテゴリーテーブル、商品出品ページ、出品商品の編集ページを実装しました。
初心者にも、すごく便利に感じたancestryに関する備忘録です。
最終ゴール
実装したい動作
- 親カテゴリーの表示(haml)
- 親カテゴリーが選択されたら、紐づく子カテゴリーを表示(Ajax)
- 子カテゴリーが選択されたら、紐づく孫カテゴリーを表示(Ajax)
- 孫カテゴリーが選択されたら、サイズセレクトボックスを表示(jQuery)
0. 準備
- gemの導入 →
bundle install
gem 'ancestry'
- モデル
class Item < ApplicationRecord
belongs_to :category
end
class Category < ApplicationRecord
has_many :items
has_ancestry #この記述を追加することで階層化できる
end
- 階層化したいデータを
db/seeds.rb
に書き、rails db:seed
すると、categoriesテーブルにデータが入る!! -
seeds.rb
とは、何者か? 簡単に言うと、初期データのこと。
例えば、DBをリセットした場合、データも消えてしまう。そんな時、seeds.rb
に初期データを残しておくと、rails db:seed
で復元できる。つまり、**データの変更や削除は基本行わないけど、DBで管理したいって時が使い時!**と理解しました。 - 当初、ancestryカラムにnull制約をかけていたため、以下のエラーが発生しました。null制約を外してから、再度
rails db:seed
すると、テーブルに無事データが入りました。
Mysql2::Error: Field 'ancestry' doesn't have a default value
# レディース
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: "その他"}])
lady_2 = lady.children.create(name: "ジャケット/アウター")
lady_2.children.create([{name: "テーラードジャケット"},{name: "ノーカラージャケット"},{name: "Gジャン/デニムジャケット"},{name: "レザージャケット"},{name: "ダウンジャケット"},{name: "ライダースジャケット"},{name: "ミリタリージャケット"},{name: "ダウンベスト"},{name: "ジャンパー/ブルゾン"},{name: "ポンチョ"},{name: "ロングコート"},{name: "トレンチコート"},{name: "ダッフルコート"},{name: "ピーコート"},{name: "チェスターコート"},{name: "モッズコート"},{name: "スタジャン"},{name: "毛皮/ファーコート"},{name: "スプリングコート"},{name: "スカジャン"},{name: "その他"}])
:
作成されたcategoriesテーブル
・ 親カテゴリーのancestryは、NULL
・ 子カテゴリーのancestryには、親のid
が入り、紐づいている
・ 孫カテゴリーのancestryには、親と子のid
が入り、紐づいている( 表記: 親のid/子のid )
id | name | ancestry |
---|---|---|
1 | レディース | NULL |
3 | トップス | 1 |
2 | Tシャツ/カットソー(半袖/袖なし) | 1/2 |
: | : | : |
21 | ジャケット/アウター | 1 |
22 | テーラードジャケット | 1/21 |
: | : | : |
1. 親カテゴリーの作成(ビュー)
- 親カテゴリーのみ
haml
に記載。子カテゴリー、孫カテゴリーは設置場所のみ確保。 - 親カテゴリーが選択されたタイミングで、JSで、親カテゴリーに紐づく子カテゴリーを表示。孫カテゴリーも同様の流れ。
:
.item_input__body
:
.item_input__body__category#category_box
.pulldown#partent_box
= f.select :category_id, options_for_select( @category_parent_array.map{|c| [c[:name], c[:id]]}),{include_blank: "選択してください"}, { class: "parent_category_box", id: "parent_category"}
= icon('fa', 'chevron-down')
.pulldown.item_input__body__category__children#children_box
-#親カテゴリー選択によって子カテゴリー表示
.pulldown.item_input__body__category__grandchildren#grandchildren_box
-#子カテゴリー選択によって孫カテゴリー表示
:
#size_box
:
.pulldown#size_selectbox
=f.collection_select :size_id, Size.all, :id, :name, {include_blank: "選択してください"}, {class: "size_box"}
= icon('fa', 'chevron-down')
:
2.親カテゴリー ⇄ 子カテゴリー ⇄ 孫カテゴリーの関係を定義
(コントローラー)
・ 親カテゴリー: ancestryがNULL のレコードを取得し、@category_parent_arrayに格納(複数のビューで使い回したいので、インスタンス変数で定義)。
・ 子カテゴリーと孫カテゴリーは、親カテゴリーから、Ajax通信で紐づくデータを取得。
・ 子カテゴリー:親カテゴリーのnameから、紐づく子カテゴリー一覧を取得し、@category_childrenに格納。
・ 孫カテゴリー:子カテゴリーのidから、紐づく孫カテゴリー一覧を取得し、@category_grandchildrenに格納。
class ItemsController < ApplicationController
before_action :set_category, only: [:new, :edit, :create, :update, :destroy]
before_action :set_item, only: [:show, :edit, :update, :destroy, :purchase, :buy]
#jsonで親の名前で検索し、紐づく小カテゴリーの配列を取得
def get_category_children
@category_children = Category.find(params[:parent_name]).children
end
#jsonで子カテゴリーに紐づく孫カテゴリーの配列を取得
def get_category_grandchildren
@category_grandchildren = Category.find("#{params[:child_id]}").children
end
private
#親カテゴリー
def set_category
@category_parent_array = Category.where(ancestry: nil)
end
def item_params
params.require(:item).permit(
:category_id,
:size_id,
#省略
)
end
end
(ルーティング)
・ resources :自動で生成されないactionへのパスを設定
・ member :パスに、:id情報が付く場合に使用
・ collection :パスに、:id情報が付かない場合に使用
※ 子カテゴリーと孫カテゴリーは、jsonをフォーマットにしておく。
resources :items do
collection do
get 'get_category_children', defaults: { format: 'json' }
get 'get_category_grandchildren', defaults: { format: 'json' }
end
member do
get 'get_category_children', defaults: { format: 'json' }
get 'get_category_grandchildren', defaults: { format: 'json' }
end
end
(jbuilder)
子カテゴリーのidとnameを取得し、@category_childrenに格納。
json.array! @category_children do |child|
json.id child.id
json.name child.name
end
孫カテゴリーのidとnameを取得し、@category_grandchildrenに格納。
json.array! @category_grandchildren do |grandchild|
json.id grandchild.id
json.name grandchild.name
end
3. 親カテゴリーの選択をきっかけに、紐づく子カテゴリーを表示
子カテゴリー、孫カテゴリー用に、selectボックスのoptionの入れ物を定義しておく。
function appendOption(category) {
let html =
`<option value="${category.id}" data-category="${category.id}">${category.name}</option>`;
return html;
}
:
子カテゴリーのselectボックスを定義。
:
//子カテゴリーのHTML
function appendChildrenBox(insertHTML) {
let childSelectHtml = '';
childSelectHtml =
`<select class="item_input__body__category__children--select" id="children_category">
<option value="" data-category="" >選択してください</option>
${insertHTML}</select>
<i class = "fa fa-chevron-down"></i>`;
$('#children_box').append(childSelectHtml);
}
:
同様に、孫カテゴリーのselectボックスも定義。
:
//孫カテゴリーのHTML
function appendGrandchildrenBox(insertHTML) {
let grandchildSelectHtml = '';
grandchildSelectHtml =
`<select class="item_input__body__category__grandchildren--select" id="grandchildren_category" name="item[category_id]">
<option value="" data-category="" >選択してください</option>
${insertHTML}</select>
<i class = "fa fa-chevron-down"></i>`;
$('#grandchildren_box').append(grandchildSelectHtml);
}
:
親カテゴリーを選択したタイミングで、Ajax通信。親カテゴリーを選択し直す度に、下位のselectボックスを初期値に戻す。
:
//親カテゴリー選択によるイベント
$(document).on("change","#parent_category", function() {
//選択された親カテゴリーの名前取得 → コントローラーに送る
let parentCategory = $("#parent_category").val();
if (parentCategory != "") {
$.ajax( {
type: 'GET',
url: 'get_category_children',
data: { parent_name: parentCategory },
dataType: 'json'
})
.done(function(children) {
//親カテゴリーが変更されたら、子/孫カテゴリー、サイズを削除し、初期値にする
$("#children_box").empty();
$("#grandchildren_box").empty();
$('.size_box').val('');
$('#size_box').css('display', 'none');
let insertHTML = '';
children.forEach(function(child) {
insertHTML += appendOption(child);
});
appendChildrenBox(insertHTML);
})
.fail(function() {
alert('error:子カテゴリーの取得に失敗');
})
}else{
$("#children_box").empty();
$("#grandchildren_box").empty();
$('.size_box').val('');
$('#size_box').css('display', 'none');
}
});
:
4. 子カテゴリーの選択をきっかけに、紐づく孫カテゴリーを表示
上記同様、子カテゴリーを選択したタイミングで、Ajax通信。子カテゴリーを選択し直す度に、下位のselectボックスを初期値に戻す。
:
//子カテゴリー選択によるイベント発火
$(document).on('change', '#children_box', function() {
//選択された子カテゴリーidを取得
let childId = $('#children_category option:selected').data('category');
//子カテゴリーが初期値でない場合
if (childId != ""){
$.ajax({
url: 'get_category_grandchildren',
type: 'GET',
data: { child_id: childId },
datatype: 'json'
})
.done(function(grandchildren) {
if (grandchildren.length != 0) {
$("#grandchildren_box").empty();
$('.size_box').val('');
$('#size_box').css('display', 'none');
let insertHTML = '';
grandchildren.forEach(function(grandchild) {
insertHTML += appendOption(grandchild);
});
appendGrandchildrenBox(insertHTML);
}
})
.fail(function() {
alert('error:孫カテゴリーの取得に失敗');
})
}else{
$("#grandchildren_box").empty();
$('.size_box').val('');
$('#size_box').css('display', 'none');
}
});
:
5. 孫カテゴリーの選択をきっかけに、サイズselectボックスを表示
サイズのselectボックスは、hamlで実装しており、cssで、隠している状態。孫カテゴリーを選択すると、display: block
で、表示させるような仕様。
# size_box{
display: none;
}
:
//孫カテゴリー選択によるイベント発火
$(document).on('change', '#grandchildren_box', function() {
let grandchildId = $('#grandchildren_category option:selected').data('category');
if (grandchildId != "") {
$('.size_box').val('');
$('#size_box').css('display', 'block');
} else {
$('.size_box').val('');
$('#size_box').css('display', 'none');
}
});
振り返り
商品出品ページの実装当初は、親カテゴリーはnameだけを取得して、配列を定義していた。new
、create
アクションのみではこれでも問題はない。
# 親カテゴリー
@category_parent_array = Category.where(ancestry: nil).pluck(:name)
しかし、上記のitemsコントローラーで分かる様に、itemsテーブルには、孫カテゴリーのidを保存している。なので、商品した出品の編集ページ(edit
アクション)では、孫カテゴリーのid情報から、紐づく子カテゴリー、親カテゴリーを取得し、表示させたい。つまり、親カテゴリーにはnameに加え、id情報も必要になった。
pluckメソッドにidを追加 もokだが、ancestryがnilのレコードを取得する方がシンプルという事で、結果、以下のようにしました。
:
def edit
#カテゴリーデータ取得
@grandchild_category = @item.category
@child_category = @grandchild_category.parent
@category_parent = @child_category.parent
#カテゴリー一覧を作成
@category = Category.find(params[:id])
# 紐づく孫カテゴリーの親(子カテゴリー)の一覧を配列で取得
@category_children = @item.category.parent.parent.children
# 紐づく孫カテゴリーの一覧を配列で取得
@category_grandchildren = @item.category.parent.children
end
:
private
# 親カテゴリー
def set_category
@category_parent_array = Category.where(ancestry: nil)
end
:
※ 参考までに、edit
アクションでは、
・ 各selectボックスを選択された状態にするため、各カテゴリーのデータ取得
・ 孫カテゴリーに紐づく子カテゴリー、親カテゴリー一覧取得
を行っています。