簡體   English   中英

在elixir中使用Ecto.Repo時,哪些功能被稱為“引擎蓋下”

[英]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的參數。

為了使這一點更加清晰,想象一下我們有下表, 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函數)被調用時調用的函數鏈有更好的理解,請在下面解釋。

我有時間更深入地研究這個問題,覺得我現在有了更好的理解。

Repo.insert順序列出當用戶在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

(這是鳳凰應用程序的默認設置)

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

定義函數的位置

步驟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

定義函數的位置

此步驟確保以變更集的形式傳遞給do_insert的數據。

4.2

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

因為它很長,所以不會粘貼整個功能。 定義函數的位置

此函數執行大量數據操作並檢查錯誤。 如果一切順利,最終會調用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

定義函數的位置

這個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 )+ .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

定義函數的位置

為了在已經太長的答案中保存一些文字,我不會詳細介紹。 這個函數實際上並沒有把我們傳遞給Repo.insert的params(回到第一組)。

如果我們想編輯params,我們需要在AdapterName模塊中這樣做。 我們需要定義自己的insert函數,以便它不再調用步驟6中定義的insert函數。

步驟8. AdapterName - 定義我們自己的插入。

為簡單起見,我們只是將步驟6中定義的insert復制到AdapterName模塊中。 然后我們可以修改該函數來更新params,因為我們認為合適。

如果我們這樣做,我們最終得到的功能就像......

  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

現在,這會插入我們原先想要的不同值。

希望有人覺得這很有幫助。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM