Соглашения 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