【问题标题】:Which functions are called 'under the hood' when using Ecto.Repo in elixir在 elixir 中使用 Ecto.Repo 时,哪些函数被称为“幕后”
【发布时间】:2019-02-24 05:31:05
【问题描述】:

我正在尝试更好地了解 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 的参数。

为了更清楚一点,假设我们有下表,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 适配器的想法是,当用户调用其中一个 repo 函数时,它会调用 Ecto.Adapters.Postgres.Connection 模块中的相应函数。这似乎确实发生了,但在此之前似乎正在发生其他步骤。

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

【问题讨论】:

    标签: postgresql elixir phoenix-framework ecto


    【解决方案1】:

    我有时间对此进行了更深入的研究,并觉得我现在有了更好的理解。

    我将按顺序列出用户在 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
    

    (这是 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 步。适配器名称

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

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

    这个模块中没有定义插入函数,但是因为它是 'using' 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
    

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

    希望有人觉得这很有帮助。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-03-24
      • 2020-08-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多