У меня было время изучить это более глубоко и почувствовать, что теперь у меня есть лучшее понимание.
Я собираюсь перечислить шаги, по порядку, которые происходят, когда пользователь звонит 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, ?,, "e_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
Теперь она вставляет другое значение какмы изначально хотели.
Надеюсь, кто-то найдет это полезным.