A note of a person who is learning programming, SakaTaQ

ロック好きのプログラミング学習

カテゴリ機能で使用したpluck、ancestryについてまとめ

.pluck メソッド

pluckメソッドとは、1つのモデルで使用されているテーブルからカラム (1つでも複数でも可) を取得するクエリを送信するのに使用できる。
引数としてカラム名のリストを与えると、指定したカラムの値の配列を、対応するデータ型で返します。

(公式ドキュメントより引用)

相変わらずですが、コードと合わせないとよく分からん。 実際に使用したコードを記載。
@category_parent_array = Category.where(ancestry: nil).pluck(:name)
最初に@category = Category.pluck(:name)のような基本的な書き方をみた時にwhereいるのかな?ってなり、引数に(:name, ancestry: nil)とか試してみたけどダメでした。

f:id:saka20taku43:20200429130057p:plain

まぁ、引数にいろんな書き方混ぜたらダメだよねって感じですが、どのみち単体では使えない。
で、pluckの後に.limit(5)みたいなクエリメソッドはメソッドチェーン出来ないけど、前なら出来るとか書いてあったので前にwhereつけたら行けてしまったのであんまり深掘ってなかったんですよね💦
SQL的なことを考えると最初にwhereでancestryがnilのものだけ取り出し.pluckで配列にするって考えればなんとなく納得。クエリを直接トリガするので後には付けれないってあったのでSQLのこと少し思い出した次第で。
.distinctで重複をなくしたり、.joinsで関連するテーブルの取り出ししたり、sqlクエリみたいな名前のメソッドと併用すると便利に取り出せるみたい。

最初はmapメソッドで書いていたみたいなのですが、途中レビューで修正があったのでこの時pluckを知りました。
使い分けとしてはテーブルの特定のカラムなどの取り出しはpluckで、まとめて使いたい時はmapで。理由はSQLクエリの処理にかかる時間、とのこと。

今回使わなかったんで書いてなかったけど、引数に(:id, :name)のように複数渡すと[[1, "レディース"], [2, "メンズ"], ...]みたいな2次元配列とかいうやつになるらしい。2次元って言葉は画像やゲーム以外で聞くと途端に難しい言葉に感じるよね。


gem 'ancestry'

ActiveRecordのモデルの「レコード」をツリー(階層構造)に編成できるようにするgemらしいです。
意味はなんとなく分かるんですが、DB設計時に用意された「ancestryカラム」がどういった内容で保存されて階層として認識するように識別するのか実装前は全く謎でした。
gem入れる時はGemfile(最下)に

gem 'ancestry'

記入後にターミナルで

% bundle install

と入力し、実行。

既に用意してあるテーブルにancestryカラムを追加する場合はマイグレーションファイルを発行する。ターミナルにコマンドを入力して、実行

% rails g migration add_ancestry_to_[テーブル名] ancestry:string:index

インデックスは好みだとは思いますが、カテゴリ検索とかで使うので早い方がいいです、多分。
で、発行されたマイグレーションファイルに特に不備がなければ

% rails db:migrate

を実行しマイグレートしておく。
テーブル名と同名のモデルクラスのファイル [モデル名.rb]のアソシエーション部分に

class [model] <ApplicationRecord
  has_ancestry
end

実際にカテゴリ登録する際に表示されるレコード情報については主にdb/seeds.rbを使って用意していく。gem 'Faker'については別で記事にする予定。
因みに公式は大分センスあるダミーが用意されてる。
出品機能では、ブランドテーブルのレコードを用意するくらいでしか使ってなかったのであんまり使い方はわからなかったけど、メンバーの書いたコードのおかげである程度は自分でも書くことができました。

Faker::Config.locale = :ja

lady = Category.create(name: "レディース")
lady_tops = lady.children.create(name: "トップス")
lady_pants = lady.children.create(name: "パンツ")
lady_tops.children.create([{name: "Tシャツ/カットソー(半袖/袖なし)"}, {name: "Tシャツ/カットソー(七分/長袖)"}, {name: "シャツ/ブラウス(半袖/袖なし)"},{name: "シャツ/ブラウス(七分/長袖)"},{name: "ポロシャツ"}])
lady_pants.children.create([{name: "デニム/ジーンズ"}, {name: "ショートパンツ"}, {name: "カジュアルパンツ"},{name: "ハーフパンツ"}])

mens = Category.create(name: "メンズ")
mens_tops = mens.children.create(name: "トップス")
mens_pants = mens.children.create(name: "パンツ")
mens_tops.children.create([{name: "Tシャツ/カットソー(半袖/袖なし)"}, {name: "Tシャツ/カットソー(七分/長袖)"}, {name: "シャツ(半袖/袖なし)"}, {name: "シャツ(七分/長袖)"}, {name: "ポロシャツ"}])
mens_pants.children.create([{name: "デニム/ジーンズ"}, {name: "ショートパンツ"}, {name: "カジュアルパンツ"}, {name: "ハーフパンツ"}])

かなりの量なのでとりあえず一部のみ。用意したらターミナルで

% rake db:seeds

入力して実行し、テーブルにレコードを投入。
ancestryカラムでは 親カテゴリのレコードにはnilが入り、子カテゴリには親のレコードid、孫カテゴリには祖先/親のレコードidが1/3のような形で入るようになっている。
これがわからない状態では出品した時はどういった状態で保存されるのか分からなかったので非常に困ったわけですが、itemsテーブルでは外部キーカラムとして保存しているので、他のリレーションしているカラムと同じだった。呼び出す時とてれこになってたわけです💦ハズカシ

ancestryが導入されることでいくつか使用できるインスタンスメソッドがあるようですね。
今回の実装では.children、.parentを主に使用しました。 .parentは親カテゴリの取得に使う。トップスでいうとレディースやメンズが相当。祖先を直接指すのは...rootかな?実装当初は触れてなかったこともあり自身で調べてなかったですね😅
今回は使わずに.parent.parentにようにチェーンを使用していました。
.childrenは子カテゴリの取得に使用する。レディースに対してトップスやパンツが相当。孫取得は.indirectsがあるみたいですが、複数あるのでどう返ってくるかで使いやすさが変わりそう。複数ある時点でどうせ配列だけど
他にもいくつかのメソッドが用意されているようです。
参考にさせていただいた中で、一番見やすかった記事のリンクを貼っておきます。
【翻訳】Gem Ancestry公式ドキュメント
 

ビューでは先のpluckメソッドの記述でセレクトボックスを用意してある。
選択された際に、このセレクトボックスのvalueJavaScriptajax通信で指定したURLに送信。

$.ajax({
  url: 'get_category_children',
  type: 'GET',
  data: { parent_name: parentCategory },
  dataType: 'json'
})

送られているデータはparent_name: parentCategoryのキーバリュー方式。
ルーティングにはcollection、member両方で

get 'get_category_children', defaults: { format: 'json' }
get 'get_category_grandchildren', defaults: { format: 'json' }

とある(僕が書いたわけではないので「ある」としてます)。次は用意してある指定先のアクション。[:parent_name]の部分でさっきのvalue(親カテゴリの名前)を受け取り、Categoryテーブルからその子レコード(.children)を取得(find_by)。

def get_category_children 
  @category_children = Category.find_by(name: "#{params[:parent_name]}", ancestry: nil).children
end

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

json形式での送信は以下のjbuilder。複数だからなのか配列で返る。

json.array! @category_children do |child|
  json.id child.id
  json.name child.name
end

通信が成功したら配列は引数childrenに入っている(例の如く未確認だけどそうとしか思えん)。

.done(function(children){
  $('#children_wrapper').remove(); //親が変更された時、子以下を削除
  $('#grandchildren_wrapper').remove();
  var insertHTML = '';
  children.forEach(function(child){
    insertHTML += appendOption(child);
  });
  appendChidrenBox(insertHTML);
})

forEach文で生成されているoption要素。

function appendOption(category){
  var html = `<option value="${category.id}" data-category="${category.id}">${category.name}</option>`;
  return html;
}

それを引数として以下の関数を呼び出し

function appendChidrenBox(insertHTML){
  var childSelectHtml = '';
  childSelectHtml = `<select id="child_category" name="item[category_id]">
                       <option value="---" data-category="---">---</option>
                         ${insertHTML}
                     <select>`
  $('.listing__category').append(childSelectHtml);
}

form_with内の親カテゴリのセレクトボックスの下に同じようなボックスをぶっこむ。
孫の要素も同じような流れで生成している。

参考にさせていただいたと思われる記事
ancestryによる多階層構造データを用いて、動的カテゴリーセレクトボックスを実現する~Ajax~

ajax通信苦手ですね。何か知らんけど出来てるみたいなのが多いので、表現が合ってるかとか認識のすり合わせができるといいんだけど...。
あ、responseやstatusで接続の成否とか取り出せるとかあったかな。ウロオボェ  
 
セレクトボックスの話があったので、次はenumやらactive_hash使った時のこと書いとこう。
正直enumあんまり覚えてないけど、復習がてら。
 
したらな❗️ 👋