2023/12/6 追記
いいねが多くなってきたので1点追記します。
本記事ではサンプルコードとしてAdminUserというモデルに2回アクセスしています。
パフォーマンス観点では、なるべくクエリ発行は少なくするべきなので、
配列をActive Record Relationに戻す必要が本当にあるのか等、
他の方法がないかよく検討してください。
変換できるのか
誤解を生む可能性がある為はじめに書いておきます。
「配列をActive Record Relationに変換したい」という理由でこの記事を見ていただいている方がいると思いますが、
厳密には配列をActive Record Relationに変換することは出来ません。
ただし、取得した配列を使って、Active Record Relationを取得することは出来ます。
私自身「配列 Active Record Relation 変換」で検索したりしたので、あえてこのようなタイトルにしています。
配列とActive Record Relationについて
はじめに簡単に説明をしますが、分かる方はここまで飛ばしてください。
配列とActive Record Relation、それぞれの例を上げてみます。
#Active Record Relationを作ります。
AdminUser.where(id: [2, 3])
=> [#<AdminUser:0x00007fd2621de8e0
id: 2,
name: "山田">,
#<AdminUser:0x00007fd2621de4f8
id: 3,
name: "佐藤">]
#Arrayを作ります。
AdminUser.where(id: [2, 3]).select(&:name)
=> [#<AdminUser:0x00007fd26231ad80
id: 2,
name: "山田">,
#<AdminUser:0x00007fd26231aa88
id: 3,
name: "佐藤">]
結果は同じように見えますが、.class
をつけてみると違いが分かります。
AdminUser.where(id: [2, 3]).class
=> AdminUser::ActiveRecord_Relation
AdminUser.where(id: [2, 3]).select(&:name).class
=> Array
モデルからデータを取り出す際に、where
やfind
、select
、map
など色々なメソッドを使うことがあると思いますが、Railsでは、使うメソッドによって返り値の型が決まっています。
上記の例の使い方で言うと、where
だとActive Record Relationが返り、select
だとArrayクラスが返ることになります。
※厳密にはselect
は使い方によってはActive Record Relationが返ります。
例えばこんな時に困る
Viewに何らかのデータを表示したい。
where
を使ってActive Record Relationにしたはいいが、
そのあとselect
でデータの絞り込みなどを行った。
Viewに表示する前に並び替えをしたいので、order
メソッドを使ったら、エラーが発生した。
Railsで開発をする際によく使うorder
と言うメソッド。並び替えが出来ます。
#idが大きい順に並び替える
AdminUser.where(id: [2, 3]).order(id: :desc)
=> [#<AdminUser:0x00007fd260347a30
id: 3,
name: "佐藤",
#以下省略
このorder
メソッドをArrayクラスに対して使うとこのようなエラーになります。
AdminUser.where(id: [2, 3]).select(&:name).order(id: :desc)
=> NoMethodError: undefined method `order' for #<Array:0x00007fd26027d578>
このような場合に、AdminUser.where(id: [2, 3]).select(&:name)
をActive Record Relationに変換したい、という欲望が生まれます。
結論
ここからがタイトルのような欲望を叶える具体的な方法です。
admin_usersというArrayクラスから@admin_usersというActive Record Relationを作りたいなら、こうします。
admin_users = AdminUser.where(id: [2, 3]).select(&:name)
@admin_users = AdminUser.where(id: admin_users.map(&:id))
要はwhere(id: admin_users.map(&:id))
をしているだけですが、
何をしているかというと、既にある配列のidだけを取り出して配列を作って、それをwhere
の引数に使ってAdminUserから再度データを取得しています。
注意点として、下記があるでしょうか。
- データの中身の順番が変わる可能性があること
- データを再取得する処理を行っていること(大量のデータの場合は余計な時間がかかる)
実装方法
この処理を使って実装するにあたって、同じ処理をたくさんするのであれば、スッキリとまとめてしまいたいです。
2つの方法を紹介します。
1. コントローラーやモデルに再取得メソッドを作る
2. Arrayクラスを拡張して再取得メソッドを作る
1.コントローラーやモデルに再取得メソッドを作る
コントローラーやモデルに下記のように書きます。
def self.get_activerecord_relation(arr)
where(id: arr.map(&:id))
end
使い方
admin_users = AdminUser.where(id: [2, 3]).select(&:name) => #Arrayクラス
@admin_users = AdminUser.get_activerecord_relation(admin_users) => #Active Record Relation
ちょっと気持ち悪いかなと思います。
2.Arrayクラスを拡張して再取得メソッドを作る
array.rbというファイルを作ります。
class Array
def to_activerecord_relation
return ApplicationRecord.none if self.empty?
clazzes = self.map(&:class).uniq
raise 'Array cannot be converted to ActiveRecord::Relation since it does not have same elements' if clazzes.size > 1
clazz = clazzes.first
raise 'Element class is not ApplicationRecord and as such cannot be converted' unless clazz.ancestors.include? ApplicationRecord
clazz.where(id: self.map(&:id))
end
end
core_ext.rbというファイルを作ります。
require 'core_ext/array'
使い方
admin_users = AdminUser.where(id: [2, 3]).select(&:name) => #Arrayクラス
@admin_users = admin_users.to_activerecord_relation => #Active Record Relation
2の方法は、Rubyのオープンクラスという後からメソッドを追加したり出来る機能を使った方法で、
Arrayクラスに全体に対して、to_activerecord_relation
というメソッドが使えるようにしてしまおう、みたいな感じです。
オープンクラスに関しては今回紹介したもの以外には例えば、
Stringクラスにto_bool
というメソッドを追加して、"true"
や"false"
という文字列が代入されたオブジェクトをboolean型に変換出来るようにする、なども出来ます。
参考にさせていただいた記事
https://qiita.com/shibadai/items/ddbc76a8b980cd8354bc
https://stackoverflow.com/questions/17331862/converting-an-array-of-objects-to-activerecordrelation