简体   繁体   English

在elixir中使用Ecto.Repo时,哪些功能被称为“引擎盖下”

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

I am trying to get a better understanding of Ecto adapters in elixir. 我试图更好地了解Elixir中的Ecto适配器。 I have begun trying to build my own adapter using Ecto.Adapters.Postgres as a base. 我已经开始尝试使用Ecto.Adapters.Postgres作为基础构建我自己的适配器。 This seemed like a good choice to start with as it is the default adapter used with Phoenix. 这似乎是一个很好的选择,因为它是Phoenix使用的默认适配器。

I can use my adapter in my own projects now by updating the the following line in my project's repo file... 我现在可以通过在项目的repo文件中更新以下行来在我自己的项目中使用我的适配器...

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

At the moment it has the same functionality as the postgres adapter. 目前它具有与postgres适配器相同的功能。 I have been trying to edit some of the functions found in the Ecto.Adapters.Postgres.Connection and I have realised that they do not work quite how I expected. 我一直在尝试编辑Ecto.Adapters.Postgres.Connection中的一些函数,我已经意识到它们的工作方式并不像我预期的那样。

The insert function for example does not actually use the params passed into Repo.insert . 例如, insert函数实际上并不使用传递给Repo.insert的参数。

To make this a little more clear imagine we have the following table, Comments ... 为了使这一点更加清晰,想象一下我们有下表, Comments ......

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

Now Repo.insert(%Comments{comment: "hi"}) is called. 现在Repo.insert(%Comments{comment: "hi"})

I want to modify the adapter, so that it ignores the "hi" value that is passed in and instead inserts a comment of "I am the adapter, and I control this database. Hahaha (evil laugh)"... 我想修改适配器,以便它忽略传入的“hi”值,而是插入“我是适配器的注释,我控制这个数据库。哈哈哈(邪恶的笑)”......

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

However, the insert function does not appear to actually take the data to be stored as an argument. 但是, insert函数似乎并不实际将数据存储为参数。

My initial thought of what happened with ecto adapters was that when a user calls one of the repo functions it called the corresponding function in the Ecto.Adapters.Postgres.Connection module. 我最初想到ecto适配器发生的事情是当用户调用其中一个repo函数时,它调用了Ecto.Adapters.Postgres.Connection模块中的相应函数。 This does appear to happen but other steps seem to be happening before this. 这似乎确实发生了,但其他步骤似乎在此之前发生。

If anyone has a better understanding of the chain of functions that are called when Repo.insert (and any other Repo function) is called, please explain below. 如果有人对Repo.insert (和任何其他Repo函数)被调用时调用的函数链有更好的理解,请在下面解释。

I have had the time to look into this more deeply and feel that I now have a better understanding. 我有时间更深入地研究这个问题,觉得我现在有了更好的理解。

I'm going to list the steps, in order, that happen when a user calls Repo.insert in an elixir app. Repo.insert顺序列出当用户在elixir应用程序中调用Repo.insert时发生的步骤。

Step 1. Call Repo.insert 步骤1.调用Repo.insert

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

Step 2. AppName.Repo Module 第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. use Ecto.Repo允许在模块中定义的所有函数在调用它的模块中使用。 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). 这意味着当我们调用AppName.Repo.insert ,它会转到我们的模块,看到没有定义为insert的函数,看到use marco,检查该模块,看到一个名为insert的函数并调用该函数(这不完全是它是如何工作的,但我觉得它解释得很好)。

Step 3. Ecto.Repo Module 第3步.Ecto.Repo模块

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

Where function is defined 定义函数的位置

Step 4. Ecto.Repo.Schema Module 步骤4. Ecto.Repo.Schema模块

4.1 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 定义函数的位置

This step ensures that the data that is passed to do_insert in the the form of a changeset. 此步骤确保以变更集的形式传递给do_insert的数据。

4.2 4.2

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

Not pasting whole function as it is very long. 因为它很长,所以不会粘贴整个功能。 Where function is defined 定义函数的位置

This function does a fair amount of data manipulation and checks for errors. 此函数执行大量数据操作并检查错误。 If all goes well it ends up calling the apply function 如果一切顺利,最终会调用apply函数

4.3 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 定义函数的位置

This apply/4 function calls the Kernel.apply/3 function with the module , function name and arguments . 这个apply/4函数使用modulefunction namearguments调用Kernel.apply/3函数。 In our case the module is AdapterName and the function is :insert . 在我们的例子中,模块是AdapterName ,函数是:insert

This is where our adapter comes into play :D (finally). 这是我们的适配器发挥作用的地方:D(最后)。

Step 5. AdapterName 步骤5. AdapterName

The apply/3 function call above takes us to our created adapter. 上面的apply/3函数调用将我们带到了我们创建的适配器。

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

There is no insert function defined in this module but as it is ' using ' Ecto.Adapters.SQL let's look at this module next. 此模块中没有定义插入函数,但因为它正在“ 使用Ecto.Adapters.SQL我们来看看这个模块。

Step 6. Ecto.Adapters.SQL Module 步骤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 is defined as a module attribute and is just the current calling module ( MODULE ) + .Connection. @conn被定义为模块属性 ,只是当前的调用模块MODULE )+ .Connection。

The calling module, as discussed in point 5 is AdapterName 如第5点所述,调用模块是AdapterName

That means in the insert function, the following line... 这意味着在insert功能中,以下行...

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

is the same as 是相同的

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

As our adapter is just the same as the postgres adapter , it takes us to this next function. 由于我们的adapterpostgres adapter ,它将我们带到下一个功能。

Step 7. AdapterName.Connection 步骤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 定义函数的位置

To save some text in an answer that is already too long, I won't go into too much detail. 为了在已经太长的答案中保存一些文字,我不会详细介绍。 This function doesn't actually take the params we passed into Repo.insert (way back in set one). 这个函数实际上并没有把我们传递给Repo.insert的params(回到第一组)。

If we want to edit the params we need to do so in the AdapterName module. 如果我们想编辑params,我们需要在AdapterName模块中这样做。 We need to define our own insert function so that it no longer calls the insert function defined in step 6. 我们需要定义自己的insert函数,以便它不再调用步骤6中定义的insert函数。

Step 8. AdapterName - Define our own insert. 步骤8. AdapterName - 定义我们自己的插入。

For the sake of simplicity, we are going to just copy the insert defined in step 6 into our AdapterName module. 为简单起见,我们只是将步骤6中定义的insert复制到AdapterName模块中。 Then we can modify that function to update params as we see fit. 然后我们可以修改该函数来更新params,因为我们认为合适。

If we do this we end up with a function like... 如果我们这样做,我们最终得到的功能就像......

  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

This now inserts a different value as we originally wanted. 现在,这会插入我们原先想要的不同值。

Hopefully someone finds this helpful. 希望有人觉得这很有帮助。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM