Exclusão suave na API REST

imagem

Para que o usuário não sinta dores devido a dados irremediavelmente perdidos, vale a pena pensar na exclusão suave. Com a exclusão suave, o registro não é fisicamente excluído do banco de dados, mas apenas marcado como excluído. Isso facilita a recuperação de dados, redefinindo o sinalizador.

Eu recentemente implementei a exclusão suave em um de nossos serviços REST. Aqueles que estão interessados ​​no que eu fiz, eu convido você a gato.

Entrada obrigatória


O debate sobre a possibilidade de usar remoção leve é ​​muito antigo. Basta olhar para os longos holivares aqui e aqui .

O mais razoável é a posição segundo a qual tudo depende da situação. Há casos em que a exclusão suave é conveniente ou mesmo necessária; há casos em que os argumentos dos oponentes da exclusão suave merecem atenção. A propósito, um argumento importante contra a exclusão suave é a resposta que veio de 2018: se estamos falando de contas de usuário, a exclusão suave é contrária ao GDPR .

Decidimos que, em nosso serviço de armazenamento de documentos, é necessária uma exclusão suave.

Abordagem RESTful


Se queremos implementar a exclusão suave em um serviço, precisamos entender como ele deve ficar do ponto de vista da interface. Uma pesquisa na Internet mostrou que uma pergunta típica que as pessoas têm é se deve usar DELETE {resource} como antes, ou é melhor usar o método PATCH em vez disso com um corpo que inclua algo como {status: 'delete'} .

Aqui a opinião do povo é inequívoca: é necessário usar DELETE como antes. Do ponto de vista do cliente, a exclusão também é uma exclusão na África. Nada deve mudar: se um recurso for excluído, ele se torna inacessível; se o cliente deseja excluir o recurso, ele sabe que o método HTTP DELETE é para esse fim. Não é necessário dedicar o cliente em detalhes de exatamente como o serviço implementa a exclusão.

Além disso, estava preocupado com a questão de como recuperar recursos excluídos. Obviamente, esse problema é resolvido pela administração do banco de dados. No entanto, eu gostaria de poder fazer isso por meio da API REST. E aqui entramos em conflito. Acontece que o cliente ainda precisa se dedicar aos detalhes da implementação?

A pesquisa não retornou resultados por um longo tempo, até que me deparei com um bom artigo de Dan Yoder . O artigo examina a semântica de diferentes solicitações HTTP e sugere que, em vez de exclusão física, mova recursos remotos para o arquivo morto . Além disso, será bom se DELETE retornar um link para o recurso arquivado. O usuário sempre pode restaurar o recurso excluído enviando uma solicitação POST para o arquivo morto.

Desenho


Nosso serviço REST é criado na API da Web do ASP.NET usando o Entity Framework. Como eu disse, faço uma exclusão simples para um recurso chamado documento.

Portanto, primeiro você precisa adicionar colunas à tabela correspondente. Como sinalizador, eu uso um carimbo de data / hora chamado Excluído. Se o valor não for NULL, o recurso será considerado excluído. Além disso, é útil ter informações sobre quem excluiu o recurso.

ALTER TABLE Documents ADD Deleted datetime NULL, DeletedBy int NULL GO 

A ação DELETE no controlador agora definirá simplesmente os valores desses campos em vez de excluir fisicamente o registro. Além disso, DELETE retornará um corpo com uma referência padrão ao documento arquivado:

 { "links": { "archive": "documents/{id}/deleted" } } 

De fato, este é um ponto importante: o link ajuda o cliente a entender que o documento não é excluído, mas movido .

O novo controlador para documentos arquivados deve fornecer os seguintes métodos:
GET documentos / excluídosObtém uma coleção de todos os documentos excluídos
GET documentos / {id} / excluídosRetorna o documento excluído
Documentos do POST / {id} / excluídosRecupera documento excluído;
não requer um corpo; retorna 201 Criado
EXCLUIR documentos / {id} / excluídosExclui fisicamente um documento

Implementação


Inicialmente, planejei adicionar duas visualizações ao meu banco de dados:

 CREATE VIEW DeletedDocuments AS SELECT * FROM Documents WHERE Deleted IS NOT NULL GO CREATE VIEW AvailableDocuments AS SELECT * FROM Documents WHERE Deleted IS NULL GO 

Pareceu-me que isso seria menos problemático: em vez de definir condições no código, eu apenas obtive duas propriedades DbSet diferentes no meu contexto de banco de dados. É verdade que você precisa ter duas entidades idênticas no modelo, mas essa é a propriedade dos objetos POCO no contexto do EF - cada tabela corresponde a exatamente uma entidade.

A propósito, as representações no SQL podem ser úteis para o Entity Framework em outros aspectos: com a ajuda deles, por exemplo, você pode consultar tabelas de outro banco de dados se não quiser criar vários contextos de banco de dados.

No entanto, no meu caso, o número com as visualizações não passou. Durante a autorização, você precisa trabalhar com todos os documentos, porque os usuários têm os mesmos direitos para excluir os documentos que os existentes.

Portanto, decidi ter apenas um DbSet Documents no DbContext e no código toda vez que descubro exatamente o que é necessário no momento:

 var availableDocuments = DbContext.Documents.Where(d => d.Deleted == null); var deletedDocuments = DbContext.Documents.Where(d => d.Deleted != null); var allDocuments = DbContext.Documents; 

Recursos Relacionados


Um documento é um recurso ao qual outros recursos estão associados. Por exemplo, temos um alias de documento. Ou seja, é possível obter um documento não apenas no caminho documents / {id} , mas também no caminho documents / {alias} , em que o alias é uma string exclusiva.

Depois de excluir um documento, todos os aliases associados a ele devem se tornar "invisíveis": se anteriormente o cliente receber uma lista de todos os aliases usando documentos / aliases GET, depois de excluir o documento, seus aliases da lista desaparecerão.

Mas eles permaneceram no banco de dados! Queremos fornecer a capacidade de restaurar o documento no estado em que ele foi excluído. Isso pode causar confusão para o cliente: ele está tentando adicionar um novo alias para outro documento, a lista retornada de documentos / aliases GET não contém essa linha e o serviço se recusa a adicioná-lo.

Eu não acho que isso seja um problema sério. No entanto, se você precisar resolvê-lo, poderá adicionar os documentos GET do terminal / excluídos / aliases . Então tudo se encaixa: o serviço não pode adicionar um alias, pois esse valor já é usado pelo documento remoto.

A questão pode surgir: vale a pena lançar um alias da lista retornada de documentos / aliases ? Deixe-os ficar! Não acho que essa decisão seja correta. Acontece que a lista de aliases conterá links quebrados, porque o serviço retornará 404 Não encontrado se o cliente tentar obter o documento excluído por alias. Se se trata dos recursos filhos associados ao documento, o comportamento deve ser exatamente o mesmo como se estivéssemos excluindo o documento fisicamente.

Limpeza de arquivos


A exclusão suave, além de poder recuperar dados com facilidade, tem várias outras vantagens. A operação de exclusão em bancos de dados relacionais é uma operação cara. E mesmo que a exclusão de um registro leve à exclusão em cascata de registros em outras tabelas, isso estará repleto de conflitos. Portanto, a remoção macia é mais rápida e confiável do que a remoção física.

Mas há uma desvantagem significativa. A base está começando a crescer.

Portanto, na fase final, você deve cuidar da limpeza automática do arquivo. Obviamente, você pode limpar a base manualmente, mas é melhor automatizar esse processo. Se definirmos diretamente a data de validade de um objeto remoto, por exemplo, 30 dias, o cliente poderá exibir a página de arquivo na qual os elementos cuja vida útil está próxima do fim serão destacados.

Minhas mãos ainda não alcançaram essa tarefa. Planejamos adicionar ao nosso sistema de tarefas uma tarefa que uma vez por dia execute uma consulta SQL simples que remova todos os objetos sujos do arquivo morto. Como parâmetro, a tarefa deve ter uma data de validade. Será necessário garantir que o valor atual desse parâmetro seja armazenado em algum lugar do mesmo local. Em seguida, será possível implementar um método no serviço que retorne esse valor ao cliente.

Source: https://habr.com/ru/post/pt440886/


All Articles