LoginSignup
48
51

More than 5 years have passed since last update.

Railsで自由自在なSQLが組み上がるまで

Posted at

ActiveRecord / Arel

以下の続編としてお読みください
Railsの複雑な検索はスコープを使おう
RailsでSQL文をどんどん部品化していきます

今回DatabaseがRedshift(Postgresqlに似ている)です

Arel::Nodes::NamedFunction

DB固有の関数を使う場合などはこれ

# こんな感じのテーブルがあるよ
# create table timeaxis (time_s timestamp)
time_table = Arel::Table.new('timeaxis')
time_table.project(
  Arel::Nodes::NamedFunction.new(
    'date_trunc',
    [Arel::Nodes::build_quoted('day'),
     time_table[:time_s]]
  ).as('daily')
).group(
  Arel::Nodes::NamedFunction.new(
    'date_trunc',
    [Arel::Nodes::build_quoted('day'),
     time_table[:time_s]]
  )
)

# 得られるSQL
# SELECT
#   date_trunc('day', "time_table"."time_s") AS daily
# FROM
#   "time_table"
# GROUP BY
#   date_trunc('day', "time_table"."time_s")

タイムスタンプを日次にする例、scopeとして宣言しておけば、dayの部分を引数にして1時間ごと、月ごとなど変更は容易です

NamedFunctionは第一引数を関数名として、第二引数に配列(中身はArelオブジェクト)をとりカンマ区切りでつなぐようです。第二引数の配列内には2つを超えるオブジェクトも渡せます

build_quotedは文字列をシングルクオートで囲むみたい

PostgresqlのCASTをスコープに設定するなら

scope :cast, lambda { |str, type|
  Arel::Nodes::NamedFunction.new(
    'CAST', [
      Arel::Nodes::As.new(
        Arel::Nodes.build_quoted(str),
        Arel::Nodes::SqlLiteral.new(type)
      )
    ]
  )
}

# cast('20.2', 'float') => CAST('20.2' AS float)
# cast('1 HOUR', 'interval') => CAST('1 HOUR' AS interval)

文字列と変換する型を引数として渡せば型変換してくれます

Asは二つの引数をASでつないでくれるみたい
SqlLiteralは渡した文字列をArelオブジェクトにしてくれる

Arel::Nodes::InfixOperation

あまり使い道は見いだせませんが、こんなことも可能です

Arel::Nodes::NamedFunction.new(
  'SELECT',
  [Arel::Nodes::InfixOperation.new(
    '+',
    Arel::Nodes::NamedFunction.new(
      'CAST', [
        Arel::Nodes::As.new(
          Arel::Nodes.build_quoted('2015-08-01'),
          Arel::Nodes::SqlLiteral.new('TIMESTAMP')
        )
      ]
    ),
    Arel::Nodes::InfixOperation.new(
      '*',
      Arel::Nodes::NamedFunction.new(
        'GENERATE_SERIES', [0, 23]
      ),
      Arel::Nodes::NamedFunction.new(
        'CAST', [
          Arel::Nodes::As.new(
            Arel::Nodes.build_quoted('1 HOUR'),
            Arel::Nodes::SqlLiteral.new('INTERVAL')
          )
        ]
      )
    )
  )]
).as('hourly')

# 得られるSQL
# SELECT
#    (CAST('2015-08-01' AS TIMESTAMP) + GENERATE_SERIES(0, 23)
#    *
#    CAST('1 HOUR' AS INTERVAL))
# AS hourly

結局1時間ごとを得るのかよ...

InfixOperationは第一引数にオペレータをとり、第二引数と第三引数をつなぐみたい

意味は無いが、下記のようにも使える

Arel::Nodes::NamedFunction.new(
  'CAST', [
    Arel::Nodes::InfixOperation.new(
      'AS',
      Arel::Nodes.build_quoted('2015-08-01'),
      Arel::Nodes::SqlLiteral.new('TIMESTAMP')
    )
  ]
)

# CAST('2015-08-01' AS TIMESTAMP)

先述の通りAsを使うのが良いと思う
なにより、Arelでは第一引数をオペレータとして扱うので不具合の原因となりそうなので非推奨

まとめ

Arel::Nodesを利用すればArelで用意されていない関数なども使うことができるようになります
また本投稿では全てのモジュールを扱っておりませんので、下記を参考ください
Module: Arel::Nodes

48
51
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
48
51