10
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Elixir Ecto: 動的にカラム指定で select する

Last updated at Posted at 2015-11-11

概要

Ecto で動的に必要なカラムを指定する select を実装します。

動機

GraphQL のように取得するフィールドを指定できる API の場合、SQL のクエリーでも取得するカラムを制限した方がデータ転送量などで効率化できそうです。
※ キャッシュのヒットなどにも影響する可能性もあるので、楽観的な推測が入ってます。

API からの指定によりカラムを選択して SELECT するマクロを作成します。

Ecto.Query.select

Ecto でカラムを SELECT するには次のようになります。

Ecto.Query.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
[{:f, [], nil}]
id_title
{:%{}, [],
 [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 は次のように使う仕様とします。

select_ex
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"] を変数に代入しておいて、変数を渡すことでカラムを指定できます。

完成はこちらです。

queryex.ex
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

実行確認はこちらのサンプルコードでできます。

最後に

こういった内部の関数を呼び出して無理やり実現したときに、公開されたインタフェースでさくっと実現できる方法を見落としているのではないかと、常に気になります。

参考

10
9
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
10
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?