在 Ecto 查询中使用变量引用命名绑定

Use variable to reference named binding in an Ecto Query

我正在尝试构建一个函数来搜索查询中给定 table 的字段中的术语。

对于像

这样的查询
initial_query = 
  Answer
  |> join(:left, [a], q in assoc(a, :question), as: :question)
  |> join(:left, [a, q], s in assoc(a, :survey), as: :survey)

我希望能够在 :question:survey 引用的 table 中进行搜索。

现在,此代码有效:

initial_query
|> or_where(
  [question: t], #:question hard coded
  fragment(
    "CAST(? AS varchar) ILIKE ?",
    field(t, ^field),
    ^"%#{search_term}%"
  )
)

但是,我想要一个将命名绑定作为参数的函数,但我找不到实现它的方法。

我的尝试:

defp search_field(initial_query, table, field, search_term) do
  initial_query
  |> or_where(
    [{table, t}],
    fragment(
      "CAST(? AS varchar) ILIKE ?",
      field(t, ^field),
      ^"%#{search_term}%"
    )
  )
end

报错

** (Ecto.Query.CompileError) unbound variable t in query. If you are attempting to interpolate a value, use ^var expanding macro: Ecto.Query.or_where/3

当这样调用时:

search_field(initial_query, :question, :text, search_text)

defp search_field(initial_query, table, field, search_term) do
  initial_query
  |> or_where(
    [{^table, t}],
    fragment(
      "CAST(? AS varchar) ILIKE ?",
      field(t, ^field),
      ^"%#{search_term}%"
    )
  )
end

给予

** (Ecto.Query.CompileError) binding list should contain only variables or {as, var} tuples, got: {^table, t} expanding macro: Ecto.Query.or_where/3


有没有办法在 Ecto 查询中使用变量来引用命名绑定?

诀窍是检索命名绑定在绑定中的位置。命名绑定存储在 %Ecto.Query{aliases: aliases} 字段中。

def named_binding_position(query, binding) do
  Map.get(query.aliases, binding)
end

def search_field(query, table, field, search_term) do
  position = named_binding_position(query, table)
  query
  |> or_where(
    [{t, position}],
    fragment(
      "CAST(? AS varchar) ILIKE ?",
      field(t, ^field),
      ^"%#{search_term}%"
    )
  )
end

我们首先在query.aliases中查找命名绑定的位置。然后使用这个位置来构建查询。

现在,当我们调用

Answer
|> join(:left, [a], q in assoc(a, :question), as: :question)
|> join(:left, [a, q], s in assoc(a, :survey), as: :survey)
|> search_field(:question, :text, "bogus")

它应该产生类似

的东西
#Ecto.Query<from a in Answer,
left_join: q in assoc(a, :question), as: :question,
or_where: fragment("CAST(? AS varchar) ILIKE ?", q.text, ^"%bogus%")>

值得注意的是,%Query.aliases 中的 {t, position} 元组引用命名绑定的位置是一个内部实现,没有记录。因此,可能会发生变化。有关详细信息,请参阅 https://github.com/elixir-ecto/ecto/issues/2832

所以这个问题的答案似乎是 Ecto 不支持这样做的方法。 @maartenvanvliet 解决方案效果很好,缺点是依赖内部实现。

我对这个问题的解决方案是让函数 search_field 始终在最后加入的 table 中搜索,使用 here 描述的 ... 语法:

# Searches for the `search_term` in the `field` in the last joined table in `initial_query`.
defp search_field(initial_query, field, search_term) do
  initial_query
  |> or_where(
    [..., t],
    fragment(
      "CAST(? AS varchar) ILIKE ?",
      field(t, ^field),
      ^"%#{search_term}%"
    )
  )
end

所以这个函数可以这样使用:

Answer
|> join(:left, [a], q in assoc(a, :question), as: :question)
|> search_field(:text, search_text)
|> join(:left, [a, q], s in assoc(a, :survey), as: :survey)
|> search_field(:title, search_text)

在我看来,它仍然读起来很好,缺点是要求我们能够更改 initial_query