
Привет друзья. Ни для кого не секрет, что работая на крупных проектах со сложной логикой, Active Record становится не помощником, а обузой. Представьте, что вам необходимо сделать очень сложный запрос для PostgreSQL нативным образом (на чистом SQL), где должно присутствовать некоторое количество переменных. Но в Rails есть одна неприятная мелочь, функционал выполнения нативных запросов не позволяет использовать именованные биндинги. Но решение есть :) Опробовано и успешно внедрено на проекте с Rails API 5.2 + Ruby 2.6.0 + Postgres 11.
Итак, чуть подробнее о проблеме. Основной метод, который позволяет вам выполнять собственные SQL запросы — это exec_query:
sql = 'SELECT id, name, desc FROM schema.news WHERE id=$1' bindings = [[nil, 100]] new = ActiveRecord::Base.connection.exec_query(sql, 'SQL', bindings).first
На примере выше видно, что формирование биндингов происходит, мягко говоря, через одно место, когда мы пытаемся получить из базы новость под номером 100. Биндинги не могут быть именованными, а только нумерованными. А это очень сильно усложняет чтение и поддержку нативных запросов. Как вариант можно использовать вызов метода find_by_sql для класса модели:
sql = 'SELECT id, name, desc FROM schema.news WHERE id=:id' new = New.find_by_sql([sql, id: 100]).first
Здесь уже все приятнее и понятнее. Но вот вопрос, это более менее приемлемо, если требуется выполнить простой запрос. А вот если запрос реально сложный, то прогонять его через модель и сам Active Record — это большая потеря в скорости (медленно) и производительности (жрет ресурсы сервера). Почему нет именованных биндингов при работе с нативными запросами, для меня загадка, но решение есть — это написать собственную небольшую обертку, которая умеет очень просто работать с именованными биндингами, что я и сделал.
Привожу код статического класса:
# Class for work with SQL query. # Can use clean SQL with hash bindings. # Convert JSON fields to hash. # Can use if not need get model object! class SqlQuery # Create sql query with hash bindings # # @param [String] sql SQL query # @param [Hash] bind bindings data for query # # @return [Array] executed SQL request data and return array with hashes def self.execute(sql, bind = {}) bindings = [] bind_index = 1 # Get all bindings if exist unless bind.empty? bind.each do |key, value| # Change name bind to $ bind sql.gsub!(/(?<!:):#{key}(?=\b)/, "$#{bind_index}") bind_index += 1 # Add new bind data bindings << [nil, value] end end # Execute query, convert to hash with symbol keys result = ActiveRecord::Base.connection.exec_query(sql, 'SQL', bindings).map(&:symbolize_keys) # Convert JSON data to hash result.map do |v| next if v.nil? v.each do |key, val| v[key] = json_to_hash(val) end end end # Convert JSON to hash if correct data # # @param [String] json string # @return [Hash] return hash if json is correct or input data def self.json_to_hash(json) JSON.parse(json, symbolize_names: true) rescue json end end
Как видно из кода, все просто, как угол дома. Запрос работает так:
sql = 'SELECT id, name, desc FROM schema.news WHERE id=:id' binding = { id: 100 } new = SqlQuery.execute(sql, binding).first
На выходе всегда получается только хэш. Немного пояснений. Метод execute принимает в себя строку запроса и хэш с биндингами. Понятно, что биндинги в запросе и хэше должны совпадать. После чего мы проходим циклом по хэшу с биндингами и заменяем их на нумерованные переменные вида $1, $2 и т. д. в самом запросе, попутно создавая массив нумерованных значений, где первый элемент массива равен $1, второй — $2 и так далее. После чего выполняем запрос с помощью стандартного метода exec_query, пробегая по ответу мапером и конвертируя ключи в хэше на символы. После этого мы еще раз пробегаем мапером по ответу, где проверяем каждое значение поля на содержание в нем JSON. Если JSON есть и он валиден, то конвертируем его в хэш с ключами символами, если в поле не JSON, то кидаем исключение, в котором возвращаем назад значение. Вот и все.
Как видите, нет смысла ставить сотни всяких gem, просаживая общую производительность, для того, чтобы получить нужный результат. Очень многие нужные решения можно написать очень быстро самому, потратив минимум времени и кода.
Ссылки на гитхаб и руби гемы с плагином:
github.com/kirill-dan/active_sql_bindings
rubygems.org/gems/active_sql_bindings
Всем удачи, до новых встреч.
Перепечатка с собственного блога. Оригинал здесь
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как вы считаете, Ruby имеет будущее?
56.99%Ruby жив и будет жить.53
16.13%Ruby уже мертв.15
16.13%Ruby доживает свои дни.15
29.03%Хайп не должен стоять во главе выбора технологии.27
Проголосовали 93 пользователя. Воздержались 11 пользователей.
