Соглашения Rails становятся настолько инстинктивными для инженеров по мере их разработки, что они часто копируют один и тот же код и шаблоны проектирования, не понимая, что существует множество способов делать вещи, которые либо более производительны, либо чище и проще в обслуживании.
Если поискать немного повнимательнее, можно обнаружить целый мир «лучших» решений для выполнения многих часто используемых функций.
#отсутствует и #связано с ActiveRecord
Знаете ли вы, что можете легко запрашивать модели, у которых нет или хотя бы одна из указанных ассоциаций?
# (v6.1) Gets all posts that have no comments and no tags. Post.where.missing(:comments, :tags) # (v7.0) Gets all posts that have at least 1 comment Post.where.associated(:comments)
ActiveRecord больше и меньше, чем при использовании бесконечного диапазона
Пока вы используете Rails 5.0+ и Ruby 2.6, вы можете использовать объект диапазона (бесконечность) меньше и больше, чем в отношении ActiveRecord?
# (v5.0) Returns all users created in the last day. User.where(created_at: 1.day.ago..) # (v5.0) Returns all users with < or = 10 login attempts. User.where(login_attempts: ..10)
Вариант ActionPack для динамического рендеринга различных макетов.
Это поразило меня!
Иногда вы хотите использовать другой макет представления, например, обычные пользователи используют один макет, а администраторы — другой. Варианты запросов делают именно это!
# (v4.1) ActionPack variants class DashboardController < ApplicationController def show request.variant = current_user.admin? ? :admin : :regular end end # If admin, uses: app/view/dashboards/show.html+admin.erb # If not, uses: app/view/dashboards/show.html+regular.erb
Использование #scoped и #none
Иногда вам нужно либо вернуть объект отношения ActiveRecord, который представляет все записи модели, либо вообще не возвращать никаких записей. Это можно сделать с помощью #scoped
и #none
. Исторически «none» имитировалось путем возврата пустого массива, но использование массива вызывает проблемы и означает, что вы не можете гарантировать, что возвращаемое значение (например, в примере ниже) будет реагировать на те же сигнатуры методов, что и другие пути. Сделаю. Это просто лучший объектно-ориентированный дизайн.
def search(query) case query when :all scoped when :published where(:published => true) when :unpublished where(:published => false) else none end end
Почему мой запрос медленный? Используйте #to_sql
и #explain
Иногда отношения ActiveRecord не всегда ведут себя так, как вы ожидаете, а иногда вам нужно убедиться, что запросы к базе данных используют правильные индексы. Убедитесь, что ваша упорная борьба с вашими отношениями ActiveRecord генерирует SQL (и поведение базы данных, которое вы себе представляете).
# Output the SQL the relation will generate. Post.joins(:comments).to_sql # Output the database explain text for the query. Post.joins(:comments).explain
Фильтрация результатов ActiveRecord с помощью слияния
Я действительно не могу поверить, что это не описано (или, по крайней мере, если и есть, я этого не видел) ни в какой документации по умолчанию, ни в какой-либо книге или руководстве, которые я читал. Это совершенно сбивает с толку, поскольку это невероятно распространенный шаблон использования, и мало кто о нем знает. Он позволяет вам присоединиться к именованной области, отфильтрованной по результату этой именованной области.
class Post < ApplicationRecord # ...
# Returns all the posts that have unread comments. def self.with_unread_comments joins(:comments).merge(Comment.unread) end end
Назначение нескольких переменных с помощью оператора splat *
Одна вещь, о которой должен знать каждый, — это использование оператора splat для объектов, отличных от массивов.
match, text, number = *"Something 981".match(/([A-z]*) ([0-9]*)/)
# match = "Something 981" # text = "Something" # number = 981
Другие примеры включают:
a, b, c = *('A'..'Z')
Job = Struct.new(:name, :occupation) tom = Job.new("Tom", "Developer") name, occupation = *tom
(спасибо за это вики сообщества slack-overflow)
Асинхронные запросы
В Rails 7.0 появился #load_async
, который загружает отношения ActiveRecord (запросы) в фоновых потоках. Это может значительно повысить производительность, необходимую для загрузки нескольких несвязанных запросов в контроллер.
def PostsController
def index
@posts = Post.load_async
@categories = Category.load_async
end
end
В Rails 6.0 (или младше) приведенные выше запросы занимали 200 мс каждый, контроллеру потребовалось бы 400 мс для их последовательного выполнения. С #load_async
в Rails 7 эквивалентный код занял бы столько времени, сколько самый длинный запрос!
Потоковые сгенерированные файлы из действий контроллера
send_stream
в Rails 7.0 позволяет вам передавать данные клиенту от контроллера, чтобы данные генерировались на лету. Раньше это нужно было буферизовать (или сохранять во временном файле), а затем использовать send_data
для передачи данных. Это будет здорово для SSE и длинных опросов в Rails.
send_stream(filename: "subscribers.csv") do |stream|
stream.write "email_address,updated_at\n"
@subscribers.find_each do |subscriber|
stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
end
end
find_each
— это старая, но полезная штука, которой очень мало пользуются!
Наконец, перестаньте использовать #each
для перебора большого количества записей. При использовании #each
Active Record выполняет весь запрос, создает экземпляры ВСЕХ объектов, необходимых для запроса, и заполняет их атрибуты из набора результатов. Если запрос возвращает МНОГО данных, этот процесс медленный и, что более важно, использует тонну памяти. Вместо этого, когда вы знаете, что будет 100 или 1000 результатов, используйте #find_each
только для загрузки объекта пакетами (по умолчанию) по 1000 записей (вы можете изменить это при каждом использовании). Вот пример:
Book.where(:published => true).find_each do |book| puts "Do something with #{book.title} here!" end