在Elixir中使用Ecto.Repo时哪些功能被称为“幕后”

时间:2019-02-18 16:55:18

标签: postgresql elixir phoenix-framework ecto

我正在努力更好地理解长生不老药中的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中的参数。

为使这一点更加清晰,请想象一下,我们有下表Comments ...

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

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

我想修改适配器,以便它忽略传入的“ hi”值,而是插入注释“我是适配器,并且我控制此数据库。哈哈哈(恶笑)” ...

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

但是,insert函数似乎并没有真正将要存储的数据用作参数。

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

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

1 个答案:

答案 0 :(得分:1)

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

我将按顺序列出当用户在长生不老药应用程序中调用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
  

(这是phoenix应用程序的默认设置)

     

use Ecto.Repo允许在调用它的模块中使用该模块中定义的所有功能。这意味着,当我们调用AppName.Repo.insert时,它会转到我们的模块,看到没有定义为insert的函数,看到了use marco,检查了该模块,看到了一个名为insert的函数,然后调用该函数(这并不完全是它的工作方式,但我认为它已经很好地解释了这一点)。

第3步。Ecto.Repo模块

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

Where function is defined

第4步。Ecto.Repo.Schema模块

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)
  

不粘贴整个函数,因为它很长。   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步。AdapterName

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

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

此模块中未定义插入函数,但由于它是“ 使用Ecto.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,仅仅是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

现在这将插入我们最初想要的其他值。

希望有人发现这有帮助。