113
85

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Railsの生SQL概要:execute, select_all, find_by_sqlの使い方を覚える

Last updated at Posted at 2019-12-21

Railsで使用する生SQL

Railsにおいて、ActiveRecordのwhere等では表現できない複雑なクエリを使用してDBのデータを取得する場合、生でSQLを書く必要が出てくると思います。

しかし、AcitiveRecordで生sqlを実行するメソッドは数多くあり、用途によってどれを使用すべきか分からないという方もいるのではないでしょうか。

そこで今回はよく使用されるであろう、execute/select_all/find_by_sqlについて、どのように呼び出すのか戻り値はなにかどのような用途で使用すべきかについて説明します。

先に結論を書いておくと

execute -> クエリの実行
select_all -> セレクト文の結果取得
find_by_sql -> ActiveRecordオブジェクトの取得

となります。詳しくは以下ご覧ください

環境

Rails 5.2.3
Ruby 2.6.0

SQL

今回は全てのメソッドで同じSQLを実行します。
StatusテーブルとAccountテーブルをJOINして、それぞれのデータを取得するselect文です。

select s.text, a.username, a.updated_at from statuses s inner join accounts a on a.id = s.account_id where a.id =1

では1つずつ確認します。

execute

概要

executeは、SQLの実行を行うメソッドです。(https://api.rubyonrails.org/v5.2.3/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-execute)

実行

ActiveRecord::Base.connection.execute(sql)のようにして実行します。

res = ActiveRecord::Base.connection.execute('select s.text, a.username, a.updated_at from statuses s inner join accounts a on a.id = s.account_id where a.id = 1')
   (0.6ms)  select s.text, a.username, a.updated_at from statuses s inner join accounts a on a.id = s.account_id where a.id = 1
=> #<PG::Result:0x00007f89f93da560 status=PGRES_TUPLES_OK ntuples=5 nfields=3 cmd_tuples=5>

このようにPG::Resultのインスタンスが返ってきました。このインスタンスは使用するDBによって異なりますが、中身としては実行後のstatus情報が入っています。

用途

この返り値から見ても、executeはその名の通り、クエリの実行を目的としており、取得を目的としているものではありません。

そのため、insertやupdate等のクエリを直接実行したい場合に使用するメソッドと言えます。

似たような実行を目的としたメソッドに、create(レコードの作成)やtransaction(トランザクションの実行)などがあります。
https://api.rubyonrails.org/v5.2.3/files/activerecord/lib/active_record/connection_adapters/abstract/database_statements_rb.html)

select_all

概要

select_allは、実行したSelect文のレコードを取得するメソッドです。
https://api.rubyonrails.org/v5.2.3/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-select_all

実行

実行する場合は、ActiveRecord::Base.connection.execute(sql)というように、sqlを引数に渡して実行します。

res = ActiveRecord::Base.connection.select_all('select s.text, a.username, a.updated_at from statuses s inner join accounts a on a.id = s.account_id where a.id = 1')
   (0.7ms)  select s.text, a.username, a.updated_at from statuses s inner join accounts a on a.id = s.account_id where a.id = 1
=> #<ActiveRecord::Result:0x00007f89f93fa310
 @column_types=
  {"text"=>#<ActiveRecord::Type::Text:0x00007f89f6fa8e80 @limit=nil, @precision=nil, @scale=nil>,
   "username"=>#<ActiveModel::Type::String:0x00007f89f6d5ce88 @limit=nil, @precision=nil, @scale=nil>,
   "updated_at"=>#<ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime:0x00007f89f6d5c3e8 @limit=nil, @precision=nil, @scale=nil>},
 @columns=["text", "username", "updated_at"],
 @hash_rows=nil,
 @rows=
  [["今日から始めるドン", "sobameshi0901", "2019-12-21 11:32:44.065979"],
   ["何しているドン", "sobameshi0901", "2019-12-21 11:32:44.065979"],
   ["マストドン!", "sobameshi0901", "2019-12-21 11:32:44.065979"],

ActiveRecord::Result(https://api.rubyonrails.org/classes/ActiveRecord/Result.html
)のインスタンスが返ってきました。

アトリビュートとして、columnsにはsqlのselectで指定したカラムの一覧を配列が、rowsにはレコードの配列が入っています。

また、カラム名をkey、値をvalueとしたハッシュを1件のレコードとした配列を取得することができるto_aメソッドが用意されていたり、Enumerableがincludeされているので、eachmapなどのメソッドを使用することもできます。

res.to_a
=> [{"text"=>"今日から始めるドン", "username"=>"sobameshi0901", "updated_at"=>"2019-12-21 11:32:44.065979"},
 {"text"=>"何しているドン", "username"=>"sobameshi0901", "updated_at"=>"2019-12-21 11:32:44.065979"},
 {"text"=>"マストドン!", "username"=>"sobameshi0901", "updated_at"=>"2019-12-21 11:32:44.065979"}

res.each{|a|puts "#{a['username']}:#{a['text']}"}
sobameshi0901:今日から始めるドン
sobameshi0901:何しているドン
sobameshi0901:マストドン!

用途

このように、select文の結果のレコードを扱いやすい形にして返してくれるのがこのselect_allメソッドです。

selectクエリを実行した状態(ActiveRecordを継承した1モデルではなく、様々なテーブルを組み合わせた結果)をそのまま扱いやすい形で欲しいという場合にはこのメソッドが使用できるでしょう。

関連するメソッドにはselect_one(1件だけ取得する)、select_values(selectの最初のカラムのレコードのみを取得する)などがあります。
https://api.rubyonrails.org/v5.2.3/files/activerecord/lib/active_record/connection_adapters/abstract/database_statements_rb.html)

find_by_sql

概要

find_by_sqlは、ActiveRecordを継承したモデルのインスタンス一覧を取得するメソッドです。
https://api.rubyonrails.org/v5.2.3/classes/ActiveRecord/Querying.html#method-i-find_by_sql

実行

Status.find_by_sql('select s.text, a.username, a.updated_at from statuses s inner join accounts a on a.id = s.account_id where a.id = 1')のように、ActiveRecordモデルから実行します。

res = Status.find_by_sql('select s.text, a.username, a.updated_at from statuses s inner join accounts a on a.id = s.account_id where a.id = 1')
  Status Load (0.9ms)  select s.text, a.username, a.updated_at from statuses s inner join accounts a on a.id = s.account_id where a.id = 1
=> [#<Status:0x00007f89f2f1b160 id: nil, text: "今日から始めるドン", updated_at: Sat, 21 Dec 2019 11:32:44 UTC +00:00>,
 #<Status:0x00007f89f2f1afa8 id: nil, text: "何しているドン", updated_at: Sat, 21 Dec 2019 11:32:44 UTC +00:00>,
 #<Status:0x00007f89f2f1a5a8 id: nil, text: "マストドン!", updated_at: Sat, 21 Dec 2019 11:32:44 UTC +00:00>,]

このように、一覧にStatusインスタンスの一覧が配列になって返ってきます。
返ってきたStatusインスタンスは、selectで選択されたカラム(今回はtextとupdated_at)をアトリビュートとして持っています。

selectに入っているカラムでも、Statusモデルのアトリビュートにもともと存在しないものは無視されます。
また、カラム名で判断されるため、今回のようにselectでaccountsテーブルのupdated_at(a.updated_at)を選択していた場合でも、Statusインスタンスのアトリビュートとなってしまうので注意してください。

用途

whereなどでは表現できない/しづらい複雑なクエリで検索した上で、ActiveRecord継承モデルの一覧を取得したい場合に使用するのがよいでしょう。

ちなみに、このメソッド、定義元を見ると、select_allを使用していることが分かります。
その結果をモデルのcolumnに存在するアトリビュートのみを抽出して、インスタンスを生成していました。


def find_by_sql(sql, binds = [], preparable: nil, &block)
      result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable)
      column_types = result_set.column_types.dup
      attribute_types.each_key { |k| column_types.delete k }
      message_bus = ActiveSupport::Notifications.instrumenter

      payload = {
        record_count: result_set.length,
        class_name: name
      }

      message_bus.instrument("instantiation.active_record", payload) do
        result_set.map { |record| instantiate(record, column_types, &block) }
      end
end

終わりに

生SQLの実行メソッドはまだまだたくさんありますが、単純に実行するもの、セレクト結果をそのまま取得するもの、ActiveRecord継承モデルのインスタンスを取得するものと、基本的な使い方がわかったのではないでしょうか。

また頻繁に使用するメソッドなどがあれば追加していきます。

113
85
0

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
113
85

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?