****** 検索 ****** ============== 多対多の対応 ============== 成績データを追加してみよう。 * 学生に対しても科目に対しても複数の成績データが存在するので、どちらのモデルにも入らない(関係データベースはカラムが固定なので、可変個のデータは入れられない)。 * 新しく ``Score`` モデル(ある学生のある科目の成績ごとに1個のデータ)を作り、それを ``Student`` や ``Course`` と対応付ける。 安直に ``scaffold`` でコントローラとビューも作ってしまおう。カラムは学生ID、科目ID、評語の三つ。 :: % rails generate scaffold score student_id:integer course_id:integer score:string % rails db:migrate モデル間の対応は次のように定義する。詳しくは `Active Record の関連付け (アソシエーション) - RailsGuides `_ を見よ。 * ``app/models/score.rb`` .. code-block:: ruby belongs_to :student belongs_to :course * ``app/models/student.rb`` .. code-block:: ruby has_many :scores has_many :courses, through: :scores * ``app/models/course.rb`` .. code-block:: ruby has_many :scores has_many :students, through: :scores こうすると、 ``Student`` と ``Course`` が ``Score`` を介して多対多の対応関係で結ばれる。例えば、 http://localhost:3000/scores/new で1番の学生の成績データを入力しておくと、 ``Student.find(1).courses`` で履修した科目の配列が得られる。 :: root@c65985cfc603:/home/school# rails c Loading development environment (Rails 7.0.4) irb(main):001:0> Student.find(1).courses Student Load (1.2ms) SELECT "students".* FROM "students" WHERE "students"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] Course Load (0.6ms) SELECT "courses".* FROM "courses" INNER JOIN "scores" ON "courses"."id" = "scores"."course_id" WHERE "scores"."student_id" = ? [["student_id", 1]] => [#] .. image:: has-many-through.png ================================== 検索に関するページのルーティング ================================== ある科目である成績をとった学生を検索してみよう。検索条件の入力と、結果の表示の二種類のページが必要になる。 検索条件の入力ページを ``/students/search`` 、結果表示ページを ``/students/result`` と決めると、ルーティングは次のように書ける。 .. code-block:: ruby resources :students do collection do get 'search' get 'result' end end * ``resources :students`` の後にブロックを付け、その中に ``collection do ... end`` を書く。 * ``collection do ... end`` の中には単一ルーティングと同じ形(VerbとURLパターン)を書く。ただし、ここで書いたバターンは、 ``/students/`` の後にくっつくので、結局 ``/students/search`` と ``/students/result`` というURLになる。学生全体に対して一つのページを作りたい時に使う。 * もし学生一人ずつにそれぞれページを作りたい場合は、 ``collection`` ではなく ``member`` を使う。その場合は、 ``/students/:id/`` の後にくっつく。 ========================== 検索条件を入力するページ ========================== ``/students/search`` にアクセスすると、 ``get 'search'`` の次に何も書いていないので、自動的に ``StudentsController`` の ``search`` メソッドが呼び出される。ここには何も書くことはない。 ``app/controllers/students_controller.rb`` に空のメソッドを定義しておく。 .. code-block:: ruby # GET /students/search def search end 何も書いていないので自動的に ``app/views/students/search.html.erb`` が呼び出される。そこに検索条件を入力するフォームを書く。 .. code-block:: erb

成績を指定して学生を検索

<%= form_with(url: result_students_path, method: :get) do |form| %>
<%= form.label :course, '科目' %> <%= form.collection_select :course, Course.all, :id, :name %>
<%= form.label :score, '成績' %> <%= form.select :score, {'S' => 'S', 'A' => 'A', 'B' => 'B', 'C' => 'C', 'D' => 'D'} %>
<%= form.submit '検索' %>
<% end %> * ``form_with`` の引数はハッシュで、 ``url`` と ``method`` がキーになっている。 * ``url`` は、このフォームのデータが送られる宛先のURL。 ``result_students_path`` は結果表示ページのパスを返すヘルパーメソッド。 * ``method`` は、データを送る時のHTTPメソッド。Rails7では ``get`` を指定しないとTurboを使った非同期通信になり、表示に ``turbo-frame`` を使う必要がある。ちなみに Rails guide には、検索結果をブックマークしたりできるので ``get`` の方が良い( ``get`` リクエストは検索パラメータを含むURLになるので)と書いてある。 .. image:: search-get.png ========================== 検索結果を表示するページ ========================== 検索ボタンを押すと、フォームに入力したデータが ``/students/result`` に送信される。 これに対応して ``StudentsController`` クラスに ``result`` メソッドを追加する。ここで検索を行うわけだが、二つのやり方がある。 * とりあえずデータベースから全部データを取り出して、1個ずつRubyプログラムで条件が成り立つかどうか判定する。この例だと ``Student.all.select do |x| ... end`` というようなループで処理する。どんな条件でも書けるが、不要なデータもいったん取り出して処理するので遅い。また、データの件数が多い場合は、 ``all`` だと大量のメモリが必要になるので ``find_each`` などを使って少しずつ取り出すようにしなければならない。 * 関係データベースの機能を使って、条件に合うデータだけを取り出す。条件はSQLか、SQLを生成するメソッドを使って書く。SQLがわかっていないとつらい。 ここでは2番目のやり方でやってみる。詳しくは `Active Record クエリインターフェース - RailsGuides `_ を見よ。 .. code-block:: ruby # POST /students/search def result @students = Student.joins(:scores).where('course_id = ? AND score = ?', params[:course], params[:score]) render :index end * ``Student.joins(:scores)`` で学生と成績のテーブルを結合する。正確には ``SELECT students.* FROM students INNER JOIN scores ON students.id = scores.student_id`` というSQLを生成する。 * ``where`` の引数に条件を書く。第一引数の中の ``?`` は、順にその後の引数で置き換えられる(この時、SQLで特別な意味を持つ文字はエスケープされる)。このようにしないで ``"course_id = #{params[:course]}"`` のように第一引数の中に直接パラメータを埋め込むと SQL injection というセキュリティホールになる。 * 関係データベースの中では学生と成績のテーブルを結合して検索が行われるが、Rubyの世界ではクラスが結合されるわけではないので、検索結果は ``joins`` メソッドを呼び出したクラス(この場合は ``Student`` )のインスタンスの配列になる。 * 最後に検索結果を表示する。新しくERBファイルを作ってもよいが、面倒なので一覧表示用のビューを使いまわす。検索結果を代入する変数が ``@students`` であり、一覧表示の時と同じ変数名であるところがミソ。 ``render :index`` は ``index.html.erb`` をビューとして使うように指定する。 .. note:: ここでは検索結果として学生の氏名など ``Studnet`` モデルの属性だけ表示しているが、履修科目や成績なども表示しようとすると、もう一度 ``Score`` モデルにアクセスする必要がある。そこで改めてデータベースにクエリを飛ばすのは効率が悪いので、 ``joins`` の代わりに ``preload`` や ``eager_load`` や ``includes`` を使うと結合したテーブルをメモリに保持しておくことができる。 * `ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い - Qiita `_ .. image:: search-post.png ============== 今日のまとめ ============== `ここまでのソースコードはこちら `_