ecto:来自运行时值的动态片段

时间:2019-03-25 01:22:44

标签: postgresql elixir ecto

我正在构建一个API,允许最终用户在数据列表上指定排序,并返回分页结果。例如,API请求可能指定一种name: :asc, id: :ascage: :desc, id: :desc。在实现中,我尝试使用row-value syntax从现有的Ecto查询中选择后续页面。

在Rails中,这很容易-只需插入一个字符串并使用一些参数:

# @param [ActiveRecord::Relation] rel
def slice_query(rel, cursor_params)
  # A little hand-waving
  operator, comparisons = parse_params(cursor_params)
  # Suppose `operator` is `<` and comparisons looks like `{name: "Last Row Value", id: "last-row-value"}
  fragment = "(#{comparisons.keys.join(',')}) #{operator} (#{comparisons.size.times.map { '?' }.join(',')})"
  # fragment in this case should look like "(name, id) < (?, ?)"
  rel.where(fragment, *comparisons.values)
end

作为Ecto / Elixir的新手,我不知道如何使用Ecto做到这一点。这些文档似乎表明可以通过宏来实现自定义片段,但是我对如何将运行时值动态插值到基于宏的实现中一无所知。

我尝试过这样的事情:

def slice_query(query, cursor_params)
  # More hand-waving; `parse_params` is a bit of pseudocode.
  # Full source is below
  {operator, comparisons} = parse_params(cursor_params)
  query |> where(row_value_comparison(operator, comparisons)
end

特别地,operator是二进制字符串,而comparisonsEnum.map的结果。

使用row_value_comparison from here,但是我遇到了各种编译器投诉,具体取决于我如何(或不插入)operatorcomparisons到该宏调用中:

  • query |> where(row_value_comparison(operator, comparisons)产生protocol Enumerable not implemented for {:comparisons, [line: 114], nil}
  • query |> where(row_value_comparison(operator, ^comparisons)产生protocol Enumerable not implemented for {:^, [line: 114], [{:comparisons, [line: 114], nil}]}
  • query |> where(row_value_comparison(operator, unquote(comparisons)))产生variable "comparisons" does not exist and is being expanded to "comparisons()", please use parentheses to remove the ambiguity or change the variable name
  • query |> where(row_value_comparison(comparator, unquote(^comparisons)))产生cannot use ^comparisons outside of match clauses

完整的模块源,如注释中所述:

defmodule MyProject.Resolvers.Connection do
  import Ecto.Query
  alias MyProject.Repo

  # `cursor_params` should be a Map containing either
  # :after (and optionally :first) (to paginate forward), or
  # :before (and optionally :last) (to paginate backwards).
  # :after or :before should be parsed Cursors.
  #
  # This method implements ApplyCursorsToEdges according to the spec:
  # https://facebook.github.io/relay/graphql/connections.htm#sec-Arguments
  def paginate_with_results(query, cursor_params) do
    total_count = query_count(query)
    query = slice_query(query, cursor_params)
    {has_previous_page, has_next_page} = query_page_info(query, cursor_params)
    query = limit_results(query, cursor_params)

    %{
      results: Repo.all(query),
      page_info: %{
        total_count: total_count,
        has_next_page: has_next_page,
        has_previous_page: has_previous_page
      }
    }
  end

  defp order_bys(query) do
    query.order_bys
    |> Enum.map(fn %Ecto.Query.QueryExpr{expr: expr} ->
      expr
      |> Enum.map(fn {dir, ast} ->
        {dir,
         ast
         |> Macro.expand(__ENV__)
         |> Macro.to_string()
         |> String.replace_prefix("&0.", "")
         |> String.replace_suffix("()", "")
         |> String.to_atom()}
      end)
    end)
    |> List.first()
  end

  defp single_sort_direction?(query) do
    directions = order_bys(query) |> Enum.map(fn {dir, _} -> dir end) |> Enum.uniq()
    if Enum.count(directions) == 1, do: Enum.at(directions, 0), else: false
  end

  defp query_count(query) do
    query |> select([x], count(x.id)) |> Repo.one()
  end

  # https://elixirforum.com/t/building-an-ecto-macro-to-generate-a-row-value/19680/2
  defmacro row_value_comparison(operation, comparisons) do
    # when is_binary(operation) and is_list(comparisons) do
    question_marks = Enum.map(comparisons, fn _ -> "?" end) |> Enum.join(", ")
    frag_text = "(#{question_marks}) #{operation} (#{question_marks})"

    frag_arguments =
      comparisons
      |> Enum.unzip()
      |> Tuple.to_list()
      |> List.flatten()

    quote do
      fragment(unquote(frag_text), unquote_splicing(frag_arguments))
    end
  end

  defp slice_query(query, cursor_params) do
    sort_descriptors = order_bys(query)

    query =
      if Map.has_key?(cursor_params, :after) do
        after_cursor = cursor_params.after

        if direction = single_sort_direction?(query) do
          comparator = if direction == :asc, do: ">", else: "<"

          comparisons =
            Enum.map(sort_descriptors, fn {_dir, field} ->
              {field, after_cursor.sort_values[field]}
            end)

          query |> where(row_value_comparison(comparator, unquote(comparisons)))
        else
          # TODO: Handle multi-direction sort
          query
        end
      end
    # Etc
  end
end

0 个答案:

没有答案