Rails中destroy_all无法清理S3多附件,destroy可正常执行
问题:使用
Tour.destroy_all时无法清理S3中的多附件图片 背景信息
基于Rails API开发的系统中,Tour模型包含两种ActiveStorage附件:单文件的cover_image和多文件的images,创建流程正常,但销毁记录时出现附件清理不一致的问题。
控制器创建流程代码
def create_tour if(@params[:tour_cover_image].present?) tour_images = @params.select{ |key, _| key.to_s.start_with?('tour_image-') } tour = Tour.new(@tour_params) tour.save! tour.cover_image.attach( io: File.open(@params[:tour_cover_image].tempfile), filename: @params[:tour_cover_image].original_filename, content_type: @params[:tour_cover_image].content_type, key: "public/tours/#{tour.id}/cover_image/#{SecureRandom.uuid}-#{@params[:tour_cover_image].original_filename}" ) tour_images.each do |key, image| tour.images.attach( io: File.open(image.tempfile), filename: image.original_filename, content_type: image.content_type, key: "public/tours/#{tour.id}/images/#{SecureRandom.uuid}-#{image.original_filename}" ) end else tour = Tour.new(@tour_params.except(:images, :cover_image)) tour.save! end tour end
清理数据的Rake任务
namespace :tours do desc "This will destroy all tours and their dependencies" task destroy_all: :environment do Tour.destroy_all puts "All tours and their dependencies have been destroyed" end end
初始Tour模型(带销毁回调)
class Tour < ApplicationRecord has_many :tour_vehicles_tours, dependent: :destroy has_many :tour_vehicles, through: :tour_vehicles_tours has_many :schedule_stop, dependent: :destroy belongs_to :user has_one_attached :cover_image has_many_attached :images before_destroy :purge_images def purge_images images.purge cover_image.purge end end
问题现象
- 执行
Tour.destroy_all(通过Rake任务)时,purge_images回调触发,但仅cover_image会从S3被移除,images未被清理,且回调中images.attached?返回false。 - 手动执行
Tour.last.destroy销毁单条记录时,回调正常工作,cover_image和images均能从S3被清理。
原因分析
destroy_all会批量加载记录并逐个调用destroy,但批量场景下ActiveStorage的has_many_attached关联不会自动预加载;加上before_destroy回调时机在记录销毁前,此时关联的附件记录可能已被标记为待删除,导致查询不到images,自然无法执行purge。
而单条记录调用destroy时,ActiveRecord会确保关联数据被正确加载,因此images能正常访问并被清理。
可行解决方案
改用after_destroy_commit回调,通过Blob的key前缀直接查询并清理对应附件,绕开关联加载的问题:
class Tour < ApplicationRecord has_one_attached :cover_image has_many_attached :images after_destroy_commit :purge_images def purge_images begin if cover_image.attached? cover_image.purge Rails.logger.info "Successfully purged cover image for Tour #{id}" end # 通过key前缀定位当前Tour下的所有图片Blob image_blobs = ActiveStorage::Blob.where("key LIKE ?", "public/tours/#{id}/images/%") if image_blobs.present? image_blobs.each(&:purge) Rails.logger.info "Successfully purged #{image_blobs.size} images for Tour #{id}" end rescue StandardError => e Rails.logger.error "Failed to purge images for Tour #{id}: #{e.message}" end end end
该方案直接通过存储路径定位附件,确保批量销毁时也能正确清理S3中的文件。
内容的提问来源于stack exchange,提问作者Thiago David
相关产品推荐
相关产品推荐

