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あんまり覚えてないけど、復習がてら。
 
したらな❗️ 👋

画像複数投稿で必要だったメソッドのオプション、その他確認したこと

前回の続き

dependent: :destroy

アソシエーションの記述であるhas_manyやbelongs_toなどの後に書くオプション。
関連するレコードを同時に削除できる。

class User < ApplicationRecord
  has_many :items, dependent: :destroy
end
class Item < ApplicationRecord
  belongs_to :user
end

こういった構成になっていれば、例えばユーザーが退会した時にusersテーブルからそのユーザーの情報(レコード)が消えるとそれに関連しているレコード、ここでいうなら出品している商品の情報も同時に削除される、ということ。
ログアウトした時とかdestroyアクション動いてる気がするけど、多分それは関係ない...確かめたわけじゃないけど、deviseはsessionだのregistrationだの色々コントローラーがあるので、sessionのdestroyの時はdependentは動かないとかそんな感じだと思う。


allow_destroyオプション

accepts_nested_attributes_forを使用した際に記述したオプション。
nested_attributesなレコードを一緒に削除してくれる。allow_destroy: trueの形で使う。
今回で言うとItemモデルに対してImageモデルはnested_attributesなので、Itemモデルのレコードが削除された時に関連しているImageモデルのレコードも削除される。この時、対象に{_destroy: 1}のように渡される。
削除する際にストロングパラメーターにて以下の記述が必要。


images_attributes: [:name, :_destroy, :id]

コントローラーのストロングパラメータにて
permit(:name, ..., images_attributes: [:name, _:destroy, :id])のように記述。
モデル名s_attributes: [:同時に保存を許可するモデルのカラム名]みたいな感じ。
保存するだけなら[:_destroy, :id]はいらない。でも更新とか削除するなら必要。
editアクションなどのfields_forで:imagesを呼ぶと、form_withで呼び出された:itemsに関連している子モデルのレコードの分、fields_forは生成される。
submitして送信した際にターミナルで確認したのだが、それぞれ{id: xx}のような形であることが見れたので、fields_forから受け取ったオブジェクトにメソッドを使用することで、その情報が入っているフォームヘルパーが生成されているのではないかと思っている。(...書いててよく分からんくなってきた💦)

fields_for自体がオブジェクトの分だけ生成されるのが分からなくて、最初はコントローラーでインスタンスを用意してそれを使用していたので画像が5枚の商品でeachをかけて表示させた時に25枚になってしまったとかいい思い出。
後、何だか途中からfields_forのことっぽくなっちゃったのはご愛嬌😌


ストロングパラメーター

params.require(:item).permit(:name, :description, ...).merge(user_id: current_user.id)のように記述する。
Railsではセキュリティ対策として、ストロングパラメーターを使用することでテーブルの内容を保存したり変更をしたりする際に、記述したものだけを保存しそれ以外の情報は許可しないようにする仕組みがある。
form_withから送信されコントローラーが受ける値はハッシュ形式になっており、{}で囲われているのだが、params自体が{}の形式で受け取っているため、そのままでは{}の中に{}がある状態になってしまい、参照することができない。
その為にrequireを使用している。()の中にはモデル名をシンボル形式で指定している。
permitではその後に続く()の中に記述されたattributesの変更を許可している。
これまでの自分の認識ではpermitの中に記述するのは「カラム名」だったわけだけど、images_attributes:などのように複数のモデルを同時に保存するための記述によりimagesテーブルの:nameカラムもattributesの1つとして同時に保存するための許可をすることができる、と言うことを知ったのでここに記述しているのはattributesとして扱うものを記述するという認識でいる。(厳密には違うのかもしれないが誰かに答えを聞いたわけではないのであってるかどうかは知らない)
因みに実際に書いていたストロングパラメータは

params.require(:item).permit(:name, :price, :description, :category_id, :status, :condition, :size, :ship_price, :ship_area, :ship_day, :ship_method, :brand_id, item_images_attributes: [:image_url, :_destroy, :id]).merge(user_id: current_user.id)

モデル名に_を使用したため分かりにくくなったのもいい思い出。


persisted

ActiveRecord::Baseから継承されているインスタンスが使用できるメソッドで、レコード内容を更新する際に使用した。
@item.persisted?のように書くことで、インスタンスに代入されたレコード情報がデータベースに保存済みかそうでないかを確認、trueやfalseの真偽値を返す。 if文にそのまま上記の記述を突っ込むことで条件文を作成できる。

= f.fields_for :item_images do |image|
   (省略)
      = image.label :name do
        編集
        = image.file_field :name
      .buttons--delete 削除
  - if @item.persisted?
    = image.check_box :_destroy

ざっくりとした構成はこんな感じで削除ボタンを押した時に、(省略)あたりのdivタグを削除するようにJavaScript書いてた。
checkboxはimageオブジェクトから生成され、:_destroyを設定。チェックされてsubmitすることでRailsが該当する:idを検索しそのレコードを削除する仕組み。
気をつけたこととして、checkboxがこのdivタグの中にある場合、削除ボタンでcheckbox自体が同時に消失してしまうと画像を消せなくなってしまう(チェックしたことにならなくなる)。
その為、チェックボックスはdisplay: noneにしておき、削除ボタンが押された時に割り振っておいたチェックボックスのindexを取得し、そのindexが振られたチェックボックスを.propでcheckedするように記述。 それと同時に(省略)のプレビュー用のdivタグを削除することで添付した画像自体が削除されたように見える。
この時、新しく画像を挿入してもそちらとは受け渡し方が違うのかバッティングはしなかった。
この部分に関しては記事書いてる時点では思い出せない。多分ターミナルの情報は確認したと思うんだけど...


今回はここまで。
当時の振り返りを記事にしようって気付いたのが随分後だったので、今になってスクショとか撮っとけばよかったなとか思ってる😓
次はカテゴリ機能の事でも。担当してないけど見て分かる範囲のことを記憶が新しい内に残しておきたい。

したらな❗️ 👋

画像複数投稿機能で使ったメソッドなどについてのまとめ

今回TECH::CAMP最終課題で使った「一つの物品に対して複数の画像を添付して出品する」際に利用することになったメソッドについてざっくりとまとめておきます。
今よりも理解が深まる時が来た時に更新予定。

accepts_nested_attributes_for

Active::Recordから継承しているメソッド。モデルの親子関係(ネストされている)を作る。
これにより1つのformで複数のテーブルのレコードを作成することができる。
モデルクラスに記述。 (ここではItemが親モデルで、Imageが子モデル)

class Item < ApplicationRecord
  has_many :images
  accepts_nested_attributes_for :images
end
class Image < ApplicationRecord
  belongs_to :item
end

最初、自分はこの記述があればhas_manyのようなassociationの記述はいらないんじゃないかと思ってましたが、当たり前ですが至る所でしっかりエラーが出ました💦
親子関係であるかと多対多などのリレーションは別。
その記述がなければメソッドのように item.images のような形で取り出すこともできない。


fields_for

associationで紐づいているモデルを1つのformで登録、更新するために使用するメソッド。
今回のバージョンで使用したform_withなどでは、ブロックで囲った中の変数(フォームビルダーオブジェクトというらしい)を生成する。通常、これに対してヘルパーメソッドであるtext_fieldやnumber_fieldを使用するように書いている。
fields_forもそれと同様の書き方でオブジェクトを生成できる。 この時、同じフォーム内で関連づけられた別のモデルオブジェクトを編集できる、とのこと。 言葉だけだとよくわからんのだけど、form_withを少し書いている人なら実際の記述を見ると分かりやすいのかもしれない。

= form_with scope: :item, model: @item, local: true, multipart: true do |f|
  = render 'layouts/error_messages', model: f.object
  = f.fields_for :item_images do |image|
    = image.label :name do
      = image.file_field :name
  = f.text_field :name, maxlength: 40,  placeholder: "商品名(40文字まで)"
  = f.text_area :description, maxlength: 1000, placeholder: "商品の詳細を入力してください(1000文字以内)"
  = f.submit "出品する"

今回のアプリケーションではerb形式ではなくhamlを使用したためそのまま書いた。
正直色々見直す部分があったのかもしれない。(scopeとか本当はいらなかったような気が...)
生成されたビルダーオブジェクトを通じてメソッドを適用しているからなのか分からないが、idやnameに関しては特に指定してがなければ勝手にform側で生成してくれる。
モデルをネストさせる時に使うので、accepts_nested_attributes_forを使用する時はセットで使うようなイメージ。


validates_associated

1つのフォームで複数のモデルを投稿した時に、関連モデルの変更にバリデーションをかける際に必要なヘルパー...
なんですが、子であるImageモデル側にvalidates :name, presence: trueあればcreateの時は問題ないみたい。ただ、updateの際はこれがないと同モデル内で変更がない場合、バリデーションが働かない。

class Item < ApplicationRecord
  has_many :images
  accepts_nested_attributes_for :images

  validates_associated :images
  validates :images
end

この記述をした当初はnew, createアクションだったため記述理由は分からなかったが、実装途中で更新や編集の際に必要だということが分かった。
また、これまでの認識ではvalidates続くバリデーション対象はカラム名と思っていたのだが、今回のことで属性名(attributes)なのではないかという認識に至った。


 

今回はここまで。
次回はこれらを使用する際に必要だったオプションや他に知ったことについて書いていこうと思います。

したらな!✋

マージ後に起きた問題(カテゴリ登録編)

某スクールのチーム開発にてフリマアプリを作成中、それぞれのメンバーがサーバーサイドで実装したものを統合した後、確認と修正をしている際にいろいろな問題にあたったので、それを対処するまでのログとして記事を書いてます。
初学者なので詳しい人からするとおかしいこと言ってるかもしれない。
コメントで指摘、助言いただければ幸いです。


結論から言うと、form_withでscopeを使っていた時に自動で生成されるHTML要素のname属性が原因だったと思われる。とりあえず経緯。


  
既に開発のメインとしているdeveloperブランチには他の3人がそれぞれブランチを切った後に、ユーザー機能、カード登録機能、商品購入機能、カテゴリ機能、出品削除機能を実装しマージ。
自分はその後に商品出品機能を実装完了し、マージ。
他の機能やページ変遷を確認し都度修正しながら、商品出品機能のカテゴリを登録できるか確認した所で問題が発生。

f:id:saka20taku43:20200419205625p:plain

何故か同じ内容が2つparamsに送信されている。しかもROLLBACKされていて意味不明。
ついでに出品機能に対してカテゴリは外部キーとして設定しているハズなのに文字列を送っている。
  
  
...あ、だからROLLBACKしたのかな?
それか2つ同じ形式の値を送った事で無効化されたとか。
route.rbとかapplication.scssで同じ事書いたら全部無視されたっけ。

初学者である自分にしてみれば意味わからんことだらけでしたが、とりあえずカテゴリ関連の記述があるコードを見ていく事に。   

controllerのストロングパラメーターとフォームは統合した際に既に修正していたので問題なさそう。 というかそこが違ってたらUnpermittedとか出るよね、多分。
っていうか、違うattributes送信したら以下のような感じでエラーメッセージが出たのは最初に確認した。

カテゴリを入力してください
Categoryを入力をしてください

これ出たときは流石にパニックになった。
...まあ、それはどうでもいいので続きを書いてく。

modelは正直関係なさそうな気がしていた。
validates間違っていたら違うエラー出そう。そして今回のROLLBACKはそれ以前にマイグレーション時に設定していたnull: falseによるものだよねきっと。

正直初日(昨日)は他にも大量にエラーが出てたのでカラム名とか弄ってたら0時過ぎてた(他の確認も並行していて10時間以上経過していた)ので一段落つけました。
分からないながらに進行していたので無意味に時間が過ぎていたような感じ。
  
で今日は朝早くに起きれなかったのでAM11時から続行(-A-;) ネスゴシタ

親カテゴリーのみで送信したり、子カテゴリーのみで送信したり孫カテゴリーまで送信したり。 その時のターミナルのエラ〜ログの違いを見て、以下のことが分かりました。

  • 何も入力されていなければ category_id=>"---" になり、item=>{...} の外側に2つ目は送信されていない

  • 親カテゴリだけなら category_id=>"親"になり item=>{}の外側に category=>"---"が生成される

  • 子カテゴリまで入力すると category_id=>"親"は変わらず item=>{}の外側に category=>"---"が生成される

  • 孫カテゴリまで入力すると category_id=>"親"は変わらず item=>{}の外側には category=>"孫"が生成される

  
親は常に送信されるが外側のカテゴリ送信については、仕様としてセレクトボックスが選択されるたびに自動で生成される中で、子から孫が生成された時に常に更新されているのが見て取れた。
つまり親はhaml上のf.selectで読み込まれる状態が続いていて、jQueryで生成された子孫は最新の物だけが読み込まれていた、と言うことになるんだよね多分。

で、そういえば送信された値が名前なのが何でなのかよく分からなかったのですが、chromeで検証している時にformで送信されるのがoptionタグのvalueだということに漸く気がついた。
そこからHTML要素を生成しているjQueryを見ることに。

jQueryは非同期でセレクトボックスを生成しているだけだと思っていたのでよく見ていなかったが、見た時に一番最初に目に入ったのが、jQueryを通して生成されるテンプレートリテラルで囲まれたHTMLの要素。

ここで生成されている要素に <option value="${category.name}" ... > とあったのでこれを .id に直した所、送信する値が名前ではなく数値に変えることができた。
...少なくとも子と孫は。

後は生成された物を検証で見ていた時にname属性の名前が親と子孫で違うのに気がついた。
そこで、子孫の name="category_id"を、親の name="item[category_id]"に変更してみた所、無事paramsの値が最新の要素である孫のみを送信できる状態にすることができ、何とか事無きを得た。   
  
  
因みにこの後、アソシエーションの関係で画像投稿に関するエラーに見舞われるのは別の話😅   

反省点

scopeを使っていることを情報として出していなかったこと、他の人がどういうコードを書いているかがよく分かっていないこと、でかなり苦しんだ土日となりました。 後はまだまだ、Railsの動きに付いていけてないのがよく分かりました。
今回は実装画面のエラーメッセージをユーザビリティを考えて日本語化していましたが、ターミナルのエラーログとか確認していると何でもかんでも翻訳すればいいもんじゃないなって思えた。

ただ、時間は掛かっても自分で解決できていると言うことにすごく自信がついた週末になりました。 今後も仮説と検証をいくつも持って常に冷静に対処できれば良いなと思いました。  
したらな!✋