Rails 8.0, MySQL. Модели заказа лет шесть, колбэки добавляли по одному:
class Order < ApplicationRecord
after_create :send_confirmation
after_save :recalculate_customer_stats
after_save :sync_to_warehouse, if: :saved_change_to_status?
after_update :notify_manager, if: -> { total > 50_000 }
before_destroy :restore_stock
private
def send_confirmation
OrderMailer.with(order: self).confirmation.deliver_now
end
# остальные такие же короткие
end
Понадобился импорт старых заказов из CSV, около 40 тысяч строк. На стейдже он разослал подтверждения всем, у кого там есть почта, хорошо ещё, что адреса на стейдже подменены. Можно обвесить всё unless importing?, но, по-моему, это путь в никуда. Как бы вы это распутали?
Правило, которое меня ни разу не подводило: в колбэках модели только то, без чего данные будут неверными. Письма, синхронизация со складом, уведомление менеджеру - это действия бизнеса, и им место там, где этот бизнес происходит, то есть в явном сценарии оформления заказа:
class PlaceOrder
def self.call(params)
order = Order.create!(params)
OrderMailer.with(order: order).confirmation.deliver_later
CustomerStatsJob.perform_later(order.customer_id)
order
end
end
# контроллер
@order = PlaceOrder.call(order_params)
# импорт создаёт записи, и больше ничего
rows.each { |attrs| Order.create!(attrs) }
Тогда импорту не нужны никакие флаги. Всё за раз не переноси, начни с почты, она больнее всего.
Если какие-то колбэки останутся, переведи их с after_save на after_commit. Сейчас sync_to_warehouse может отправить на склад заказ из транзакции, которая потом откатится, и склад будет ждать то, чего нет. С джобами то же самое: джобу, поставленную внутри транзакции, воркер может взять раньше, чем запись станет видна, и получит RecordNotFound.
Вынесла подтверждение и уведомление менеджеру в PlaceOrder, синхронизацию со складом перевела на after_commit, пересчёт статистики пока остался колбэком, доберусь в следующем спринте. Импорт 40 тысяч заказов на стейдже прошёл за 11 минут и не отправил ни одного письма. Заодно нашлось место в админке, где заказ создавался мимо всего этого, теперь и оно через сервис.