前提
Ruby on Railsを使い始めて、1年半ほど経ちました。
今まではモデル数が5つ以下のシンプルなアプリしか触ってこなかったのですが、最近10以上のモデル数のアプリを触ることが多くなり、手詰まり感を感じ始めてきました。
今回取り上げるテーマ
すぐ書ける? 条件に沿ったデータ抽出の書き方
知っている人はこのメソッドで取得すれば一発!なのですが知らないことで、データの抽出だけに多大な時間をかけてしまい、、その上結局ガリガリとSQLを書くという可読性の低いコードができてしまい、、なんともRailsの良いとこを全く生かしていない書き方になってしまっていました。
その状況を脱するために最近学んだことをまとめておくことにしました。
とりあえずcurrent_user
使えばいいでしょ。今までの知識レベル
データ抽出を書く場面に出くわした場合、チームの他の人のコードを疑いもなく真似し、例えばログイン中のユーザーのコメント一覧を取得するというお題の場合、特に意識することもなく
- current_user.comments という書き方
- Comment.where(user_id: current_user.id) という書き方
という感じでとりあえず取りたいデータが取れればいーじゃんということにしか意識が向いていませんでした。
参照方法によってパフォーマンスに影響を与えるなんて考えもせず、とりあえず取りたいデータが取れればいーじゃんということにしか意識が向いていませんでした。
仮にこの発展形で、コメントに子となるテーブルが存在してるようなレコードを抽出してそれを1つづつ表示する・・・というケースの場合に、書き方によってはN+1 問題につながるというか、そもそもN+1も言葉しか知らないレベルでした
そんなレベルだと前述の通り、複数テーブルのデータをいい感じに表示したい状況になった場合、うーんと頭を抱えてしまい数日が過ぎていたのです。。あいたた〜|д・)
最近学んだこと
ということで、最近学んだ抽出方法を例を使いながら挙げていきます。
サンプルアプリはこちら
モデル一覧
- 都道府県一覧:Prefectureモデル
- 市区一覧:Cityモデル
- 町村一覧:Townモデル
アソシエーション(関連)
class Prefecture < ActiveRecord::Base
has_many :cities
end
class City < ActiveRecord::Base
belongs_to :prefecture
has_many :towns
end
class Town < ActiveRecord::Base
belongs_to :city
end
1. 親子関連を定義しているデータの抽出
親から子のデータを取得する場合
Q. 東京都(id=13)に属するCity一覧を取りたい
Prefecture.find(13).cities
Q. 千代田区(City.first, id=13101)に属するTown一覧を取りたい
City.first.towns
Q. 東京都(id=13)に属するTown一覧を取りたい場合
city_ids = City.where(prefecture_id: 13).pluck(:id)
Town.where(city_id: city_ids)
考え方: まず東京都(id=13)に属するCity一覧を取得し、次に各Cityに紐づくTownを取得する。
ActiveRecordのメソッドpluckを使うと必要なカラムのデータのみ取得し、配列にしてくれる。
もちろん、 city_ids
変数に入れずに、ワンライナーで書いてもOK!
また、上記の例で既出だがwhereのカラムの条件値に配列を指定することもできる〜。
純粋なSQLではINを使っていたので、これは便利!
Town.where(city_id: [13101, 13102, 13103])
子から親のデータを取得する場合
子が一意に決まっているので、簡単に取れる
Q. あるTown(1番目のTown)に紐づく、CityとPrefectureをとる場合
Town.first.city
Town.first.city.prefecture
2. 件数を取得する
Q. 東京都(id=13)に属するCityの件数
City.where(prefecture_id: 13).count
countでなくsizeメソッドを使っても同じ結果になる。
Q. 東京都に属するTownの件数
合計の件数
Town.where(city_id: City.where(prefecture_id: 13)).count
city_ids = City.where(prefecture_id: 13)
と変数に入れて、↑の city_id: city_ids
としてもOK。
個別の件数
A区: xx件
B区: xx件 みたいに表示したい
- スタンダードな方法
cities = City.where(prefecture_id: 13)
cities.each { |city| puts "#{city.name} : #{city.towns.count}件" }
- group句を使った方法
city_ids = City.where(prefecture_id: 13).pluck(:id)
Town.where(city_id: city_ids).group(:city_id).count
結果
{[city_id] => [キーのcityのtown数], ...}
のハッシュの形で返ってきてしまった。。
結果を加工することが必要!
ここでRubyのメソッドを使ってみよう。Hashクラスのeachメソッドを使う
town_counts = Town.where(city_id: city_ids).group(:city_id).count
town_counts.each { |city_id, town_count| puts "#{City.find(city_id).name} : #{town_count}件" }
これで希望通りの表示ができた!
3. ActiveRecordのメソッドだけで無理に完結しなくていいんだよ(Rubyを使おう!)
欲しいデータを取得するためには、ActiveRecordのメソッドだけを使って抽出するだけでなく、Rubyのメソッドを使って抽出した結果を加工するという技もあるのです!
(↑の最後の例がその一つ)
Railsビギナーな私は、ついついActiveRecordのメソッドを上手に組み合わせて抽出したい!!という頭しかなかったため、時間をかけて気づけば長々としたSQL文を書いてしまい、後で見たときにこれ何してるの?と自分のコードでもなることがしばしばあったり。。
というのも今までRubyを差し置いて、Ruby on Railsの学習しかしてこなかったので、Rubyの文法やメソッドは正直知らない。。
なんともったいない!Rubyが使えるようになると、ぐっと世界が広がってきた気がします。
では使ってみましょうヽ( ´∇`)ノ
今までのお題をちょっと応用して
Q. 東京都に属するCityの中で、23区のCityと23区以外のCity一覧を取得する
- ActiveRecordのメソッドを使ったスタンダードな方法
23区のCity一覧
どれが23区のCityか分からないので、文字列検索を使って抽出してみる
tokyo_metropolitan_cities = City.where("prefecture_id = ? AND name LIKE '%区'", 13)
ちなみに文字列のみでの検索の場合、
直接値を記述せずに疑問符?を使う方がセキュリティ上良いらしいです。知らなかった〜。参考リンク
23区以外のCity一覧
other_cities = City.where("prefecture_id = ? AND name NOT LIKE '%区'", 13)
結果は無事取れました!が、同じようなSQLを2回発行した感じですね〜。
- Rubyのメソッドを使った方法
tokyo_metropolitan_cities, other_cities = City.where(prefecture_id: 13).partition { |city| city[:name].match(/.+区/) }
ワンライナーで取れました!
tokyo_metropolitan_cities
には23区のCity一覧が、other_cities
にはそれ以外の東京都のCity一覧が入っています。
条件によって結果を二つの配列に格納するEnumerableクラスのpartition、正規表現を使った文字列の検索を行うmatchメソッドを使いました!
詳しい使い方はリンク先のページを参照してください。
特にpartitionメソッド、かなり使えそうですね〜。
まとめ(なぜこれらを知っておくことが重要なのか)
- データの参照はController、Modeと様々な箇所で必要になる。毎日のように書く。なのできちんとした方法を覚えておくべき
- 参照の記述を適当にしているとアプリ全体のパフォーマンスに関わり、バグの要因になる(と思う)
- 書き方を広く知っておくことで自分の開発効率が格段にアップする
まだ学習途中ですが、これからも様々なコードを見て、自分の使える知識を増やしていきたいと思います。
参照周りでも、scopeなど始め使いこなせるようになるべき知識が数多!
そして、じわりじわりと必要性を感じているRubyも学んでいこうと思います╭( ・ㅂ・)و