Ecto加入动态构建的条件

时间:2018-06-01 03:22:22

标签: elixir ecto

我正在尝试使用左连接构建一个Ecto查询,并在连接上添加可选的额外条件。我将尝试用典型的帖子和评论示例来描述它。

发布has_many评论 评论belongs_to Post。

让我们说评论有两个布尔字段,已批准并有特色。

我想获得所有帖子,无论他们是否有评论,因此左连接。 我希望预加载注释,但最好是一个SQL查询。 我想选择过滤已批准和有特色的评论。

我正在尝试编写类似这样的函数,如果批准或特征不是nil,它们将被包含在连接中,如果它们是nil,它们将被忽略。我还没有找到比这样更好的方法:

def posts_with_comments(approved, featured, some_var) do
  query = Post
  |> where([p], p.some_field == ^some_var

  cond do
    !is_nil(approved) and !is_nil(featured)
      -> join(query, :left, [p], c in Comment, [post_id: p.id, approved: ^approved, featured: ^featured])

    !is_nil(approved)
      -> join(query, :left, [p], c in Comment, [post_id: p.id, approved: ^approved])

    !is_nil(featured)
      -> join(query, :left, [p], c in Comment, [post_id: p.id, featured: ^featured])

    true -> join(query, :left, [p], c in Comment, [post_id: p.id])
  end

  |> preload([p, c], [comments: c])
  |> select([p], p)
  |> Repo.all

end

这有效但必须有更好的方法。如果我有第三个参数,它会变得疯狂。我正在寻找一种方法来为on join()参数动态构建该列表。由于要求确定,我尝试这样做的尝试失败了。

我无法将这些条件放在where中,因为如果我执行where t.approved == true之类的操作,我只会获得批准的帖子。

2 个答案:

答案 0 :(得分:1)

我认为答案是使用dynamic函数。

这很有效。 (省略了我之前的some_var条件。)

def posts_with_comments(approved, featured) do
  query = Post
  join(query, :left, [p], c in Comment, ^do_join(approved, featured))
  |> preload([p, c], [comments: c])
  |> Repo.all
end

defp do_join(approved, featured) do
  dynamic = dynamic([p, c], c.post_id == p.id)

  dynamic =
  case approved do
    nil -> dynamic
    _ -> dynamic([p, c], ^dynamic and c.approved == ^approved)
  end

  case featured do
    nil -> dynamic
    _ -> dynamic([p, c], ^dynamic and c.featured == ^featured)
  end
end

这比我的第一次尝试要好得多,因为它是一个简单的连接,只是在更多条件而不是条件爆炸的情况下变得更长。

作为一项练习,我不能通过提供一个字段列表并使用像reduce这样的东西来使它更通用。我遇到的问题是使字段名称(例如,c.approved)从变量中起作用。

join似乎支持两种on参数。关键字列表(我假设暗示==)和更具表现力的格式。 dynamic似乎不适用于关键字列表。它试图将p.id扩展为p.id()。

我无法获得@ mudasobwa基于宏的解决方案。我还不是一个宏观专家,但我不知道零比赛在运行时是如何运作的。

关于宏解决方案还有一件事。出于某种原因,它也不适用于关键字列表。我希望像这样的裸骨宏可以工作:

defmacrop do_join do
  quote do
    [post_id: p.id]
  end
end

但它没有。它试图将p.id扩展为p.id()

答案 1 :(得分:0)

我会在其中声明一个帮助器和模式匹配参数:

def posts_with_comments(approved, featured, some_var) do
  query = Post
          |> where([p], p.some_field == ^some_var)
          |> join(:left, [p], c in Comment, do_join(approved, featured))
          |> preload([p, c], [comments: c])
          |> select([p], p)
          |> Repo.all
end

defmacrop do_join(nil, nil) do
  quote do: [post_id: p.id]
end
defmacrop do_join(approved, nil) do
  quote bind_quoted: [approved: approved] do
    [post_id: p.id, approved: ^approved]
  end
end
defmacrop do_join(nil, featured) do
  quote bind_quoted: [featured: featured] do
    [post_id: p.id, featured: ^featured]
  end
end
defmacrop do_join(approved, featured) do
  quote bind_quoted: [approved: approved, featured: featured] do
    [post_id: p.id, approved: ^approved, featured: ^featured]
  end
end

defmacro是必要的,以允许引脚操作符脱离上下文。

或者,Enum.reduce/3它:

# kw is approved: approved, featured: featured
defmacrop do_join(kw) do
  initial = [{:post_id, {{:., [], [{:p, [], Elixir}, :id]}, [], []}}]
  Enum.reduce(kw, initial, fn
    {_, nil}, acc -> acc
    {k, _}, acc ->
      quoted = {k, {:^, [], [{k, [], Elixir}]}}
      [quoted | acc]
  end)
end