Railsクエリはコンソールでは機能しますが、コントローラーでは機能しません

概要

名前フィールドを持つテーブル Doctors を持つアプリケーションがあります。ユーザーは、そこに任意のバージョンの名前を入力できます。インデックスビューにレコードを表示する前に、姓(文字列の最後の単語)に基づいてレコードを並べ替えようとしています。

コンソールでは機能するが、コントローラーで実行するとエラーが発生するクエリを思いつきました。

クエリは次のとおりです。

current_user.doctors.select("doctors.*, split_part(doctors.name, ' ', -1) AS lastname").order('lastname ASC')

私のテーブルの移行:

class CreateDoctors < ActiveRecord::Migration[7.1]
  def change
    create_table :doctors do |t|
      t.string :name
      t.string :field
      t.string :address
      t.string :phone
      t.string :fax
      t.string :website
      t.string :portal
      t.binary :active, default: true
      t.string :group

      t.timestamps
    end
  end
end

私のコントローラーコード:

def index
  @doctors = 
       current_user.doctors.select("doctors.*, split_part(doctors.name, ' ', -1) AS lastname").order('lastname ASC')
end

そして私のビューコード:

<%= turbo_frame_tag Doctor.new %>
<%= turbo_frame_tag "doctors" do %>
  <% @doctors.each do |doctor| %>
    <%= render 'doctor_index', doctor: doctor %>
  <% end %>
<% end %>

得られるエラーは次のとおりです。

17:16:53 web.1  | Started GET "/doctors" for ::1 at 2024-03-16 17:16:53 -0600
17:16:53 web.1  |   ActiveRecord::SchemaMigration Load (1.0ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
17:16:53 web.1  | Processing by DoctorsController#index as HTML
17:16:53 web.1  |   User Load (1.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2  [["id", 2], ["LIMIT", 1]]
17:16:53 web.1  |   Rendering layout layouts/application.html.erb
17:16:53 web.1  |   Rendering doctors/index.html.erb within layouts/application
17:16:53 web.1  |   Rendered shared/_nav-tabs.erb (Duration: 1.2ms | Allocations: 482)
17:16:53 web.1  |   Doctor Count (21.8ms)  SELECT COUNT(doctors.*, split_part(doctors.name, ' ', -1) AS lastname) FROM "doctors" WHERE "doctors"."user_id" = $1  [["user_id", 2]]
17:16:53 web.1  |   ↳ app/views/doctors/index.html.erb:12
17:16:53 web.1  |   Rendered doctors/index.html.erb within layouts/application (Duration: 83.8ms | Allocations: 17358)
17:16:53 web.1  |   Rendered layout layouts/application.html.erb (Duration: 84.2ms | Allocations: 17477)
17:16:53 web.1  | Completed 500 Internal Server Error in 254ms (ActiveRecord: 42.9ms | Allocations: 54020)
17:16:53 web.1  |
17:16:53 web.1  |
17:16:53 web.1  |
17:16:53 web.1  | ActionView::Template::Error (PG::SyntaxError: ERROR:  syntax error at or near "AS"
17:16:53 web.1  | LINE 1: ...OUNT(doctors.*, split_part(doctors.name, ' ', -1) AS lastnam...
17:16:53 web.1  |                                                              ^
17:16:53 web.1  | ):
17:16:53 web.1  |      9:   </div>
17:16:53 web.1  |     10:   <%= turbo_frame_tag Doctor.new %>
17:16:53 web.1  |     11:   <%= turbo_frame_tag "doctors" do %>
17:16:53 web.1  |     12:     <% if @doctors.count != 0 %>
17:16:53 web.1  |     13:       <% @doctors.each do |doctor| %>
17:16:53 web.1  |     14:         <%= render 'doctor_index', doctor: doctor %>
17:16:53 web.1  |     15:       <% end %>
17:16:53 web.1  |

これをコンソールで実行すると、探している結果が得られます。これは、姓順に並べられた医師のレコードのリストです。

これは Rails クエリから psql クエリへの変換の問題であると確信していますが、希望どおりに動作するように書き直す方法がわかりません。なぜコンソールでは機能するのにコントローラーでは機能しないのか、私は混乱しています。

解決策

声明は次のとおりです。

SELECT COUNT(doctors.*, split_part(doctors.name, ' ', -1) AS lastname)

そのカウントはどこから来たのでしょうか?エラーメッセージを見てみましょう。

17:16:53 web.1  |      9:   </div>
17:16:53 web.1  |     10:   <%= turbo_frame_tag Doctor.new %>
17:16:53 web.1  |     11:   <%= turbo_frame_tag "doctors" do %>
17:16:53 web.1  |     12:     <% if @doctors.count != 0 %>
17:16:53 web.1  |     13:       <% @doctors.each do |doctor| %>
17:16:53 web.1  |     14:         <%= render 'doctor_index', doctor: doctor %>
17:16:53 web.1  |     15:       <% end %>

<% if @doctors.count != 0 %> からのものです。 select を count とともに使用すると、選択された列がカウントされます。

おそらくお気づきかと思いますが、@doctors.count をチェックすることに意味はありません。 @doctors が空の場合、@doctors.each は何も行いません。追加のカウント クエリは追加のクエリであり、競合状態も引き起こしますが、この特定のケースではおそらく重要ではありません。

おそらく行をローカルで削除しましたが、保存または展開しませんでした。

姓で並べ替えたいだけの場合は、order を直接使用して、選択した列をそのままにしておくことができます。

current_user.doctors.order("split_part(doctors.name, ' ', -1) asc")

これを頻繁に実行し、医師が多数いる場合は、インデックス付きクエリを実行するとよいでしょう。その特定の式にインデックスを追加できます…

create index doctor_lastname_idx on doctors(split_part(name, ' ', -1))

または、生成された列として追加することもできます。医師テーブルに姓列を追加し、before_save コールバックでこれを行うことができます…

class Doctor < ApplicationRecord
  ...

  before_save :set_lastname

  private def set_lastname
    self[:lastname] = name.rpartition(/\s+/).last
  end
end

または、実際の仮想列を使用してデータベース内で直接実行することもできます。

change_table :users do |t|
  t.virtual :lastname, type: :string, as: 'split_part(doctors.name, ' ', -1)', stored: true
  t.index(:lastname)
end