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

时间:2016-12-23 12:58:17

标签: mysql elixir ecto arity

我想执行如下查询:

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

FIELD是MySQL特定的函数,'B', 'A', 'D', 'E', 'C'是来自List的值。

我尝试使用fragment,但它似乎不允许仅在运行时中知道动态arity。

除了使用Ecto.Adapters.SQL.query使用完全原始版本,有没有办法使用Ecto的查询DSL来处理它?<​​/ p>

编辑:这是第一种天真的方法,当然不起作用:

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))

4 个答案:

答案 0 :(得分:3)

ORM很棒,直到他们leak。一切都做到了。 Ecto很年轻(fe,它只能获得OR一起使用30 days ago条款的能力,所以它还不够成熟,不足以开发出考虑高级SQL回转的API。

调查可能的选项,您并不是唯一的请求。 Ecto issue #1485 StackOverflowElixir Forum上提到了无法理解片段中的列表(无论是order_by还是where还是其他任何地方的一部分)这个blog post。后者特别有启发性。更多关于这一点。首先,让我们尝试一些实验。

实验#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))

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

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

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

实验#4:这让我想到了我提到的那篇博文。在其中,作者根据条件的数量使用一组预定义的宏来解决缺少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&#34;与谷物&#34;可以这么说,它要求你有一个有限的,可管理的可用字段数。这可能会也可能不会改变游戏规则。

我的建议:不要试图解决ORM的漏洞。你知道最好的查询。如果ORM不接受它,请直接用原始SQL编写它,并记录ORM不起作用的原因。将其屏蔽在功能或模块后面,以便您可以保留未来更改其实施的权利。有一天,当ORM赶上时,你可以很好地重写它,而不会影响系统的其他部分。

答案 1 :(得分:1)

创建一个包含2列的表:

B 1
A 2
D 3
E 4
C 5

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

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

答案 2 :(得分:1)

我会尝试使用以下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 
;

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

答案 3 :(得分:0)

这实际上使我发疯,直到我发现(至少在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))