概要
Ecto で動的に必要なカラムを指定する select を実装します。
動機
GraphQL のように取得するフィールドを指定できる API の場合、SQL のクエリーでも取得するカラムを制限した方がデータ転送量などで効率化できそうです。
※ キャッシュのヒットなどにも影響する可能性もあるので、楽観的な推測が入ってます。
API からの指定によりカラムを選択して SELECT するマクロを作成します。
Ecto.Query.select
Ecto でカラムを SELECT するには次のようになります。
feed = RssFeed
|> Query.select([f], %{ id: f.id, title: f.title })
|> Query.limit([u], 1)
|> Repo.all
[f], %{ id: f.id, title: f.title }
の部分を動的に生成すれば、必要な機能は実装できます。
しかし、 Ecto.Query.select
はマクロで、[f]
や %{ id: f.id, title: f.title }
は list や map ではなく、list や map の形をした Ecto の DSL です。(参考)
つまり、Ecto.Query.select
の外で cols = `%{ id: f.id, title: f.title }
のようなことをやったところでエラーになります。
そこで、Ecto.Query.select
の中で呼んでいる Ecto.Query.Builder.Select.build
に AST を渡して実現することにしました。
パラメータの AST
[f]
と %{ id: f.id, title: f.title }
の AST は次のようになります。
[{:f, [], nil}]
{:%{}, [],
[id: {{:., [], [{:f, [], nil}, :id]}, [], []},
title: {{:., [], [{:f, [], nil}, :title]}, [], []}]}
[f]
は固定値なので動的に生成する必要ありません。
%{ id: f.id, title: f.title }
は id
なら [id: {{:., [], [{:f, [], nil}, :id]}, [], []}]
を作ってマージしていけばよく、動的に作るのはキーの id:
と値のタプルに含まれる :id
の2つの Atom のみです。
拡張版 select マクロ
拡張版の select
は次のように使う仕様とします。
cols = ["id", "title"]
feed = RssFeed
|> QueryEx.select(cols)
|> Query.limit([u], 1)
|> Repo.all
Query.select([f], %{ id: f.id, title: f.title })
の代わりに QueryEx.select(["id", "title"])
に置き換えるだけで Ecto.Query.select
と同じように動作します。
また、["id", "title"]
を変数に代入しておいて、変数を渡すことでカラムを指定できます。
完成はこちらです。
defmodule QueryEx do
defmacro select(query, cols) do
quote do
Ecto.Query.Builder.Select.build(unquote(query), [{:f, [], nil}], QueryEx.make_select(unquote(cols)), __ENV__)
|> Code.eval_quoted
|> elem(0)
end
end
def make_select(cols) do
{:%{}, [], make_col([], cols)}
end
defp make_col(cols, [col|tl]) do
cols = Keyword.put(cols, String.to_atom(col), {{:., [], [{:f, [], nil}, String.to_atom(col)]}, [], []})
make_col(cols, tl)
end
defp make_col(cols, []) do
cols
end
end
実行確認はこちらのサンプルコードでできます。
最後に
こういった内部の関数を呼び出して無理やり実現したときに、公開されたインタフェースでさくっと実現できる方法を見落としているのではないかと、常に気になります。