動かしたのは Rails5.0.1 です
やりたいこと
例えば、
id |
---|
1 |
2 |
3 |
4 |
5 |
のデータが有るhoges
テーブルがあったときに
Hoge.where.not(id: 3).order(:id).endmost(3)
#=> [#<Hoge:0x007f83f2b312a8 id: 2>, #<Hoge:0x007f83f2b310c8 id: 4>, #<Hoge:0x007f83f2b30f10 id: 5>]
となるようなendmost
メソッドをどう実装すればよいか、という話です。
(メソッド名は↓で紹介する記事から取りました)
- orderで昇順で取得できる
- 下からN件取れる
- ArrayではなくActiveRecord_Relationである
という条件です。
方法
RETRIEVING THE LAST N ORDERED RECORDS WITH ACTIVERECORD
に書いてある方法です。
class Hoge
class << self
def endmost(n)
all.only(:order).from(all.reverse_order.limit(n), table_name)
end
end
end
onlyやfromなどあまり見慣れないメソッドを使っていますが、発行されるクエリを見るととてもシンプルです
Hoge.where.not(id: 3).order(:id).endmost(3)
を実行した時発行されるクエリは↓です。
SELECT "hoges".*
FROM
(SELECT "hoges".*
FROM "hoges"
WHERE ("hoges"."id" != 3)
ORDER BY "hoges"."id" DESC LIMIT 3) hoges
ORDER BY "hoges"."id" ASC
from句のサブクエリとしてwhere.not(id: 3)
の部分と下からN件
が処理され、
外側のクエリでorder(:id)
で並び替えられています。
方法
def endmost(n)
all.only(:order).where(id: all.reverse_order.limit(n).select(:id))
end
sqlは↓。
SELECT "hoges".*
FROM "hoges"
WHERE "hoges"."id" IN
(SELECT "hoges"."id"
FROM "hoges"
WHERE ("hoges"."id" != 3)
ORDER BY "hoges"."id" DESC LIMIT 3)
ORDER BY "hoges"."id" ASC
先程はselect句のサブクエリでしたが、今回はwhere句のサブクエリとして下からN件
を取得しています。
注意点
Hoge.where.not(id: 3).order(:id).endmost(3)
と
Hoge.order(:id).endmost(3).where.not(id: 3)
は結果が異なります。
後者のSQLは下になります。(方法1の場合)
SELECT "hoges".*
FROM
(SELECT "hoges".*
FROM "hoges"
ORDER BY "hoges"."id" DESC LIMIT 3) hoges
WHERE ("hoges"."id" != 3)
ORDER BY "hoges"."id" ASC
where.not(3)
の部分のsqlがサブクエリの中か外か、の違いです。
サブクエリ内で下から3件
を取得した後、WHERE ("hoges"."id" != 3)
が処理されることになるので、
結果は、もともと期待していたのは
[#<Hoge:0x007f83f2b312a8 id: 2>, #<Hoge:0x007f83f2b310c8 id: 4>, #<Hoge:0x007f83f2b30f10 id: 5>]
の3件だったのに
[#<Hoge:0x007f83f1c9fc08 id: 4>, #<Hoge:0x007f83f1c8eb10 id: 5>]
の2件になってしまいます。
重要なのは、
-
endmost
以前のorder
以外は全てサブクエリとして処理される -
endmost
以降は何を書いても外側のメインのクエリとして処理される
ということです。
Hoge.where.not(id: 3).joins(:user).order(:id).endmost(3).where(id: 4).select(:id) # このコードに意味は無いです。すごく適当に作りました
としたときに発行されるクエリは↓になります。
SELECT "id"
FROM
(SELECT "hoges".*
FROM "hoges"
INNER JOIN "users" ON "users"."id" = "hoges"."user_id"
WHERE ("hoges"."id" != 3)
ORDER BY "hoges"."id" DESC LIMIT 3) hoges
WHERE "hoges"."id" = 4
ORDER BY "hoges"."id" ASC
最後に
方法1
と方法2
のどちらが良いのかは判断できてないです。
from句書いたことがないのでちょっと怖い気持ちがあるけど、
勘ではクエリ的にはwhere句よりfrom句のサブクエリのほうが早そう、(SQLの気持ちわかってないので実際どっちが効率的なのかは知らないです。。)
というところです。
こっちの方法が良い、とか、この方法は間違っている、とか、もっと良い方法がある、などありましたらコメントをいただけると幸いです
ここまででこの記事のメインは終わります。
これ以降は調べてる途中にわかった補足です。
onlyについて
onlyは
Hoge.where(id: 1).only(:order) # whereが無視される
Hoge.order(:id).where(id: 1).only(:order) whereが無視され、orderだけ有効
というようなメソッドです。
方法1
では
all.only(:order).from(all.reverse_order.limit(n), table_name)
としてましたが、
このonlyの入れる位置はもとても重要です。
これ以降も方法1
を例にして説明していきます。
そもそもonlyがない場合
def endmost(n)
all.from(all.reverse_order.limit(n), table_name)
end
Hoge.where.not(id: 3).order(:id).endmost(3)
の場合、sqlは↓になります。
SELECT "hoges".*
FROM
(SELECT "hoges".*
FROM "hoges"
WHERE ("hoges"."id" != 3)
ORDER BY "hoges"."id" DESC LIMIT 3) hoges
WHERE ("hoges"."id" != 3)
ORDER BY "hoges"."id" ASC
WHERE ("hoges"."id" != 3)
が2回現れています。
結果は変わらないはずですが、無駄に処理が行われてしまいます。
onlyをfromの中に入れた場合
def endmost(n)
all.from(all.only(:order).reverse_order.limit(n), table_name)
end
Hoge.where.not(id: 3).order(:id).endmost(3)
のケースです。
sqlは↓になります。
SELECT "hoges".*
FROM
(SELECT "hoges".*
FROM "hoges"
ORDER BY "hoges"."id" DESC LIMIT 3) hoges
WHERE ("hoges"."id" != 3)
ORDER BY "hoges"."id" ASC
注意点
のところで書いたSQLと同じです。
where.not(id: 3)
のwhere句がサブクエリ内ではなく外にいます。
その為結果は、
[#<Hoge:0x007f83f1c9fc08 id: 4>, #<Hoge:0x007f83f1c8eb10 id: 5>]
となってしまいます。
そもそもなぜこのendmostが必要になったか
Query Objectを作ろうとしていて必要になりました。
class AwesomeHugaQuery
def initialize(relation: Huga.all, limit:)
@relation = relation
@limit = limit
end
def all
@relation.awesome_scope.order(:id).endmost(@limit) # こんな感じの複雑なクエリ
end
end
query = AwesomeHugaQuery.new(relation: Huga.active, limit: LIMIT)
query.all.includes(:foo)
query.all.includes(:bar)
ということがしたかったのでした。
@relation.awesome_scope.order(:id).last(@limit)
@relation.awesome_scope.order(id: :desc).limit(@limit).reverse
だとArrayになってしまうのでquery.all.includes(:foo)
でエラーになってしまうし、
だからといって
@relation.awesome_scope.order(id: :desc).limit(@limit)
query.all.includes(:foo).reverse
query.all.includes(:bar).reverse
ともしたくなかったし、
def all(association = nil)
scope = @relation.awesome_scope
if association
scope.includes(association)
end
scope.order(:id).last(@limit)
end
query.all(:foo)
query.all(:bar)
というようなこともしたくなかったので、endmost
が必要になりました