自宅のWindowsでRuby on Rails ~ ActiveRecordを利用したクエリの実行 ~

クエリとは、データベースに対して情報を取得したり、データを操作するために使用する命令文のことです。

クエリは、SQL(Structured Query Language)と呼ばれる特定の言語を使用して記述されます。

データベースに対するクエリには、大きく分けて2つの種類があります。

1.検索クエリ

検索クエリは、データベースから情報を取得するために使用されます。

たとえば、あるテーブル内のデータを取得する、条件に一致するデータを取得する、複数のテーブルを結合して情報を取得するなどが挙げられます。

2.更新クエリ

更新クエリは、データベース内のデータを更新するために使用されます。

たとえば、あるテーブル内のデータを更新する、テーブルに新しいデータを挿入する、あるテーブルからデータを削除するなどが挙げられます。


Railsにおいては、ActiveRecordがSQLクエリを生成してデータベースに送信するための様々なメソッドを提供しています。

これらのメソッドを使用することで、SQLを直接記述することなく、簡単にデータベースを操作することができます。

また、ActiveRecordにはSQLインジェクション攻撃から保護するための機能が組み込まれているため、安全なデータベース操作が可能です。

1. ActiveRecordモデルクラスのメソッド


RailsのActiveRecordを利用したクエリの実行には、ActiveRecord::Baseモデルクラスのメソッドを使用します。

ActiveRecord::Baseは、すべてのActive Recordモデルの基底クラスであり、データベースにアクセスするための主要な機能を提供しています。

以下に、ActiveRecordを利用したクエリの実行方法をいくつか紹介します。

1-1. .all メソッド

.allメソッドは、指定されたモデルに関連するすべてのレコードを取得するために使用されます。

使用方法は以下のようになります。

books = Book.all

上記の例では、Bookモデルに関連するすべてのレコードを取得し、books変数に代入しています。

また、.allメソッドには、次のような特徴があります。

・引数を取らずに使用されます。

・非常にシンプルで柔軟なクエリメソッドであり、複数のActiveRecordのクエリメソッドと組み合わせて使用することができます。
 以下は例として.orderメソッドとの組み合わせです。

books = Book.all.order(published_at: :desc)

・ActiveRecordのリレーションを取得する際にも使用されます。
例えば、以下のように書くことで、Bookモデルに関連するすべてのCommentモデルのレコードを取得することができます。

comments = Book.all.comments

上記の例では、Bookモデルに関連するすべてのCommentモデルのレコードを取得し、comments変数に代入しています。


なお、.allメソッドは実際にはクエリを実行せず、ActiveRecordのリレーションオブジェクトを返します。

クエリを実行するには、例えば.eachメソッドを呼び出すなどしてリレーションオブジェクトを展開する必要があります。

1-2. .where メソッド

.whereメソッドは、指定された条件に合致するレコードを取得するために使用されます。

books = Book.where(author: "山田太郎")

上記の例では、Bookモデルのauthorカラムに値が"山田太郎"に該当するレコードをすべて取得し、books変数に代入しています。


また、.whereメソッドは、次のような特徴があります。

・条件式を引数に取ります。条件式は、以下のように記述されます。

Model.where(条件式)

・条件式には、比較演算子や論理演算子を含めることができます。
 以下のような条件式を記述することができます。

Model.where("カラム名 演算子 値")

ここで、Modelはクエリを実行する対象となるモデルのクラス名です。

カラム名は、クエリを実行する対象となるカラムの名前であり、演算子は比較演算子のうちの1つであり、値は比較する値です。


比較演算子には、以下のようなものがあります。

| 演算子 | 意味 |
| —- | —-|
| = | 等しい |
| > | 大きい |
| < | 小さい |
| >= | 以上 |
| <= | 以下 |
| != | 等しくない |

・プレースホルダーを使用することが推奨されています。
 条件式に文字列を直接指定することもできますが、ActiveRecordでは、SQLインジェクション攻撃に対処するために、プレースホルダーを使用することを推奨しています。
 プレースホルダーを使用する場合、条件式の中に?を記述して、プレースホルダーに対応する値を配列の形で指定します。

・複数の条件を指定することもできます。
 複数の条件を指定する場合は、ANDやORを用いて連結します。
 また、以下のように、複数の条件をハッシュ形式で指定することもできます。

books = Book.where(author: "山田太郎", published_at: Date.today - 1.week..Date.today)

上記の例では、authorカラムが"山田太郎"であり、かつpublished_atカラムが1週間以内のレコードを取得しています。


なお、.whereメソッドを呼び出した時点では、実際のクエリはまだ実行されておらず、ActiveRecordのリレーションオブジェクトが返されます。

クエリを実行するには、ActiveRecordのリレーションオブジェクトを展開するために、例えば.eachメソッドを呼び出す必要があります。

1-3. .select メソッド

.selectメソッドは、データベースから取得するカラムを指定するために使用されます。

以下のように使用します。

books = Book.select(:id)

上記の例では、Bookモデルのidカラムのみを取得し、books変数に代入しています。

また、.selectメソッドは次のような特徴があります。

・取得するカラムを引数に指定します。

・複数のカラムを指定する場合は、カンマで区切って指定することができます。

books = Book.select(:id, :title, :author)

上記の例では、idカラムとtitleカラムとauthorカラムを取得しています。

・SQLの集計関数(COUNT、SUM、AVG、MAX、MIN)を使ってデータを集計することもできます。

code>max_published_at = Book.select("MAX(published_at)").first

上記の例では、MAX関数を使ってpublished_atカラムの最大値を取得しています。

.selectメソッドに文字列を渡す場合は、引用符で囲む必要があります。


なお、.selectメソッドを使う場合、指定されたカラム以外のカラムの値は取得されません。

そのため、.selectメソッドを使う場合は、必要なカラムのみを指定するようにしましょう。

また、.selectメソッドは、データベースの負荷を軽減するためにも有効なメソッドです。

1-4. .group メソッド

.groupメソッドは、指定したカラムでグループ化したデータを取得するために使用されます。

以下のように使用します。

books = Book.group(:author)

また、.groupメソッドは次のような特徴があります。

・グループ化するカラムを引数に指定します。

・複数のカラムでグループ化する場合は、カンマで区切って指定することができます。

books = Book.group(:author, :publisher)

上記の例では、authorカラムとpublisherカラムでグループ化したデータを取得しています。

・指定したカラムごとに集計関数(COUNT、SUM、AVG、MAX、MIN)を使ってデータを集計することもできます。
 以下は、Bookモデルのauthorカラムごとの書籍数を取得する例です。

books_count_by_author = Book.group(:author).count

上記の例では、COUNT関数を使ってauthorカラムごとの書籍数を取得しています。

1-5. .joinsメソッド

.joinsメソッドは、異なるテーブルを関連付けて、複数のテーブルからデータを取得するために使用されます。

以下のように使用します。

orders = Order.joins(:customer)

上記の例では、関連付けられたOrderモデルとCustomerモデルのデータを結合して取得しています。

また、.joinsメソッドは次のような特徴があります。

・関連付けるモデルを引数に指定します。

・複数のモデルを関連付ける場合は、カンマで区切って指定することができます。

orders = Order.joins(:customer, :items)

上記の例では、OrderモデルとCustomerモデル、Itemモデルを関連付けて、3つのモデルのデータを結合して取得しています。

・INNER JOIN、LEFT OUTER JOIN、RIGHT OUTER JOIN、FULL OUTER JOINなどの結合方法を指定することもできます。
 以下は、INNER JOINを使ってOrderモデルとCustomerモデルのデータを結合して取得する例です。

orders = Order.joins(:customer).where(customers: { name: 'John' })

上記の例では、INNER JOINを使ってOrderモデルとCustomerモデルのデータを結合して、nameカラムが'John'であるCustomerモデルのデータを取得しています。

.whereメソッドを使って、Customerモデルのデータに対して絞り込み条件を指定しています。



なお、.joinsメソッドを使う場合は、必要なカラムのみを取得するようにしましょう。

余分なカラムを取得すると、パフォーマンスが低下する可能性があります。

1-6. .includesメソッド

関連するテーブルのデータをプリロードして、N+1問題を解消するために使用されます。

例えば、以下のようなコードを考えてみます。code>orders = Order.all

orders.each do |order|
  puts order.customer.name
end

上記の例では、Orderモデルの全てのデータを取得し、それぞれのOrderオブジェクトに紐づくCustomerオブジェクトのname属性を出力しています。


しかしこのコードには問題があります。

orders.eachのループ内で、それぞれのOrderオブジェクトに対してcustomerメソッドを呼び出しているため、データベースに対してN+1回のクエリが発行される可能性があります。

つまり、Orderの数が増えるにつれて、データベースに対するクエリの回数が増加し、パフォーマンスに悪影響を与える可能性があるということです。


.includesメソッドを使うことで、この問題を解決することができます。

.includesメソッドを使うと、指定した関連テーブルのデータをプリロードするため、N+1問題を解消することができます。

以下は、.includesメソッドを使って、OrderモデルとCustomerモデルのデータを取得する例です。

gt;orders = Order.includes(:customer)
orders.each do |order|
  puts order.customer.name
end

上記の例では、.includesメソッドを使って、OrderモデルとCustomerモデルの関連データをプリロードしています。

そのため、orders.eachのループ内でcustomerメソッドを呼び出す際には、追加のクエリが発行されず、パフォーマンスの低下を防ぐことができます。


また、.includesメソッドを使う場合は、必要なカラムのみを取得するようにしましょう。

余分なカラムを取得すると、パフォーマンスが低下する可能性があります。


なお、.includesメソッドは、.joinsメソッドと併用することができます。

.joinsメソッドを使って、複数のテーブルを結合し、.includesメソッドを使って、関連するテーブルのデータをプリロードすることができます。


これらのメソッドを組み合わせることで、複雑なクエリを実行することができます。

また、ActiveRecordのクエリメソッドは、チェーンメソッドとして使用することができるため、シンプルかつ柔軟なクエリを実行することができます。

2. 実装

では、自宅のWindowsでRuby on Rails ~ 非同期処理までで作っていたStockinアプリに、検索機能を実装していきます。

実装後の画面はこちらになります。


日付、商品名、業者名、在庫数で仕入一覧を検索しています。

実際のコードは以下になります。

2-1. app/views/purchase_records/index.html.erb

<div class="d-flex justify-content-between mb-4">
  <h1 class="h3">仕入一覧</h1>
  <%= link_to '新規仕入登録', new_purchase_record_path, class: 'btn btn-primary' %>
</div>

<div class="card mb-2">
  <div class="card-body">
    <%= form_tag search_purchase_records_path, method: :get, class: 'mb-4' do %>
      <div class="row g-3">
        <div class="col-md-3 mb-1">
          <%= label_tag :purchase_date, "日付", class: 'form-label' %>
          <%= date_field_tag :purchase_date, params[:purchase_date], class: 'form-control' %>
        </div>

        <div class="col-md-3 mb-1">
          <%= label_tag :product_name, "商品名", class: 'form-label' %>
          <%= text_field_tag :product_name, params[:product_name], class: 'form-control' %>
          <div class="d-flex justify-content-start">
            <div class="form-check">
              <%= radio_button_tag :product_name_match_type, "forward", true, class: 'form-check-input' %>
              <%= label_tag :product_name_match_type_forward, "前方一致", class: 'form-check-label' %>
            </div>
            <div class="px-3"></div>
          &nbs

2-2. app/controllers/purchase_records_controller.rb

class PurchaseRecordsController < ApplicationController

  def index
    @purchase_records = PurchaseRecord.includes(:product, :supplier).all
  end

  def search
    product_name_match_type = params[:product_name_match_type] == 'partial' ? '%' : ''
    supplier_name_match_type = params[:supplier_name_match_type] == 'partial' ? '%' : ''
    
    conditions = []
    values = []
    if params[:purchase_date].present?
      conditions << "purchase_records.purchase_date = ?"
      values << params[:purchase_date]
    end
    
    if params[:product_name].present?
      conditions << "products.name LIKE ?"
      values << "#{product_name_match_type}#{params[:product_name]}"
    end
    
    if params[:supplier_name].present?
      conditions << "suppliers.name LIKE ?"
      values << "#{supplier_name_match_type}#{params[:supplier_name]}"
    end
    
    if params[:product_stock].present?
      conditions << "products.stock > ?"
      values << params[:product_stock]
    end
    
    @purchase_records = PurchaseRecord
      .joins("LEFT OUTER JOIN products ON purchase_records.product_id = products.id")
      .joins("LEFT OUTER JOIN suppliers ON purchase_records.supplier_id = suppliers.id")
      .select('purchase_records.*, products.name as product_name, suppliers.name as supplier_name')<

3. 解説


3-1. ビュー

検索フォームには、

&lt;%= form_tag search_purchase_records_path, method: :get, class: &apos;mb-4&apos; do %&gt;

を記述することにより、コントローラーのsearchアクションに処理を渡しています。

ルーティングには、get &apos;search&apos;を記述します。


また、radio_button_tagヘルパーメソッドで、前方一致か、部分一致かをラジオボタンで選ぶようにしてあります。

3-2. コントローラー

部分一致の場合は検索したい文字列の前後に%をつけます。

前方一致の場合は、検索したい文字列の後ろにだけ%をつけます。

また、フォームの検索条件が空ではない場合のみ、条件式に加える処理を行っています。

インジェクション対策のため、条件式にはプレースホルダーを使用しています。

4. まとめ


今回はActiveRecordのクエリメソッドの使い方について、学習しました。

ActiveRecordのクエリメソッドを使うことで、効率的にデータベースからデータを取得することができます。

クエリメソッドは、SQLを直接書く必要がなく、Rubyのコードでクエリを記述できるため、可読性が高く、コードのメンテナンスがしやすくなります。


また、クエリメソッドを使うことで、SQLインジェクション攻撃からアプリケーションを保護することができます。

ActiveRecordは、SQL文を自動的にエスケープするため、ユーザーからの入力値をそのままクエリに組み込むことができず、安全なクエリを生成することができます。


ただし、複雑なクエリを記述する場合は、ActiveRecordのクエリメソッドだけでは限界があります。

その場合は、SQLを直接書く必要があるかもしれません。

また、ActiveRecordのクエリメソッドが自動生成するSQLを確認することで、クエリの最適化やパフォーマンスの改善を行うことができます。


ActiveRecordのクエリメソッドには、.all、.where、.select、.joins、.group、.includesなど多くのメソッドがあります。

これらのメソッドを組み合わせることで、複雑なクエリを簡単に記述することができます。

また、クエリメソッドは、Ruby on Railsをはじめとする多くのWebアプリケーションフレームワークで利用されており、Webアプリケーション開発に必須の技術となっています。


今回は以上となります。

ありがとうございました。