Como implementar soft delete de forma transparente com middleware ORM
1. Fundamentos do Soft Delete e a Abordagem com Middleware
1.1. O que é soft delete: conceito, vantagens vs. delete físico
Soft delete é uma técnica onde registros não são fisicamente removidos do banco de dados, mas marcados como "deletados" através de um campo indicador. Em vez de executar um DELETE, realizamos um UPDATE que define um timestamp no campo deleted_at.
As principais vantagens incluem:
- Recuperação de dados: Possibilidade de restaurar registros deletados acidentalmente
- Auditoria: Histórico completo de quando e por quem os registros foram deletados
- Integridade referencial: Relacionamentos podem ser preservados mesmo após "deleção"
- Conformidade legal: Atendimento a requisitos de retenção de dados
1.2. Problemas comuns na implementação manual
A implementação manual de soft delete frequentemente leva a:
- Esquecimento de filtros: Desenvolvedores esquecem de adicionar WHERE deleted_at IS NULL
- Poluição de queries: Código repleto de condições repetitivas
- Inconsistência: Diferentes partes do sistema tratam soft delete de formas distintas
- Relacionamentos quebrados: Registros deletados aparecem em joins e consultas relacionadas
1.3. Por que usar middleware ORM
O middleware ORM oferece:
- Transparência: A lógica de soft delete é aplicada automaticamente sem intervenção manual
- Centralização: Todo o comportamento de soft delete fica em um único ponto
- Redução de boilerplate: Elimina a necessidade de repetir filtros em cada consulta
- Consistência: Garante que todas as operações sigam o mesmo padrão
2. Estrutura de Dados e Convenções para Soft Delete
2.1. Campos obrigatórios
A estrutura mínima para soft delete inclui:
-- Tabela exemplo com soft delete
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
deleted_at TIMESTAMP DEFAULT NULL,
deleted_by INTEGER REFERENCES users(id)
);
-- Índice parcial para performance em consultas
CREATE INDEX idx_users_active ON users (id) WHERE deleted_at IS NULL;
2.2. Índices e constraints
Para manter a performance e integridade:
-- Índice parcial para consultas de registros ativos
CREATE INDEX idx_orders_active ON orders (customer_id) WHERE deleted_at IS NULL;
-- Chave única condicional (exemplo PostgreSQL)
CREATE UNIQUE INDEX idx_unique_active_email
ON users (email) WHERE deleted_at IS NULL;
2.3. Boas práticas de nomenclatura
Utilize traits ou mixins para padronizar:
// Exemplo de trait em JavaScript/TypeScript
const SoftDeletable = {
deletedAt: {
type: Date,
default: null
},
deletedBy: {
type: Number,
default: null
}
};
3. Implementando Middleware de Filtro Global (Leitura)
3.1. Interceptação de queries SELECT
O middleware deve adicionar automaticamente o filtro:
// Middleware de filtro global (exemplo com Sequelize)
const softDeleteMiddleware = (model) => {
model.addHook('beforeFind', (options) => {
if (!options.includeTrashed) {
options.where = {
...options.where,
deleted_at: null
};
}
});
};
3.2. Tratamento de joins e subconsultas
Para propagar o filtro para relações:
// Propagação automática para joins
const applySoftDeleteToIncludes = (includes) => {
return includes.map(include => ({
...include,
where: {
...include.where,
deleted_at: null
},
include: include.include ? applySoftDeleteToIncludes(include.include) : []
}));
};
3.3. Exceções: withTrashed() e onlyTrashed()
Métodos especiais para casos específicos:
// Exemplo de API pública
class SoftDeleteQuery {
withTrashed() {
this.includeTrashed = true;
return this;
}
onlyTrashed() {
this.where = { deleted_at: { $ne: null } };
return this;
}
}
4. Middleware de Escrita: Salvando e Atualizando com Soft Delete
4.1. Sobrescrita do método delete()
Substituir DELETE por UPDATE:
// Hook para sobrescrever delete
model.addHook('beforeDestroy', async (instance, options) => {
if (!options.force) {
await instance.update({
deleted_at: new Date(),
deleted_by: options.userId || null
});
return false; // Impede o delete físico
}
});
4.2. Comportamento de restore()
Implementação do método restore:
// Método restore
model.prototype.restore = async function(options) {
if (!this.deleted_at) {
throw new Error('Registro não está deletado');
}
await this.update({
deleted_at: null,
deleted_by: null
}, options);
};
4.3. Tratamento de operações em cascata
Para soft delete em cascata:
// Hook para cascata em relacionamentos
model.addHook('afterUpdate', async (instance, options) => {
if (instance.deleted_at && instance.previous('deleted_at') === null) {
// Soft delete de registros relacionados
const relations = await instance.getRelatedModels();
for (const relation of relations) {
await relation.update({ deleted_at: new Date() });
}
}
});
5. Gerenciamento de Relacionamentos e Integridade Referencial
5.1. Soft delete em associações
Filtrar automaticamente em relacionamentos:
// Definição de relacionamento com soft delete
User.hasMany(Order, {
foreignKey: 'user_id',
scope: {
deleted_at: null
}
});
5.2. Eager loading vs. lazy loading
Garantindo filtro em ambos os cenários:
// Eager loading com filtro automático
const user = await User.findByPk(1, {
include: [{
model: Order,
required: false,
where: { deleted_at: null }
}]
});
// Lazy loading com filtro
const orders = await user.getOrders({
where: { deleted_at: null }
});
5.3. Tratamento de chaves estrangeiras
Evitar violações ao deletar pais:
// Verificação antes de soft delete de pai
model.addHook('beforeUpdate', async (instance, options) => {
if (instance.deleted_at && !options.skipChildCheck) {
const activeChildren = await instance.countChildren({
where: { deleted_at: null }
});
if (activeChildren > 0) {
throw new Error('Não é possível deletar: existem registros filhos ativos');
}
}
});
6. Considerações de Performance e Limpeza de Dados
6.1. Impacto de índices parciais
Índices parciais melhoram significativamente a performance:
-- Índices parciais para consultas de registros ativos
CREATE INDEX idx_users_active ON users (id) WHERE deleted_at IS NULL;
CREATE INDEX idx_orders_active_date ON orders (created_at) WHERE deleted_at IS NULL;
-- Análise de plano de execução
EXPLAIN ANALYZE SELECT * FROM users WHERE deleted_at IS NULL;
6.2. Estratégias de limpeza programada
Jobs para purge físico de registros antigos:
// Job de limpeza (exemplo com node-cron)
const purgeSoftDeleted = async () => {
const threshold = new Date();
threshold.setDate(threshold.getDate() - 90); // 90 dias
await Model.destroy({
where: {
deleted_at: {
$lt: threshold
}
},
force: true // Ignora soft delete
});
};
// Agendar execução diária
cron.schedule('0 3 * * *', purgeSoftDeleted);
6.3. Uso com particionamento de tabelas
Isolamento de dados deletados:
-- Particionamento por range de deleted_at
CREATE TABLE orders (
id SERIAL,
deleted_at TIMESTAMP,
-- outros campos
) PARTITION BY RANGE (deleted_at);
CREATE TABLE orders_active PARTITION OF orders
FOR VALUES FROM (MINVALUE) TO (MAXVALUE);
CREATE TABLE orders_deleted PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO (MAXVALUE);
7. Testes e Validação da Implementação Transparente
7.1. Testes unitários
Verificar comportamento de consultas:
// Teste unitário (exemplo com Jest)
describe('Soft Delete - Leitura', () => {
it('findAll não deve retornar registros deletados', async () => {
const activeUsers = await User.findAll();
const deletedUsers = activeUsers.filter(u => u.deleted_at);
expect(deletedUsers.length).toBe(0);
});
it('withTrashed deve retornar todos os registros', async () => {
const allUsers = await User.findAll({ includeTrashed: true });
expect(allUsers.length).toBeGreaterThan(0);
});
});
7.2. Testes de integração
Comportamento em transações:
describe('Soft Delete - Transações', () => {
it('deve fazer rollback de soft delete', async () => {
const transaction = await sequelize.transaction();
try {
await user.softDelete({ transaction });
await transaction.rollback();
const restored = await User.findByPk(user.id);
expect(restored.deleted_at).toBeNull();
} catch (error) {
await transaction.rollback();
}
});
});
7.3. Validação de cenários de borda
Testes para concorrência e migrações:
describe('Soft Delete - Cenários de Borda', () => {
it('deve lidar com concorrência', async () => {
const [user1, user2] = await Promise.all([
User.findByPk(1),
User.findByPk(1)
]);
await Promise.all([
user1.softDelete(),
user2.softDelete()
]);
const final = await User.findByPk(1, { includeTrashed: true });
expect(final.deleted_at).not.toBeNull();
});
it('migrações devem preservar soft delete', async () => {
await sequelize.getMigrator().up();
const user = await User.findByPk(1, { includeTrashed: true });
expect(user.deleted_at).toBeDefined();
});
});
Referências
- Documentação Oficial do Sequelize - Soft Delete — Guia completo sobre implementação de soft delete (paranoid) no Sequelize ORM
- Prisma Documentation - Soft Delete Pattern — Tutorial oficial do Prisma sobre implementação de soft delete com middlewares
- TypeORM Documentation - Soft Delete — Documentação oficial do TypeORM sobre soft delete e recuperação de registros
- PostgreSQL Documentation - Partial Indexes — Documentação oficial sobre índices parciais para otimização de consultas com soft delete
- Mongoose Documentation - Soft Delete Plugin — Guia oficial do Mongoose para implementação de soft delete como plugin
- Node.js Best Practices - Soft Delete — Práticas recomendadas para implementação de soft delete em aplicações Node.js