Как использовать raw sql с ecto Repo

У меня есть требование upsert, поэтому мне нужно вызвать хранимую процедуру postgres или использовать общее табличное выражение. Я также использую расширение pgcrypto для паролей и хотел бы использовать функции postgres (такие как «crypt» для кодирования / декодирования паролей).

Но я не могу найти способ заставить Ecto поиграть с необработанным sql частично или полностью. Предполагается ли, что ecto будет поддерживать только elixir dsl и не разрешать шелушение на raw sql, когда dsl недостаточно?

Я обнаружил, что могу делать запросы через адаптер (Rocket - это имя приложения)

q = Ecto.Adapters.Postgres.query(Rocket.Repo,"select * from users limit 1",[])

Но не знаю, как это сделать в модели. Я новичок в elixir, и мне кажется, я смогу использовать Ecto.Model.Schem. schema / 3, но это не удается

Rocket.User.__schema__(:load,q.rows |> List.first,0)
** (FunctionClauseError) no function clause matching in Rocket.User.__schema__/3    

person Krut    schedule 03.01.2015    source источник
comment
Обновился до ecto 4.0 и вышеперечисленное работает! Не уверен, почему Repo внутренне не поддерживает sql-запросы, но я рад двигаться дальше   -  person Krut    schedule 03.01.2015
comment
Repo не поддерживает запросы, потому что он не зависит от SQL! У меня смешанные чувства, если __schema__(:load, ..., ...) предназначен для прямого использования ... но ваше решение идеально подходит для текущего кода!   -  person José Valim    schedule 03.01.2015
comment
Я хочу сделать что-то подобное на примере проекта Ecto, над которым я работаю, поэтому я хотел бы поблагодарить автора вопроса и оставить пример Хосе. У меня есть древовидная структура с использованием parent_id (список смежности), и я хотел бы иметь возможность а) использовать рекурсивные запросы postgres и б) возвращать дерево моделей, поэтому, даже если это никогда не будет непосредственно в самом Ecto, я бы мне до сих пор нравится официальный способ создания кучи моделей из некоторых данных, которые я получил сам.   -  person chrismcg    schedule 03.01.2015
comment
@Krut, пожалуйста, ответьте на свой вопрос и примите его, чтобы этот вопрос больше не оставался без ответа. Так будущим посетителям будет легче найти ответ. Спасибо!   -  person Patrick Oscity    schedule 05.01.2015
comment
@PatrickOscity все еще разрабатывает полный ответ, часть запроса: хорошо, но все же нужно выяснить, как лучше всего получить результат в Ecto.Model   -  person Krut    schedule 30.01.2015
comment
Я просто хотел бы обновить это упоминание, используя API фрагментов, упомянутый ниже, - это путь.   -  person José Valim    schedule 06.01.2016
comment
@ JoséValim, похоже, API фрагментов допускает только фрагменты, мне нужно выдавать целые запросы, потому что SQL не может быть абстрагирован через ecto dsl. Эквивалент find_by_sql в ActiveRecord   -  person Krut    schedule 06.01.2016


Ответы (8)


В Ecto 2.0 (бета) с Postgres вы можете использовать Ecto.Adapters.SQL.query() (текущие документы, 2.0-beta2 docs) для выполнения произвольного SQL; в дополнение к списку самих строк («rows») он возвращает список имен столбцов («columns»).

В приведенном ниже примере я

  1. запустить собственный запрос без параметров,
  2. преобразовать имена столбцов результата из строк в атомы и
  3. объедините их с каждой строкой результатов и сопоставьте их в структуру с помощью Kernel.struct ()

(Вы, вероятно, захотите запустить версию query() (без шума!) И проверить {ok, res}.)

qry = "SELECT * FROM users"
res = Ecto.Adapters.SQL.query!(Repo, qry, []) # a

cols = Enum.map res.columns, &(String.to_atom(&1)) # b

roles = Enum.map res.rows, fn(row) ->
  struct(MyApp.User, Enum.zip(cols, row)) # c
end
person jamesvl    schedule 20.04.2016
comment
Kernel.struct будет обходить приведение типов для схемы, поэтому я думаю, что это сработает, если вызвать набор изменений в качестве последней функции. Однако я хотел бы просто получить changeset.data (очень быстро читать документы 2.0), и теперь это ссылка на структуру? - person Krut; 22.04.2016

Теперь, когда Ecto 1.0 вышел, это должно работать некоторое время:

Добавьте в свой Repo модуль следующие функции:

def execute_and_load(sql, params, model) do
  Ecto.Adapters.SQL.query!(__MODULE__, sql, params)
  |> load_into(model)
end

defp load_into(response, model) do
  Enum.map response.rows, fn(row) ->
    fields = Enum.reduce(Enum.zip(response.columns, row), %{}, fn({key, value}, map) ->
      Map.put(map, key, value)
    end)

    Ecto.Schema.__load__(model, nil, nil, [], fields, &__MODULE__.__adapter__.load/2)
  end
end

И используйте как таковые:

Repo.execute_and_load("SELECT * FROM users WHERE id = $1", [1], User)
person Sean S    schedule 04.09.2015

Модифицированное решение для Ecto 2.0:

в repo.ex:

  def execute_and_load(sql, params, model) do
    Ecto.Adapters.SQL.query!(__MODULE__, sql, params)
    |> load_into(model)
  end

  defp load_into(response, model) do
    Enum.map(response.rows, fn row ->
      fields = Enum.reduce(Enum.zip(response.columns, row), %{}, fn({key, value}, map) ->
        Map.put(map, key, value)
      end)
      Ecto.Schema.__load__(model, nil, nil, nil, fields,
                           &Ecto.Type.adapter_load(__adapter__, &1, &2))
    end)
  end

Использование:

Repo.execute_and_load("SELECT * FROM users WHERE id = $1", [1], User)

ОБНОВЛЕНИЕ: для Ecto 3 вы можете использовать __MODULE__.load(model, fields) вместо Ecto.Schema.__load__

person thousandsofthem    schedule 25.06.2016
comment
А что, если мне нужно использовать необработанный SQL с левыми соединениями? Я пробовал этот метод, но он не работает для объединений. Он возвращает данные, связанные с моделью, указанной в параметрах функции. - person W.M.; 19.07.2016
comment
Отлично! Я немного уменьшил его, на случай, если это поможет. Мне не удалось отформатировать его в этом комментарии, поэтому я застрял здесь: stackoverflow.com/a/46247304/45114 - person Michael Bishop; 15.09.2017

Помимо Ecto.Adapters.SQL.query / 4 существует также Ecto.Query.API.fragment / 1, который можно использовать для отправки выражений запроса в база данных. Например, чтобы использовать функцию массива Postgres array_upper, можно использовать

Ecto.Query.where([x], fragment("array_upper(some_array_field, 1)]" == 1)
person licyeus    schedule 20.08.2015

Ecto 2.2.8 предоставляет Ecto.Query.load/2, поэтому вы можете сделать что-то вроде этого:

use Ecto.Repo

def execute_and_load(sql, params, model) do
  result = query!(sql, params)
  Enum.map(result.rows, &load(model, {result.columns, &1}))
end

См. https://hexdocs.pm/ecto/Ecto.Repo.html#c:load/2

person eahanson    schedule 23.01.2018
comment
Кажется, что это не работает, если запрос включает объединения или вычисляемые столбцы. Я заметил, что некоторые атрибуты не загружаются с помощью phoenix_ecto ~ ›4.0 - person tfwright; 24.03.2019

Ecto, по крайней мере, с версии ~> 0.7 вы должны использовать:

Ecto.Adapters.SQL.query / 4

def query(repo, sql, params, opts \\ [])

Выполняет пользовательский SQL-запрос для данного репо.

В случае успеха он должен вернуть кортеж: ok, содержащий карту как минимум с двумя ключами:

•: num_rows - количество затронутых строк •: rows - набор результатов в виде списка. nil может быть возвращен вместо списка, если команда не дает ни одной строки в качестве результата (но все же дает количество затронутых строк, как команда удаления без возврата)

Параметры

•: timeout - время ожидания завершения вызова в миллисекундах,: infinity будет ждать бесконечно (по умолчанию: 5000) •: log - если false, запрос не заносится в журнал.

Примеры

iex> Ecto.Adapters.SQL.query (MyRepo, «ВЫБРАТЬ $ 1 + $ 2», [40, 2])

% {rows: [{42}], num_rows: 1}

person Peck    schedule 28.01.2015
comment
Это просто возвращает данные строки, а не структуру экто. - person Krut; 29.01.2015

Это образец https://stackoverflow.com/users/1758892/thousandsofthem, но он немного уменьшился (кредит: его /ее)

defmodule MyApp.Repo do
  [...]
  def execute_and_load(sql, params, schema) do
    response = query!(sql, params)
    Enum.map(response.rows, fn row ->
      fields = Enum.zip(response.columns, row) |> Enum.into(%{})
      Ecto.Schema.__load__(schema, nil, nil, nil, fields,
        &Ecto.Type.adapter_load(__adapter__(), &1, &2))
    end)
  end
end
person Michael Bishop    schedule 15.09.2017

По крайней мере, с ecto 4.0 вы можете запрашивать с помощью адаптера, а затем передавать результаты в Ecto.Model. schema / 3:

q = Ecto.Adapters.Postgres.query(Rocket.Repo,"select * from users limit 1",[])
Rocket.User.__schema__(:load,q.rows |> List.first,0)
person Krut    schedule 05.01.2015
comment
Ecto 4.0 пока нет. - person denis.peplin; 28.03.2017