QA@IT

Railsの生SQLをArel(ActiveRecord)で書きなおすには?

5016 PV

Rails 3.2で、あるモデルに、こんなクラスメソッドがあります。

def import(level, max = 10)
  db = ActiveRecord::Base.connection
  result = db.execute("SELECT word FROM levels WHERE level == #{level} AND word NOT IN (SELECT entry FROM words) limit #{max};");

10個のwordをimportするものです。スキーマは、

create_table "levels", :force => true do |t|
  t.string  "word"
  t.integer "level"
end

create_table "words", :force => true do |t|
  t.string  "entry"
  t.integer "level",      :default => 0
  t.text    "definition"
  t.string  "thesaurus",  :default => "none"
end

という感じです。SQLではなくArelで書き直せるというのは分かっているのですが、どうやればいいのかよくわかりません。正しい書き方は?

回答

Level.
  where(level: level).
  where('word NOT IN (SELECT entry FROM words)').
  limit(10).uniq.pluck('word')

とかでどうでしょう。(DISTINCTは勝手につけました)

あるいは、2つめのwhereは下記のように書いたほうが、インデックスの張り方を考えるときなど楽かもしれません。もちろん、最近のDBMSは勝手にcoolな計画で実行してくれる可能性も高いですが。

  where('NOT EXISTS(SELECT 1 FROM words WHERE words.entry = level.word)').

複雑目の条件をARのクエリインターフェースに食わせる手段として、EXISTS/NOT EXSITSなサブクエリはよく使いますね(パフォーマンス注意)。

編集 履歴 (1)
  • なるほど。結構生っぽさは残るのですね。[Squeel](https://github.com/ernie/squeel)を使うと、もう少しRubyっぽくなるのかな? ところでsqlite3を使っているのですが、EXISTSはないようです。がくっ http://www.sqlite.org/lang.html -
  • where('word NOT IN (SELECT entry from words)') は、where("word NOT IN (?)", Word.pluck(:entry)) でも良さそうでしょうか? このほうがややマシのような -
  • それだと2回クエリ投げますがそこにこだわらないならいいと思います。 -
w = Word.arel_table
Level.select(:word).where(level: 1).where(Level.arel_table[:word].not_in w.project(w[:entry])).limit(1)

とにかく生で書かずにという方向で書いてみましたがどうでしょうか。

上記をsqlite使用のrails console上で.to_sqlすると

SELECT  word FROM "levels"  WHERE "levels"."level" = 1 AND ("levels"."word" NOT IN (SELECT "words"."entry" FROM "words" )) LIMIT 1

となるのでおそらくお望みのカンジになってるのではないでしょうか。

編集 履歴 (0)
  • 確かに上手くいきました。そもそも私は Arel::Table が分かっていないようなので、もう少し調べてみます。ありがとうございました! -
ウォッチ

この質問への回答やコメントをメールでお知らせします。