Railsガイドにきちんと目を通して新しい知識を得る - Active Record クエリインターフェイス編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、Active Record クエリインターフェイスの章です。

railsguides.jp

データベースからオブジェクトを取り出すメソッド

リンクはこちら

create_with

APIドキュメントを見ましたが、そもそもwhereで絞り込んだ後にnewするとwhereで絞り込んだ値を持つのを知りませんでした、、

memos = Memo.where(content: 'sample01')
memo = memos.new
memo.content # => 'sample01'

memos = Memo.create_with(content: 'sample02')
memo = memos.new
memo.content # => 'sample02'

extract_associated

コレクションに対して、それに関連づくレコードを配列で取得できます。
注意点としては、Memo::ActiveRecord_Relation ではなく Arrayで返ってくるところでしょうか。

Memo.where(id: [3, 4]).extract_associated(:user)
# =>  [#<User id: 2, email: "sample04@example.com", ...>, 
# =>   #<User id: 1, email: "sample02@example.com", ...>]

from

FROM句のサブクエリを指定できます。

subquery = Memo.where(content: nil).to_sql
Memo.where(user_id: 1).select('id').from("(#{subquery}) AS memos")
#=> Memo Load (0.2ms)  SELECT "memos"."id" FROM (SELECT "memos".* FROM "memos" WHERE "memos"."content" IS NULL) AS memos WHERE "memos"."user_id" = ?  [["user_id", 1]]

readonly

readonlyを使い取得したコレクションは更新等ができなくなります。
バリデーションを無視するupdate_attributeでも無理でした。

memo = Memo.readonly.first

memo.update(content: 'update')
# => ActiveRecord::ReadOnlyRecord

memo.update_attribute(:content, 'update')
# => ActiveRecord::ReadOnlyRecord

references

includesと一緒に使い、where等を使った際にテーブル名を関連付ける。

User.includes(:memos).where('memos.user_id = 1').references(:memos)

# でもこれは以下のように書く方がよさそう
User.includes(:memos).where(memos: { user_id: 1})

単一のオブジェクトを返すクエリメソッド

リンクはこちら

findはidを複数受け取ることもできる

findってidを一つだけ受け取るものと思っていましたが、複数受け取ることもできるんですね。
where(id: ...)より短く書けるので、普通に使う場面多そうだなと思いました。 レコードがないときはエラーを返す点はwhereとは違いますね。

Memo.find(1, 2)
# =>  SELECT "memos".* FROM "memos" WHERE "memos"."id" IN (?, ?)  [["id", 1], ["id", 2]]
# => [#<Memo:0x0000000112f37880 id: 1, content: "sample01", ...>
 #<Memo:0x0000000112f377b8 id: 2, content: "sample02", ...>]

Memo.find(1, 100) # id: 100のレコードはない
# => ActiveRecord::RecordNotFound

take

「どのレコードが取り出されるかは指定されません。」ということで使うケースがあまりなさそうではあります。

Memo.take
# => SELECT "memos".* FROM "memos" LIMIT ?  [["LIMIT", 1]]

条件の上書き(rewhereなど)

リンクはこちら

いくつかは知っていましたが、一覧できちんと見れたのでよかったです。
rewhereなどは、使うとしたらdefault_scopeを使っていて、上書きしたいようなときでしょうか。
(その場合、rewhereを使わないといけない時点で実装がやや怪しい気がしますが)

class Memo < ApplicationRecord
  default_scope { where.not(content: nil) }
  ...
end

Memo.all
#=> [#<Memo:0x00000001083ea0b8 id: 1, content: "sample01", user_id: 1,...>,
 #<Memo:0x00000001083e9ff0 id: 2, content: "sample02", user_id: 1, ...>,
 #<Memo:0x00000001083e9f28 id: 3, content: "sample03", user_id: 2, ...>,
 #<Memo:0x00000001083e9e60 id: 4, content: "sample04", user_id: 2, ...>]

Memo.rewhere(content: nil)
# => [#<Memo:0x00000001079a38e8 id: 5, content: nil, user_id: 1, ...>,
 #<Memo:0x00000001079a3578 id: 6, content: nil, user_id: 1, ...>]

ifを使った際のscopeとクラスメソッドの違い

リンクはこちら

scopeはifの条件に当てはまらないときにはActiveRecord::Relationオブジェクトを返します。

class Memo < ApplicationRecord
  scope :content_presents, -> { where.not(content: nil) if false }
end

Memo.content_presents
# => [#<Memo:0x0000000109ee8ec8 id: 1, content: "sample01", ...>,
 #<Memo:0x000000010a1bda90 id: 2, content: "sample02", ...>, ...]
class Memo < ApplicationRecord
  def self.content_presents
    where.not(content: nil) if false
  end
end

Memo.content_presents
# => nil

動的検索(find_by_attribute)

リンクはこちら

find_by_カラム名で検索できるんですね。
レコードが見つからなかった場合にnilを返すのもfind_byと同じです。

Memo.find_by_content('sample01')
# => #<Memo:0x000000010afc7028 id: 1, content: "sample01", ...>

Memo.find_by_content('No Record Content')
# => nil

SQLで検索

リンクはこちら

積極的に使う機会は少ないかもですが、カスタムSQLを記述する方法として、コードリーディング等で見かけるかもなので、知れてよかったです。

find_by_sql

以下の例だと、Memoクラスのインスタンスが配列で帰ってきます。
インスタンスといえ、contentのみを取得しているので、created_atなどの情報は持っていません。

memos = Memo.find_by_sql("SELECT content FROM memos")
#=> [#<Memo:0x000000010acaf9a8 id: nil, content: "sample01">, #<Memo:0x000000010acaf8e0 id: nil, content: "sample02">, #<Memo:0x000000010acaf818 id: nil, content: "sample03">, #<Memo:0x000000010acaf750 id: nil, content: "sample04">, #<Memo:0x000000010acaf688 id: nil, content: nil>, #<Memo:0x000000010acaf5c0 id: nil, content: nil>]

memo.created_at
#=> ActiveModel::MissingAttributeError

select_all

ActiveRecord::Resultオブジェクトが返ってきます。
to_aを使うとハッシュの配列になります。

Memo.connection.select_all("SELECT content FROM memos")
#=> #<ActiveRecord::Result:0x000000010a80f470 @column_types={}, @columns=["content"], @hash_rows=nil, @rows=[["sample01"], ["sample02"], ["sample03"], ["sample04"], [nil], [nil]]>

Memo.connection.select_all("SELECT content FROM memos").to_a
#=> [{"content"=>"sample01"}, {"content"=>"sample02"}, {"content"=>"sample03"}, {"content"=>"sample04"}, {"content"=>nil}, {"content"=>nil}]

includesとpluck

includesが使われているときにpluckを使うと、eager_loadingが引き起こされるというのは知らなかったです。

User.includes(:memos)
#=> User Load (0.2ms)  SELECT "users".* FROM "users"
# => Memo Load (0.3ms)  SELECT "memos".* FROM "memos" WHERE "memos"."user_id" IN (?, ?, ?, ?, ?, ?, ?)  [["user_id", 1], ["user_id", 2], ...

User.includes(:memos).pluck(:id) # idsでも同じ
# => SELECT "users"."id" FROM "users" LEFT OUTER JOIN "memos" ON "memos"."user_id" = "users"."id"
# => [1, 1, 1, 1, 2, 2, 3, 4, 5, 6, 7]
# LEFT JOINされているので重複がある