LoginSignup
31
13

More than 3 years have passed since last update.

[Rails]カラムを絞るだけの目的でselectメソッドは使うな!

Last updated at Posted at 2020-08-18

ActiveRecordのselectメソッドについて

ActiveRecordでデータを取得すると基本的に該当するテーブルのすべての項目が取得されます。
発行するSQLを見ていただくと分かる通り、*で全項目取得しています。
全項目取得しているので、以降の処理ではどの項目でも参照することができます。

pry(main)> user = User.first
  User Load (0.7ms)  SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
=> #<User id: 1, name: "ham", created_at: "2020-03-10 01:03:37", updated_at: "2020-06-16 02:18:39">
pry(main)> user.id
=> 1
pry(main)> user.name
=> "ham"

ただ、全カラム使うわけではないので必要なカラムだけ取得したほうがいいのでは?という考え方もあると思います。
そんなときにselectというメソッドを使うことで取得するカラムを絞ることができます。
selectについて詳しくはRailsガイド をご覧ください。

selectを指定することで必要なカラムだけ取得することができます。
取得していないカラムは当然以降の処理では参照できません。

pry(main)> user = User.select(:id, :created_at).first
  User Load (0.7ms)  SELECT `users`.`id`, `users`.`created_at` FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
=> #<User id: 1, created_at: "2020-03-10 01:03:37">
pry(main)> user.id
=> 1
pry(main)> user.name
ActiveModel::MissingAttributeError: missing attribute: name
from /usr/local/bundle/gems/activemodel-6.0.3.2/lib/active_model/attribute.rb:221:in `value'

カラムを絞るだけのselectは使うな!

あくまで私個人の考えではあるのですが、チーム開発など複数人で開発を行っている場合はカラムを絞るだけのselectは使わないほうがいいと考えています。

なぜなのか?

下記のコードを見てください。

def hoge(user_id)
  # selectでid, nameだけ取得
  user = User.select(:id, :name).find(user_id)

  ...(様々な処理)

  generate_response(user)
end

private

def generate_response(user)
  { id: user.id, name: user.name }
end

後にhogeメソッドのレスポンスにemailを追加することになったらどうでしょうか?
Userモデルはemailカラムを持っていることとします。

おそらく該当箇所を見つけて、generate_responseにemail足せばいいだけだな!
と思い、下記のように修正すると思います。

def generate_response(user)
-  { id: current_user.id, name: current_user.name }
+  { id: current_user.id, name: current_user.name, email: current_user.email }
end

よし修正終わり!1行で出来たぜ!!テスト実行!!!

pry(main)> { id: user.id, name: user.name, email: user.email }
ActiveModel::MissingAttributeError: missing attribute: email
from /usr/local/bundle/gems/activemodel-6.0.3.2/lib/active_model/attribute.rb:221:in `value'

あれ??動かないぞ・・・
受け取っているuserがおかしいのか?
たどってたどって、、、

そうです。selectで取得カラムを絞っているのでそちらにemailを足す必要があります。
下記も修正すれば動くようになります。

def hoge(user_id)
  # selectでid, nameだけ取得
-  user = User.select(:id, :name).find(user_id)
+  user = User.select(:id, :name, :email).find(user_id)

テストもうまくい通りました!

pry(main)> { id: user.id, name: user.name, email: user.email }
=> {:id=>1, :name=>"hoge", :email=>"hoge@example.com"}

どう思いますか?

RailsのActiveRecordを使うと基本全カラムを取得すると思うので上記のように一度はハマる人が多いと思います。

1回1回の手間はそこまでではないかもしれませんが、継続的に開発されていくシステムであれば毎回同じ事が起きます。
これは結構なコストです。また最悪の場合、気づかずにバグを生む可能性もあります。

今回のselectは開発コストやバグのリスクを上げてまで実装する必要があるのでしょうか?
私は多少最適ではなかったとしても他の人が勘違いしづらいコードのほうが良いと思っています。
これが私がカラムを絞るだけで使うselectは使わないほうが良いと思っている理由です。

selectの使いどころ

selectの存在を全否定している記事になってしまったのですが、もちろん使いどころもあります。
下記のように集計関数を使ったときです。

users_group_by_name = User.select('name, count(*) AS cnt').group(:name)
users_group_by_name.each do |u|
  p u.name
  # u.cntでカウントが取得できる
  p u.cnt
end

ただ、この場合も変数名をusersなどにしてしまうと勘違いさせてしまう可能性が高いので、それとわかる変数名にしたほうが良いでしょう。

あと、たまにjoinした先のテーブルをselectを使って直接アクセスできるようにしていることがありますが、これもとても分かりづらいのでやめたほうが良いと思います。

review = Review.select('reviews.id, users.name').joins(:user).find_by(id: 1)
# これでuser.nameにアクセスできる
review.name

普通にアソシエーション経由でアクセスするかdelegateを実装しておきましょう。

app/models/review.rb
review = Review.find_by(id: 1)
# アソシエーション経由でアクセス
review.user.name
# もしくはReviewモデルにdelegateを定義しておく (delegate :name, to: :user, prefix: true)
review.user_name

まとめ

この記事ではselectに焦点を当てましたが、複数人が同じコードを触るチーム開発では他人が理解しやすい(勘違いしづらい)コードを書くことが重要だと思います。
読みやすい(勘違いしづらい)コードを書くことで開発スピードが上がり、バグも減ると思います。

31
13
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
13