在 elixir 中使用 Ecto.Repo 时调用了哪些函数 'under the hood'

Which functions are called 'under the hood' when using Ecto.Repo in elixir

我正在尝试更好地了解 elixir 中的 Ecto 适配器。我已经开始尝试使用 Ecto.Adapters.Postgres 作为基础构建我自己的适配器。这似乎是一个不错的开始选择,因为它是 Phoenix 使用的默认适配器。

我现在可以在我自己的项目中使用我的适配器,方法是更新我项目的 repo 文件中的以下行...

defmodule UsingTestAdapter.Repo do
  use Ecto.Repo,
    otp_app: :using_test_adapter,
    adapter: TestAdapter  # <------ this line
end

目前它具有与 postgres 适配器相同的功能。我一直在尝试编辑 Ecto.Adapters.Postgres.Connection 中的一些功能,但我意识到它们并没有像我预期的那样工作。

例如 insert 函数实际上并不使用传递给 Repo.insert 的参数。

为了更清楚地想象我们有以下 table、Comments...

| id | comment |
| -- | ------- |

现在调用 Repo.insert(%Comments{comment: "hi"})

我想修改适配器,使其忽略传入的 "hi" 值,而是插入注释 "I am the adapter, and I control this database. Hahaha (evil laugh)"...

| id | comment                                                            |
| -- | ------------------------------------------------------------------ |
| 1  | I am the adapter and I control this database. Hahaha (evil laugh)" |

但是,insert 函数似乎并未实际将要存储的数据作为参数。

我对 ecto 适配器发生的事情的最初想法是,当用户调用其中一个 repo 函数时,它会调用 Ecto.Adapters.Postgres.Connection 模块中的相应函数。这似乎确实发生了,但在此之前似乎还发生了其他步骤。

如果有人对调用 Repo.insert(以及任何其他 Repo 函数)时调用的函数链有更好的理解,请在下面解释。

我有时间更深入地研究这个问题,觉得我现在有了更好的理解。

我将按顺序列出用户在 elixir 应用程序中调用 Repo.insert 时发生的步骤。

步骤 1. 调用 Repo.insert

AppName.Repo.insert(%AppName.Comments{comment: "hi"})

第 2 步。AppName.Repo 模块

defmodule AppName.Repo do
  use Ecto.Repo, otp_app: :app_name, adapter: adapter_name
end

(This is the default set up for a phoenix application)

The use Ecto.Repo allows for all the functions defined in that module to be used in the module that calls it. This means that when we call AppName.Repo.insert, it goes to our module, sees there is no function defined as insert, sees the use marco, checks that module, sees a function called insert and calls that function (this is not exactly how it works but I feel it explains it well enough).

步骤 3.Ecto.Repo 模块

def insert(struct, opts \ []) do
  Ecto.Repo.Schema.insert(__MODULE__, struct, opts)
end

Where function is defined

步骤 4.Ecto.Repo.模式模块

4.1

# if a changeset was passed in
def insert(name, %Changeset{} = changeset, opts) when is_list(opts) do
  do_insert(name, changeset, opts)
end

# if a struct was passed in
# This will be called in this example
def insert(name, %{__struct__: _} = struct, opts) when is_list(opts) do
  do_insert(name, Ecto.Changeset.change(struct), opts)
end

Where function is defined

此步骤确保以变更集的形式传递给 do_insert 的数据。

4.2

do_insert(name, Ecto.Changeset.change(struct), opts)

Not pasting whole function as it is very long. Where function is defined

此函数执行大量数据操作并检查错误。如果一切顺利,它最终会调用 apply 函数

4.3

defp apply(changeset, adapter, action, args) do
  case apply(adapter, action, args) do # <---- Kernel.apply/3
    {:ok, values} ->
      {:ok, values}
    {:invalid, _} = constraints ->
      constraints
    {:error, :stale} ->
      opts = List.last(args)

      case Keyword.fetch(opts, :stale_error_field) do
        {:ok, stale_error_field} when is_atom(stale_error_field) ->
          stale_message = Keyword.get(opts, :stale_error_message, "is stale")
          changeset = Changeset.add_error(changeset, stale_error_field, stale_message, [stale: true])

          {:error, changeset}

        _other ->
          raise Ecto.StaleEntryError, struct: changeset.data, action: action
      end
  end
end

Where function is defined

apply/4 函数使用 modulefunction namearguments 调用 Kernel.apply/3 函数。在我们的例子中,模块是 AdapterName,函数是 :insert

这是我们的适配器发挥作用的地方 :D(终于)。

步骤 5.适配器名称

上面的 apply/3 函数调用将我们带到我们创建的适配器。

defmodule AdapterName do
  # Inherit all behaviour from Ecto.Adapters.SQL
  use Ecto.Adapters.SQL, driver: :postgrex, migration_lock: "FOR UPDATE"
end

此模块中没有定义插入函数,但因为它是“usingEcto.Adapters.SQL接下来让我们看一下此模块。

第 6 步。Ecto.Adapters.SQL 模块

defmodule Ecto.Adapters.SQL do

...

      @conn __MODULE__.Connection

...

      @impl true
      def insert(adapter_meta, %{source: source, prefix: prefix}, params,
                 {kind, conflict_params, _} = on_conflict, returning, opts) do
        {fields, values} = :lists.unzip(params)
        sql = @conn.insert(prefix, source, fields, [fields], on_conflict, returning)
        Ecto.Adapters.SQL.struct(adapter_meta, @conn, sql, :insert, source, [], values ++ conflict_params, kind, returning, opts)
      end

...
end

@conn 定义为 module attribute and is just the current calling module (MODULE) + .Connection.

5 点中讨论的调用模块是 AdapterName

这意味着在 insert 函数中,以下行...

@conn.insert(prefix, source, fields, [fields], on_conflict, returning)

相同
AdapterName.Connection.insert(prefix, source, fields, [fields], on_conflict, returning)

由于我们的 adapterpostgres adapter 相同,它会将我们带到下一个函数。

第 7 步。AdapterName.Connection

def insert(prefix, table, header, rows, on_conflict, returning) do
  values =
    if header == [] do
      [" VALUES " | intersperse_map(rows, ?,, fn _ -> "(DEFAULT)" end)]
    else
      [?\s, ?(, intersperse_map(header, ?,, &quote_name/1), ") VALUES " | insert_all(rows, 1)]
    end

  ["INSERT INTO ", quote_table(prefix, table), insert_as(on_conflict),
   values, on_conflict(on_conflict, header) | returning(returning)]
end

Where the function is defined

为了在已经太长的答案中保留一些文字,我不会详细介绍。这个函数实际上并没有接受我们传递给 Repo.insert 的参数(回到第一组)。

如果我们想编辑参数,我们需要在 AdapterName 模块中进行。我们需要定义自己的insert函数,使其不再调用第6步中定义的insert函数。

第 8 步。AdapterName - 定义我们自己的插入。

为了简单起见,我们只将步骤 6 中定义的 insert 复制到我们的 AdapterName 模块中。然后我们可以修改该函数以更新我们认为合适的参数。

如果我们这样做,我们最终会得到一个类似于...

的函数
  def insert(adapter_meta, %{source: source, prefix: prefix}, params, on_conflict, returning, opts) do
    Keyword.replace!(params, :comment, "I am the adapter and I control this database. Hahaha (evil laugh)") # <---- changing the comment like we wanted :D

    {kind, conflict_params, _} = on_conflict
    {fields, values} = :lists.unzip(params)
    sql = @conn.insert(prefix, source, fields, [fields], on_conflict, returning)
    Ecto.Adapters.SQL.struct(adapter_meta, @conn, sql, :insert, source, [], values ++ conflict_params, kind, returning, opts)
  end

这现在插入了我们最初想要的不同值。

希望有人觉得这有帮助。