[Elixir ORM #1] Repo & Model 的 DSL 设计

Ench Repo 逻辑相当简单,只是读配置,用 pooler 起一个池,再给自己定义个 &__using__/1 让 Model 可以 use 它就好了。

和 Ecto 最大的区别,把 Repo 和 Model 做了绑定,一个 Model 只属于一个 Repo,所以调用的时候只需要 User ~> all 而不再是 User |> Repo.all ,代码如下:

defmodule Ench.Model.Repo do
  defmacro __using__(_) do
    quote do
      @config Application.get_env(:ench, __MODULE__)

      def child_spec do
        :pooler.pool_child_spec([
          name: __MODULE__,
          init_count: @config[:poolsize],
          max_count: @config[:poolsize],
          start_mfa: {
            @config[:adapter], :start_link,
            [@config |> Dict.drop [:adapter, :poolsize]]
          }
        ])
      end

      def __adapter__ do
        @config[:adapter]
      end

      defmacro __using__([table: table_name]) do
        quote do
          use Ench.Model.Table

          def __table__ do
            unquote(table_name)
          end

          def __repo__ do
            unquote(__MODULE__)
          end
        end
      end

    end
  end
end

接下来是 Model 里 expose 的代码:

defmacro expose(field, options \\ [], [do: block]) do
  lazy = Keyword.get(options, :lazy, false)
  arg = Keyword.get(options, :with, Macro.var(:_, nil))
  [ quote do
      if unquote(lazy) do
        @implicit unquote(field)
      else
        @explicit unquote(field)
      end
    end,

    quote do
      def unquote(field)(unquote(arg)) do
        unquote(block)
      end
    end
  ]
end

Model 里有两个语法糖:

  1. expose 宏定义的「属性」,通过 user[:resume] 使用
  2. def  定义的函数,通过 user ~> collect_job(1)  使用

expose 有两个参数:

  1. :lazy此参数用来标注属性返回变量时直接计算,还是使用时计算
  2. :with此参数就是个变量名,在不需要用到的时候,用 _  消除编译警告

剩下的就只有处理[]这个语法糖了,这个在 Elixir 1.0 中通过 Access Protocol 实现, Elixir 1.1 通过 Dict behaviour 实现,数据结构和代码结构如下(Elixir 1.1 版本):

%Ench.Result{
  __repo__: User,
  explicit: %{id: 1, name: falood},
  implicit: [:resume],
}

defmodule Ench.Result do
  use Dict

  defstruct __repo__: nil,
            explicit: %{},
            implicit: []

  def fetch(%EnchAgent.Result{explicit: explicit, implicit: implicit}=r, key) do
    cond do
      key in implicit ->
        {:ok, Ench.Action.call_model(r, key, [])}
      Dict.has_key?(explicit, key) ->
        {:ok, Dict.get(explicit, key)}
      true ->
        raise "key not found"
    end
  end
end

开发过程中,曾经考虑过 has_onehas_many 这两个语法糖,最初的设计和大多数的 ORM 一样通过它们来引用外键。后来还是感觉太复杂,因为我需要的外键用法和 expose 定义的属性完全一样,实现也完全一样,只是一个函数,这种情况下引入额外的 DSL 没有优势,就把它们去掉了,统一用 expose 就好。

expose 的本质也是定义函数,和 def 基本相同,虽然不推荐,但也是可以通过 ~> 来调用的。所以使用的时候还是从语义上区分:用 expose 定义「属性」,为读操作;用 def 来定义「动作」,为写操作。

待续