【问题标题】:Ecto join with dynamically built conditions具有动态构建条件的 Ecto 连接
【发布时间】:2018-06-01 03:22:21
【问题描述】:

我正在尝试使用左连接构建一个 Ecto 查询,该连接带有可选的额外条件。我将尝试用典型的帖子和 cmets 示例来描述它。

发表 has_many 评论 评论belongs_to Post。

假设评论有两个布尔字段,已批准和精选。

我想获取所有帖子,无论它们是否有 cmets,因此左连接。 我想要预加载 cmets,但最好是一个 SQL 查询。 我想选择性地过滤已批准和精选的 cmets。

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

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

这可行,但必须有更好的方法。如果我有第三个参数,那就太疯狂了。我正在寻找一种方法来为join()on 参数动态构建该列表。由于需要固定,我的尝试失败了。

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

【问题讨论】:

  • 为什么不使用警卫?比如def posts_with_comments(approved, featured, some_var) dodef posts_with_comments(approved, featured, some_var) when is_nil?(approved) do等等
  • @Felipe-Skinner 如果我对您的理解正确,那可能会奏效,但我试图避免在警卫或任何地方进行条件测试。

标签: elixir ecto


【解决方案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 的基于宏的解决方案工作。我还不是一个宏专家,但我不知道 nil 匹配在运行时是如何工作的。

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

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

但事实并非如此。它试图将 p.id 扩展为 p.id()

【讨论】:

    【解决方案2】:

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

    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 是允许 pin 运算符脱离上下文所必需的。

    或者,或者,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
    

    【讨论】:

    • 守卫也能正常工作还是宏只是解决这个问题的更好方法?
    • 感谢@mudasobwa,虽然我还没有多少运气。我只尝试了减少的第二个选项。我认为在另一端之前缺少end),它应该是defmacro,而不是defmacrop。之后,当我尝试使用 do_join(已批准:已批准)时,它仍然无法编译。 do_join(approved: approved) is not a valid query expression。我还尝试了一个非常简单的宏,它总是简单地返回 [post_id: p.id]
    • 对不起,我认为宏是一个错字,但我现在意识到它意味着私有宏,它需要在使用前在文件中。但是……还是不行。 (Ecto.Query.CompileError) Enum.reduce( ... is not a valid query expression。我真的不需要这个,所以现在它更像是一个学习练习,但让 reduce 版本工作以供将来使用会很有用。
    • 确实,defmacrop 应该返回 query 宏可接受的 AST,这就是为什么 Enum.reduce 不应该出现在那里。相反,Enum.reduce 应该在编译阶段 完成。我已经更新了答案。
    • @mudasobwa,仍然无法编译。我认为第一个end 缺少),但在修复它之后,它显示syntax error before: post_id。它似乎不喜欢 post_id: atom。我感谢您的努力,但可能不值得花更多时间。我对使用 dynamic 的解决方案非常满意。
    猜你喜欢
    • 1970-01-01
    • 2018-04-09
    • 2015-11-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-28
    • 1970-01-01
    • 2020-06-26
    相关资源
    最近更新 更多