AdonisJS中数据库级联删除时,如何清理关联的S3存储文件?
解决方案
方案一:数据库触发器 + 定时清理任务
- 步骤1:创建中间表
deleted_post_images,用来记录待删除的图片URL,字段包含s3_url和processed(标记是否已处理)。 - 步骤2:在PostgreSQL中编写触发器函数,当
post_images表的记录被删除(包括级联删除)时,自动把被删记录的s3_url插入中间表:
CREATE OR REPLACE FUNCTION log_deleted_post_image() RETURNS TRIGGER AS $$ BEGIN INSERT INTO deleted_post_images (s3_url) VALUES (OLD.s3_url); RETURN OLD; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_post_image_deleted AFTER DELETE ON post_images FOR EACH ROW EXECUTE FUNCTION log_deleted_post_image();
- 步骤3:在AdonisJS中用
@adonisjs/cron创建定时任务,定期查询deleted_post_images里未处理的记录,调用S3 SDK批量删除对应文件,之后标记这些记录为已处理或直接删除。
这个方案不用修改现有删除逻辑,不管是级联删还是手动删,都能捕获到要删除的图片,维护成本低。
方案二:替换数据库级联删除,改用应用层级联删除
- 去掉数据库外键的
ON DELETE CASCADE配置,在应用层通过事务实现级联删除逻辑。比如删除User时,在服务类里写如下逻辑:
import Database from '@ioc:Adonis/Lucid/Database' import S3 from '@ioc:Adonis/Addons/S3' class UserService { async delete(userId) { await Database.transaction(async (trx) => { // 获取所有关联的PostImage记录 const postImages = await Database.from('post_images') .join('posts', 'post_images.post_id', 'posts.id') .join('blogs', 'posts.blog_id', 'blogs.id') .where('blogs.user_id', userId) .select('post_images.s3_url') // 批量删除S3文件 if (postImages.length > 0) { await S3.deleteObjects({ Bucket: '你的存储桶名称', Delete: { Objects: postImages.map(img => ({ Key: img.s3_url.split('/').pop() })) // 根据实际URL格式提取对象Key } }) } // 按从下到上的顺序删除数据库记录 await Database.from('post_images').whereIn('post_id', /* 关联的Post ID列表 */).transacting(trx).delete() await Database.from('posts').whereIn('blog_id', /* 关联的Blog ID列表 */).transacting(trx).delete() await Database.from('blogs').where('user_id', userId).transacting(trx).delete() await Database.from('users').where('id', userId).transacting(trx).delete() }) } }
- 同时在
PostImage模型的beforeDelete钩子中添加单条删除时的S3文件删除逻辑,覆盖手动删除单张图片的场景。
这个方案所有删除逻辑都在应用层可控,能保证数据库记录和S3文件的一致性,但需要重构现有删除逻辑,适合关联关系不是极度复杂的场景。
方案三:PostgreSQL LISTEN/NOTIFY + 实时监听进程
- 步骤1:修改触发器函数,当
post_images记录被删除时,发送数据库通知:
CREATE OR REPLACE FUNCTION notify_post_image_deleted() RETURNS TRIGGER AS $$ BEGIN PERFORM pg_notify('post_image_deleted', OLD.s3_url); RETURN OLD; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_post_image_deleted AFTER DELETE ON post_images FOR EACH ROW EXECUTE FUNCTION notify_post_image_deleted();
- 步骤2:在AdonisJS中创建自定义命令,启动独立监听进程,连接PostgreSQL并监听
post_image_deleted频道,收到消息后立即删除对应S3文件:
// commands/ListenPostImageDeletion.ts import { BaseCommand } from '@adonisjs/core/build/standalone' import Database from '@ioc:Adonis/Lucid/Database' import S3 from '@ioc:Adonis/Addons/S3' export default class ListenPostImageDeletion extends BaseCommand { public static commandName = 'listen:post-image-deletion' public static description = '监听PostgreSQL发送的图片删除通知' public async run() { const client = await Database.rawConnection() await client.query('LISTEN post_image_deleted') client.connection.on('notification', async (msg) => { const s3Url = msg.payload const key = s3Url.split('/').pop() // 提取S3对象Key await S3.deleteObject({ Bucket: '你的存储桶名称', Key: key }) this.logger.info(`已删除S3文件: ${key}`) }) this.logger.info('开始监听图片删除通知...') } }
- 部署时用PM2等工具保证监听进程持续运行。
这个方案能实时删除S3文件,避免文件长时间孤立,但需要额外维护一个监听进程,适合对实时性要求高的场景。
内容的提问来源于stack exchange,提问作者Obvious_Grapefruit
相关产品推荐
相关产品推荐

