Какие функции называются «под капотом» при использовании Ecto.Repo в эликсире - PullRequest
0 голосов
/ 18 февраля 2019

Я пытаюсь лучше понять адаптеры Ecto в эликсире.Я начал пытаться построить свой собственный адаптер, используя Ecto.Adapters.Postgres в качестве базы.Это казалось хорошим выбором для начала, так как это адаптер по умолчанию, используемый с Phoenix.

Теперь я могу использовать свой адаптер в моих собственных проектах, обновив следующую строку в файле репо моего проекта ...

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 (и любой другой функции репо), объясните ниже.

1 Ответ

0 голосов
/ 24 февраля 2019

У меня было время изучить это более глубоко и почувствовать, что теперь у меня есть лучшее понимание.

Я собираюсь перечислить шаги, по порядку, которые происходят, когда пользователь звонит 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

Где определена функция

Шаг 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 функциявызывает функцию Kernel.apply/3 с module, function name и arguments.В нашем случае модуль 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

В этом модуле не определена функция вставки, но так как ' использует ' 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)

Так как наш adapter точно такой же, как postgres 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 (в обратном порядке в наборе 1).

Если мы хотим отредактировать параметры, мы должны сделать это в модуле AdapterName.Нам нужно определить нашу собственную функцию insert, чтобы она больше не вызывала функцию insert, определенную на шаге 6.

Шаг 8. AdapterName - определите нашу собственную вставку.

Дляради простоты мы просто скопируем insert, определенный на шаге 6, в наш модуль 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

Теперь она вставляет другое значение какмы изначально хотели.

Надеюсь, кто-то найдет это полезным.

...