在Elixir中撰写Ecto查询

时间:2018-10-07 05:59:49

标签: elixir phoenix-framework ecto

我已经从客户端传入的参数列表中创建了一个查询列表:

[
  #Ecto.Query<from v in Video, where: v.brand == ^"Cocacola">,
  #Ecto.Query<from v in Video, where: v.type == ^"can">
]

但是,我需要遍历此列表并组成一个查询,这些查询都是所有查询的累加。 (下一个查询的输入是当前查询,等等。)

有人可以为我指出正确的方法。不胜感激!

我知道我可以一个一个地组成查询。但是我从客户那里得到了一些参数,并且字段列表(品牌,类型,...等)很长,并且不想为每个字段单独查询。

2 个答案:

答案 0 :(得分:2)

除非您打开单个查询结构并经历其底层实现,否则既不可能也不建议像这样在Ecto中加入查询。相反,您应该尝试将它们分解并使其可组合。

Ecto使您轻松组合查询:

defmodule VideoQueries do
  import Ecto.Query

  def with_brand(query, brand) do
    where(query, [v], v.brand == ^brand)
  end

  def with_type(query, type) do
    where(query, [v], v.type == ^type)
  end

  def latest_first(query) do
    order_by(query, desc: :inserted_at)
  end
end

您可以像这样一起调用它们:

Video
|> VideoQueries.with_brand("Cocacola")
|> VideoQueries.with_type("can")
|> VideoQueries.latest_first



现在,假设您获得了MapKeyword List个查询参数,并且想要应用它们,仍然可以通过遍历键/值来一起调用它们在运行时。您可以构建一个为您完成此操作的过滤器方法:

# Assuming args are a Keyword List or a Map with Atom keys
def filter(query, args) do
  Enum.reduce(args, query, fn {k, v}, query ->
    case k do
      :brand -> with_brand(query, v)
      :type  -> with_type(query, v)
      _      -> query
    end
  end)
end

并且可以动态地组成这样的查询:

user_input = %{brand: "Cocacola", type: "can"}

Video
|> VideoQueries.filter(user_input)
|> Repo.all



更多读物:

答案 1 :(得分:1)

尽管我同意@sheharyar的观点,可组合查询是最好的方法,但有时我们需要超越最佳实践的解决方案。因此,如上所述,我将为您的问题提供答案。

不要让我的示例架构分散您的注意力。这只是我加载的用于测试解决方案的项目...

要检查查询结构,您可以尝试以下操作:

iex(128)> Map.from_struct(from(q in OneIosThemeGen.Themes.Entry, where: q.base_hex == ^base_hex))
%{
  assocs: [],
  distinct: nil,
  from: {"entries", OneIosThemeGen.Themes.Entry},
  group_bys: [],
  havings: [],
  joins: [],
  limit: nil,
  lock: nil,
  offset: nil,
  order_bys: [],
  prefix: nil,
  preloads: [],
  select: nil,
  sources: nil,
  updates: [],
  wheres: [
    %Ecto.Query.BooleanExpr{
      expr: {:==, [],
       [{{:., [], [{:&, [], [0]}, :base_hex]}, [], []}, {:^, [], [0]}]},
      file: "iex",
      line: 128,
      op: :and,
      params: [{"#E8EBED", {0, :base_hex}}]
    }
  ]
}

如您所见,where子句位于wheres字段中。它包含一个列表。

因此,我们可以从每个查询中提取wheres字段并连接列表。这就是我在下面演示的内容。

这里是组成多个查询的where子句的示例。它只能通过“和”在一起来处理where子句。

base_hex =  "#E8EBED"
name = "bodyText"

queries = [
  from(q in OneIosThemeGen.Themes.Entry, where: q.base_hex == ^base_hex),
  from(q in OneIosThemeGen.Themes.Entry, where: q.name == ^name)
]
build = fn queries -> 
  wheres = Enum.reduce(queries, [], fn %{wheres: wheres}, acc -> wheres ++ acc end)
  from(q in OneIosThemeGen.Themes.Entry)
  |> Map.put(:wheres, wheres)
end
query = build.(queries)
rows = Repo.all(query)

# sort function for result equality assertion
sort = fn list -> Enum.sort(list, & &1.id <= &2.id) end

# Single query for results equality test
combined_query = from(q in OneIosThemeGen.Themes.Entry, where: q.base_hex == ^base_hex and q.name == ^name)
rows_combined = Repo.all(combined_query)

# Test that the results are the same
sort.(rows) == sort.(rows_combined)
# true

# Now test that running the queries individually does not return the same results
rows3 = Enum.map(queries, &Repo.all/1) |> List.flatten()
sort.(rows3) != sort.(rows)
# true

IO.puts("length {rows, rows3}: " <> inspect({length(rows), length(rows3)}))
# length {rows, rows3}: {2, 5}

请注意,此解决方案依赖于Ecto查询的内部结构,这通常是一种不好的做法。它可能会在将来的Ecto更新中中断。但是,这是针对所提出的特定问题的一种潜在解决方案。