[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 里有两个语法糖:
- 用
expose
宏定义的「属性」,通过user[:resume]
使用 - 用
def
定义的函数,通过user ~> collect_job(1)
使用
expose 有两个参数:
:lazy
此参数用来标注属性返回变量时直接计算,还是使用时计算: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_one
和 has_many
这两个语法糖,最初的设计和大多数的 ORM 一样通过它们来引用外键。后来还是感觉太复杂,因为我需要的外键用法和 expose
定义的属性完全一样,实现也完全一样,只是一个函数,这种情况下引入额外的 DSL 没有优势,就把它们去掉了,统一用 expose
就好。
expose
的本质也是定义函数,和 def
基本相同,虽然不推荐,但也是可以通过 ~>
来调用的。所以使用的时候还是从语义上区分:用 expose
定义「属性」,为读操作;用 def
来定义「动作」,为写操作。
待续