【Rails】面倒なpluckを使いこなす方法

バックエンド

はじめに

railsのpluckメソッドはActiveRecordを生成せず、指定したカラムの値が格納された配列を生成するので、パフォーマンスを改善したい時に重宝します。

ただ、配列で値が返ってくるが故に、その後の扱いが抜群に面倒です。
今回は、pluckメソッドで省エネに値を取得しつつ、開発体験を損なわない方法を紹介します。

実際にこの方法でAPIの応答時間を1/5に抑えられたこともありますので、
是非参考にしてみてください!
少し詳しめに書いてますんで、やり方だけ知りたい方は最後のまとめだけ見てってください。

pluckの使い方・問題点

pluckメソッドの使い方

pluckメソッドは以下のように使用することができます。

User.all.pluck(:id)
# => [1, 2]

また、この時に発行されるSQLクエリは以下のような感じで、必要なカラムだけ取得するので、DBにも優しい(らしい)です。

SELECT `users`.`id` FROM `users`

そのため、ActiveRecordを用いてデータを取得するよりも、パフォーマンスに優れています。
実際にベンチマークを測定してみると違いがよくわかります。
( ちなみにusersテーブルは約1800行 30列のデータを入れています。)

Benchmark.bm 10 do |r|
  r.report "pluck" do  
    User.all.pluck(:id)    
  end

  r.report "not pluck" do  
    User.all.to_a    
  end  
end

                 user     system      total        real
pluck        0.000362   0.001236   0.001598 (  0.002610)
not pluck    0.022077   0.001174   0.023251 (  0.028349)

pluckメソッドの問題点

手軽にパフォーマンスを上げれるpluckメソッドですが、値の使い勝手が悪いという問題点があります。
先述の例のように1つのカラムの値が欲しいだけなら無問題ですが、複数のカラムを取得した場合

User.pluck(:id, :name)
# => [[1, "太郎"], [2, "花子"]]

という風な配列が返ってくるため、大変使いづらいです。
1人目のnameだけ取り出したい時はusers[0][0]と記述しなければならず、書く人にも読む人にも優しくないコードになります。

本題

解決策

ハッシュを生成しましょう。
意味が伝わらないのは、数字を使ってるからなので、文字列で値を指定できるハッシュを使いましょう。

users = User.pluck(:id, :name).map do |values|
  {
    id: values[0],
    name: values[1]
  }
end

puts users
# => [
  {
    id: 1,
    name: "太郎"
  },
  {
    id: 2,
    name: "花子"
  }
]

puts users.first[:name]
# => 太郎

これで値の取り回しが良くなりました。

ただし、問題点があります。欲しいカラムが増えた際に、pluckメソッドの引数とmapメソッドのブロック内の2ヶ所に変更を加える必要があります。
また、一時的に配列の添字を使っているので、何列目が何のカラムだったっけ?ってなりがちです。

そこで、Array#transposeメソッドを使って改良してみましょう。

columns = [:id, :name]
users = User.pluck(*columns).map do |values|
  [columns, [*values]].transpose.to_h
end

これでカラム名の指定箇所を一ヶ所に集約できました。
ハッシュに変換する処理もtransposeメソッドで完結になっただけでなく、欲しいカラムが増えてもcolumnsの値を変更するだけでOKです。

[*values]columnsに指定したカラムが1つだけだった場合に対応するための記述です。
カラムが複数個になる場合は[columns, values]と同義です。

transposeとto_hについて

transposeメソッドが何やってるのか少しだけ解説します。( Array#transpose )
transposeは転置行列を生成するメソッドで、具体的には以下のような動作をします。

[[1, 2, 3],
 [4, 5, 6],
 [7, 8, 9] ].transpose
# => [
 [1, 4, 7], 
 [2, 5, 8], 
 [3, 6, 9]]

# もう少しわかりやすくしてみる

[["■", "■", "■"],
 ["◯", "◯", "◯"],
 ["★", "★", "★"]].transpose
# => [
 ["■", "◯", "★"],
 ["■", "◯", "★"],
 ["■", "◯", "★"]]
# (単純な反時計回りじゃないです。上の例で1, 5, 9の場所が変わってないことに注目してください。)

こんな感じで行と列を入れ替えた行列を転置行列と言います。
pluckの例の時、どのような動作をしていたのかというと

[columns, [*values]].transpose.to_h
# columns => [:id, :name]
# [*values] => [1, "太郎"]
# [columns, [*values]] => [[:id, :name], [1, "太郎"]]
# [columns, [*values]].transpose => [[:id, 1], [:name, "太郎"]]

こんな感じで[カラム名, 値]という配列がカラムの数だけ格納された配列が生成されてます。
to_hは、[カラム名, 値]な配列から{ カラム名 => 値 }というハッシュを生成するメソッドです。

こんな感じでpluckによって生成された値の配列からハッシュを生成してました。

まとめ

columns = [:id, :name]
users = User.pluck(*columns).map do |values|
  [columns, [*values]].transpose.to_h
end

ちなみに、User.column_namesを使うと全カラムが取得でき楽になります。
レスポンスに含めるべきでないカラムも除外できる上、DBにカラム追加しても変更点なし!

columns = User.column_names.map(&:to_sym) - [:password_digest, :secret_data]

pluckメソッドでパフォーマンスを上げつつ、transposeto_hを駆使してハッピーになりましょう!

タイトルとURLをコピーしました