我正在构建一个API,允许最终用户在数据列表上指定排序,并返回分页结果。例如,API请求可能指定一种name: :asc, id: :asc
或age: :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
是二进制字符串,而comparisons
是Enum.map
的结果。
使用row_value_comparison
from here,但是我遇到了各种编译器投诉,具体取决于我如何(或不插入)operator
和comparisons
到该宏调用中:
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