Экто-запрос и пользовательская функция MySQL с переменной арностью
Я хочу выполнить запрос, подобный следующему:
SELECT id, name
FROM mytable
ORDER BY FIELD(name, 'B', 'A', 'D', 'E', 'C')
FIELD
является специфической функцией MySQL, и 'B', 'A', 'D', 'E', 'C'
значения, поступающие из списка.
Я попытался использовать фрагмент, но он не позволяет динамическую арность, известную только во время выполнения.
За исключением того, чтобы работать в сыром виде Ecto.Adapters.SQL.query
Есть ли способ справиться с этим с помощью DSL запрос Ecto?
Изменить: Вот первый, наивный подход, который, конечно, не работает:
ids = [2, 1, 3] # this list is of course created dynamically and does not always have three items
query = MyModel
|> where([a], a.id in ^ids)
|> order_by(fragment("FIELD(id, ?)", ^ids))
3 ответа
ОРМ прекрасны, пока они не протекают. Все делают, в конце концов. Экто молод (например, он только приобрел способность OR
где предложения вместе 30 дней назад), так что просто недостаточно для разработки API, который учитывает расширенные возможности SQL-вращения.
Изучив возможные варианты, вы не одиноки в запросе. Неспособность понять списки во фрагментах (как часть order_by
или же where
или где-либо еще) упоминалось в выпуске Ecto № 1485, в Stackru, на форуме Elixir и в этом сообщении в блоге. Последнее особенно поучительно. Подробнее об этом чуть позже. Сначала давайте попробуем несколько экспериментов.
Эксперимент № 1: сначала можно попробовать Kernel.apply/3
передать список fragment
, но это не сработает:
|> order_by(Kernel.apply(Ecto.Query.Builder, :fragment, ^ids))
Эксперимент № 2: Тогда, возможно, мы сможем построить его с помощью манипуляции со строками. Как насчет давать fragment
строка, встроенная во время выполнения с достаточным количеством заполнителей для ее извлечения из списка:
|> order_by(fragment(Enum.join(["FIELD(id,", Enum.join(Enum.map(ids, fn _ -> "?" end), ","), ")"], ""), ^ids))
Который будет производить FIELD(id,?,?,?)
дано ids = [1, 2, 3]
, Нет, это тоже не работает.
Эксперимент № 3: Создание полного, окончательного SQL-кода, созданного на основе идентификаторов, с размещением необработанных значений идентификаторов непосредственно в составленной строке. Помимо того, что он ужасен, он тоже не работает:
|> order_by(fragment(Enum.join(["FIELD(id,", Enum.join(^ids, ","), ")"], "")))
Эксперимент № 4: Это подводит меня к тому посту в блоге, о котором я упоминал. В ней автор взламывает отсутствие or_where
используя набор предопределенных макросов, основанный на количестве условий для объединения:
defp orderby_fragment(query, [v1]) do
from u in query, order_by: fragment("FIELD(id,?)", ^v1)
end
defp orderby_fragment(query, [v1,v2]) do
from u in query, order_by: fragment("FIELD(id,?,?)", ^v1, ^v2)
end
defp orderby_fragment(query, [v1,v2,v3]) do
from u in query, order_by: fragment("FIELD(id,?,?,?)", ^v1, ^v2, ^v3)
end
defp orderby_fragment(query, [v1,v2,v3,v4]) do
from u in query, order_by: fragment("FIELD(id,?,?,?)", ^v1, ^v2, ^v3, ^v4)
end
Хотя это работает и использует, так сказать, ORM "с зерном", требуется, чтобы у вас было ограниченное, управляемое количество доступных полей. Это может или не может изменить игру.
Моя рекомендация: не пытайтесь подтасовать утечки ORM. Вы знаете лучший запрос. Если ORM не примет его, напишите его напрямую с использованием необработанного SQL и задокументируйте, почему ORM не работает. Защитите его от функции или модуля, чтобы в будущем вы могли оставить за собой право изменить его реализацию. Однажды, когда ORM догонит вас, вы можете просто переписать его без каких-либо последствий для остальной системы.
Создайте таблицу с 2 столбцами:
B 1
A 2
D 3
E 4
C 5
затем JOIN
LEFT(name, 1)
к нему и получите порядковый номер. Тогда сортируй по этому.
(Извините, я не могу помочь с Эликсиром / Экто / Арити.)
Это сводило меня с ума, пока я не обнаружил, что (по крайней мере, в MySQL) существует FIND_IN_SET
функция. Синтаксис немного странный, но он не принимает переменных аргументов, поэтому вы должны иметь возможность сделать это:
ids = [2, 1, 3] # this list is of course created dynamically and does not always have three items
ids_string = Enum.join(ids, ",")
query = MyModel
|> where([a], a.id in ^ids)
|> order_by(fragment("FIND_IN_SET(id, ?)", ^ids_string))
Я бы попытался решить эту проблему с помощью следующего оператора SQL SELECT:
[Примечание: сейчас у меня нет доступа к системе для проверки правильности синтаксиса, но я думаю, что все в порядке]
SELECT A.MyID , A.MyName
FROM (
SELECT id AS MyID ,
name AS MyName ,
FIELD(name, 'B', 'A', 'D', 'E', 'C') AS Order_By_Field
FROM mytable
) A
ORDER BY A.Order_By_Field
;
Обратите внимание, что список "B", "A",... может быть передан как массив или любой другой метод и заменить то, что написано в приведенном выше примере кода.