Ecto 查询和自定义 MySQL 具有可变参数的函数

Ecto query and custom MySQL function with variable arity

我想执行如下查询:

SELECT id, name
FROM mytable
ORDER BY FIELD(name, 'B', 'A', 'D', 'E', 'C')

FIELD 是一个 MySQL 特定函数,'B', 'A', 'D', 'E', 'C' 是来自列表的值。

我尝试使用 fragment,但它似乎不允许仅在运行时已知的动态元数。

除了使用 Ecto.Adapters.SQL.query 进行全原始处理外,有没有办法使用 Ecto 的查询 DSL 来处理这个问题?

编辑:这是第一种天真的方法,当然行不通:

ids = [2, 1, 3] # this list is of course created dynamically and does not always have three items
query = MyModel
        |> where([a], a.id in ^ids)
        |> order_by(fragment("FIELD(id, ?)", ^ids))

创建一个包含 2 列的 table:

B 1
A 2
D 3
E 4
C 5

然后 JOIN LEFT(name, 1) 到它并得到序数。然后按那个排序。

(抱歉,我无法帮助 Elixir/Ecto/Arity。)

我会尝试使用以下 SQL SELECT 语句解决此问题:

[注意:现在无法访问系统来检查语法的正确性,但我认为可以]

SELECT A.MyID , A.MyName
  FROM (
         SELECT id                                   AS MyID            , 
                name                                 AS MyName          , 
                FIELD(name, 'B', 'A', 'D', 'E', 'C') AS Order_By_Field
           FROM mytable
       ) A
  ORDER BY A.Order_By_Field 
;

请注意列表 'B','A',... 可以作为数组或任何其他方法传递并替换上面代码示例中的内容。

ORM 很棒,直到它们 leak. All do, eventually. Ecto is young (f.e., it only gained ability to OR where clauses together 30 days ago),所以它还不够成熟,无法开发出考虑高级 SQL 旋转的 API。

调查可能的选项,您并不是唯一一个提出请求的人。 Ecto issue #1485, on , on the Elixir Forum and this blog post 中提到了无法理解片段中的列表(无论是作为 order_bywhere 的一部分还是其他任何地方)。后者特别有启发性。稍后会详细介绍。首先,让我们尝试一些实验。

实验 #1: 可能首先尝试使用 Kernel.apply/3 将列表传递给 fragment,但这行不通:

|> order_by(Kernel.apply(Ecto.Query.Builder, :fragment, ^ids))

实验 #2: 那么也许我们可以用字符串操作来构建它。如何给 fragment 一个运行时内置的字符串,其中包含足够的占位符以便从列表中提取:

|> order_by(fragment(Enum.join(["FIELD(id,", Enum.join(Enum.map(ids, fn _ -> "?" end), ","), ")"], ""), ^ids))

给定 ids = [1, 2, 3] 会产生 FIELD(id,?,?,?)。不行,这也不行。

实验 #3: 从 ID 创建完整的最终 SQL,将原始 ID 值直接放入组合字符串中。除了可怕之外,它也不起作用:

|> order_by(fragment(Enum.join(["FIELD(id,", Enum.join(^ids, ","), ")"], "")))

实验 #4: 这让我想到了我提到的那个博客 post。在其中,作者使用一组基于条件数量的预定义宏来解决 or_where 的不足:

defp orderby_fragment(query, [v1]) do
  from u in query, order_by: fragment("FIELD(id,?)", ^v1)
end
defp orderby_fragment(query, [v1,v2]) do
  from u in query, order_by: fragment("FIELD(id,?,?)", ^v1, ^v2)
end
defp orderby_fragment(query, [v1,v2,v3]) do
  from u in query, order_by: fragment("FIELD(id,?,?,?)", ^v1, ^v2, ^v3)
end
defp orderby_fragment(query, [v1,v2,v3,v4]) do
  from u in query, order_by: fragment("FIELD(id,?,?,?)", ^v1, ^v2, ^v3, ^v4)
end

虽然可以这么说并且使用 ORM "with the grain",但它要求您拥有有限的、可管理的可用字段数量。这可能会或可能不会改变游戏规则。


我的建议:不要试图解决 ORM 的漏洞。你知道最好的查询。如果ORM不接受,直接写raw SQL,并记录为什么ORM不工作。将它隐藏在函数或模块之后,这样您就可以保留将来更改其实现的权利。有一天,当 ORM 赶上来时,您就可以很好地重写它,而不会影响系统的其余部分。

这实际上让我发疯,直到我发现(至少在 MySQL 中)有一个 FIND_IN_SET 函数。语法有点奇怪,但它不带可变参数,所以你应该可以这样做:

ids = [2, 1, 3] # this list is of course created dynamically and does not always have three items
ids_string = Enum.join(ids, ",")
query = MyModel
        |> where([a], a.id in ^ids)
        |> order_by(fragment("FIND_IN_SET(id, ?)", ^ids_string))