[Elixir ORM #2] `~>` 和 `<~` 的实现

首先列一下需求吧:

  1. C/S 模式,需要在当前 node 调用,同时需要在其它 node 通过 :rpc.call 调用
  2. 在服务端定义的 User 等 module 对应的表,不需要在每个 client 重新定义
  3. 不希望占用 where order insert update 等常用函数
  4. 不喜欢 LINQ

所以使用 ~>  和<~  只是因为我实再找不到其它的办法能满足上面的需求。

实现过程中,第一步肯定是调研。我先测试了一下 quote do: User ~> where(id: 1) ,结果是{:~>, [], [{:__aliases__, [alias: false], [:User]}, {:where, [], [[id: 1]]}]}这说明现 ~> 的优先级还是比较高的,有足够的发挥空间,这么实现是可行的。

接下来是实现,<~ 是相对简单的,因为 <~ 不会嵌套,所以可以很容易地如下实现:

defmacro left <~ {:ins, _, [arg]} do
  quote do
    Ench.Action.insert(unquote(left), unquote(arg))
  end
end

defmacro left <~ {:del, _, nil} do
  quote do
    Ench.Action.delete(unquote(left))
  end
end

defmacro left <~ {:set, _, [arg]} do
  quote do
    Ench.Action.update(unquote(left), unquote(arg), %{})
  end
end

defmacro left <~ {:inc, _, [arg]} do
  quote do
    Ench.Action.update(unquote(left), %{}, unquote(arg))
  end
end

defmacro left <~ {:update, _, [set, inc]} do
  quote do
    Ench.Action.update(unquote(left), unquote(set), unquote(inc))
  end
end

而 ~> 是多级的,quote 之后是这个样子的( [10]在打印的时候会显示为 '\n'

iex(1)> quote do: User ~> limit(10) ~> all
{:~>, [],
 [{:~>, [], [{:__aliases__, [alias: false], [:User]}, {:limit, [], '\n'}]},
  {:all, [], Elixir}]}

这里我最大的疑惑就是「这段递归要在编译的时候做还是在运行的时候做」,于是赶紧去看下 elixir 里|> 的实现,找到了这段代码 https://github.com/elixir-lang/elixir/blob/v1.1/lib/elixir/lib/macro.ex#L66-L115,这段代码给了一个明确的答案,应该在编译的时候做,就是说,在代码里写的 f(g(a))  和 a|> g |> f 在编译之后,是完全一样的。

defmacro left ~> right do
  [h | t] = unpipe({:~>, [], [left, right]})
  List.foldl(t, h, fn(expr, acc) ->
    pipe(acc, expr)
  end)
end


defp unpipe(expr) do
  unpipe(expr, []) |> Enum.reverse
end

defp unpipe({:~>, _, [left, right]}, acc) do
  unpipe(right, unpipe(left, acc))
end

defp unpipe(other, acc) do
  [other | acc]
end


defp pipe({:__aliases__, _, m}, {query, line, args}) do
  module = Module.concat(m)
  query_struct = Ench.Query.init(module) |> Macro.escape
  call = {:., [], [{:__aliases__, [alias: false], [:Ench, :Query]}, query]}
  {call, line, [query_struct | args || []]}
end

defp pipe(left, {call, line, args}) do
  {pipe_call, _, _} = quote do: Ench.DSL.do_pipe
  {pipe_call, line, [left, call, args]}
end
  
  
def do_pipe(%Ench.Query{}=query, call, args)
when call in [
  :where, :offset, :limit, :order, :first, :all, :count,
  :find, :find!, :find_by, :find_by!,
] do
  apply(Ench.Query, call, [query | args || []])
end

def do_pipe(module, call, args) when is_atom(module) do
  apply(Ench.Query, call, [Ench.Query.init(module) | args || []])
end

代码长得很像 Macro 里的 pipe 和 unpipe,只是针对需求改了下。

 

这样基本上,ORM 的部分就可以完结了,再详细看下其它部分的实现,有没有值得写的,然后,会再写一个「总结」。所以,待续。