Оглавление
- Введение
- Что такое soft delete?
- Основные стратегии реализации soft delete в JPA
- Как работают JPA-методы с soft delete
- Сравнение статического
@SQLRestriction
и динамических фильтров@Filter
- Когда использовать
@SQLRestriction
, а когда@Filter
? - Пример: soft delete связанных сущностей
- Производительность и индексы
- Централизованное управление фильтрами
- Подводные камни и полезные советы
- Лучшие практики soft delete
- Заключение
Введение
Почти в каждом приложении наступает момент, когда возникает необходимость удалять данные. Однако прямое физическое удаление записей (DELETE FROM...
) далеко не всегда лучший подход. Часто требуется сохранить возможность восстановления данных, проводить аудит изменений или выполнять нормативные требования законодательства, согласно которым удаление записей может быть нежелательным или вовсе запрещенным. В таких случаях на помощь приходит подход, известный как soft delete.
В этой статье мы подробно разберем, как эффективно реализовать soft delete в приложениях на Spring Boot с использованием JPA и Hibernate. Мы рассмотрим несколько подходов, сравним их преимущества и недостатки, а также обсудим, в каких сценариях каждый из них будет наиболее уместен.
К концу статьи вы научитесь грамотно внедрять soft delete в свои проекты, обеспечивать целостность данных и простоту поддержки кода, а также получите набор практических рекомендаций, которые помогут избежать распространенных ошибок при реализации этого подхода.
Что такое soft delete?
Soft delete – это подход, при котором запись помечается как удаленная, но фактически не удаляется из базы данных. Чаще всего для этого используется специальный булевый флаг, например поле is_deleted
, которое отмечается как true
при логическом удалении записи.
Такой способ позволяет:
- Сохранять историю и аудит: данные не теряются безвозвратно, что упрощает восстановление случайно удаленной информации и аудит изменений.
- Поддерживать целостность данных: связи между сущностями не нарушаются, т.к. “удаленные” записи остаются в базе (просто помечены неактивными).
- Выполнять требования комплаенса: в некоторых случаях по закону данные нельзя физически удалять сразу – soft delete решает эту задачу, скрывая данные от пользователей, но сохраняя их для возможного восстановления или проверки.
Основные стратегии реализации soft delete в JPA
В основе soft delete лежит наличие поля-флага в сущности (например, is_deleted
), по которому определяется, активна запись или удалена. Добавить такой флаг просто: объявляем в сущности булево поле и храним его в таблице. Например:
@Column(name = "is_deleted")
private boolean isDeleted = false;
Однако одного поля недостаточно – нужно сделать так, чтобы при удалении данных этот флаг устанавливался, а при выборке данных “удаленные” записи автоматически исключались из результатов. Рассмотрим несколько подходов, как реализовать это с помощью возможностей Hibernate/JPA.
Подход 1: Аннотации @SQLDelete
и @SQLRestriction
Первый вариант – воспользоваться расширениями Hibernate, которые позволяют переопределить поведение операций удаления и выборки для сущности. Аннотация @SQLDelete
заменяет SQL-команду DELETE
на пользовательский SQL (например, UPDATE
), а @SQLRestriction
добавляет ко всем запросам условие фильтрации.
Применим их к сущности. Например, для сущности User
:
@Entity
@SQLDelete(sql = "UPDATE users SET is_deleted = true WHERE id = ?")
@SQLRestriction("is_deleted = false")
public class User {
// ... другие поля ...
@Column(name = "is_deleted")
private boolean isDeleted = false;
}
Теперь при вызове стандартных методов удаления (например, userRepository.delete(user)
или deleteById
) Hibernate вместо физического удаления выполнит наш SQL из @SQLDelete
– то есть просто обновит флаг is_deleted
в таблице на true
. Аннотация @SQLRestriction
же добавляет ко всем запросам проверку на неравенство is_deleted = false
, тем самым глобально исключая помеченные как удаленные записи из выборок.
Важно:
@SQLRestriction
задает статичное условие и выполняется всегда для данной сущности. Невозможно параметризовать это условие во время выполнения – оно жестко зашито в запросы.
Таким образом, данный подход обеспечивает прозрачный soft delete: код приложения продолжает использовать стандартные методы JPA (findById
, findAll
, delete
и т.д.), а Hibernate под капотом преобразует операции так, что записи не удаляются физически и не попадают в результаты запросов, если они помечены удаленными.
Подход 2: Hibernate-фильтры (@Filter
)
Второй вариант – использовать фильтры Hibernate. Фильтры позволяют динамически добавлять к запросам условия фильтрации с параметрами, которые можно менять в runtime. В отличие от @SQLRestriction
, фильтр не действует постоянно, а должен быть включен явно для сеанса Hibernate.
Шаг 1: определяем фильтр. Для начала опишем фильтр в модели:
@Entity
@FilterDef(name = "activeFilter", parameters = @ParamDef(name = "isDeleted", type = "boolean"))
@Filter(name = "activeFilter", condition = "is_deleted = :isDeleted")
public class User {
// ... поля ...
}
Здесь мы объявили фильтр с именем "activeFilter"
, у которого есть параметр :isDeleted
. Условие condition = "is_deleted = :isDeleted"
означает, что при включении фильтра будут отобраны только записи с is_deleted
, равным заданному параметру. Предполагается, что для активных записей мы будем ставить isDeleted = false
. (Можно объявлять и несколько фильтров – например, по флагу удаления и по какому-то идентификатору арендатора для мультитенантных систем. Каждый фильтр описывается через @FilterDef
и @Filter
.)
Шаг 2: включаем фильтр при выполнении запросов. Допустим, в сервисе перед вызовом репозитория мы хотим применять фильтр, чтобы скрыть удаленные записи:
Session session = entityManager.unwrap(Session.class);
session.enableFilter("activeFilter").setParameter("isDeleted", false);
List<User> users = userRepository.findAll();
Сначала получаем Session
из EntityManager
и включаем фильтр activeFilter
, задав параметр isDeleted = false
. После этого вызов findAll()
вернет только записи, у которых is_deleted = false
. Если фильтр не включить, запрос вернет все записи, включая помеченные удаленными. Таким образом, мы можем гибко решать, когда применять фильтрацию.
Фильтры живут в пределах сеанса (
Session
) – их нужно включать каждый раз заново для нового запроса или транзакции. Например, в пределах одного HTTP-запроса (сессии) фильтр будет активен, но при следующем обращении его снова надо включить вручную.
Также фильтры можно комбинировать. Например, определим два фильтра – по признаку удаления и по идентификатору “тенанта” (для мультитенантности):
@Entity
@FilterDef(name = "deletedFilter", parameters = @ParamDef(name = "isDeleted", type = Boolean.class))
@FilterDef(name = "tenantFilter", parameters = @ParamDef(name = "tenantId", type = Long.class))
@Filter(name = "deletedFilter", condition = "is_deleted = :isDeleted")
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class User {
// поля...
}
В коде можно включать один или оба фильтра в зависимости от ситуации. Например, в контроллере:
@GetMapping
public List<User> getUsers(@RequestParam boolean isAdmin,
@RequestParam(required = false) Boolean deleted,
@RequestParam(required = false) Long tenantId) {
Session session = entityManager.unwrap(Session.class);
if (!isAdmin) {
// Применяем фильтры только для не-администраторов
if (deleted != null) {
session.enableFilter("deletedFilter").setParameter("isDeleted", deleted);
}
if (tenantId != null) {
session.enableFilter("tenantFilter").setParameter("tenantId", tenantId);
}
}
return userRepository.findAll();
}
В этом примере:
- Если запрос выполняет администратор (
isAdmin=true
), фильтры не применяются и возвращаются все данные. - Если обычный пользователь, то включаются фильтры в зависимости от параметров: по флагу удаления (
deletedFilter
) и по ID арендатора (tenantFilter
), чтобы ограничить видимость данных.
Итог: фильтры предоставляют более гибкий и динамический способ скрывать удаленные записи (и не только – фильтры могут использоваться для любой динамической фильтрации). Но ответственность за их включение лежит на разработчике: нужно не забывать активировать нужные фильтры в нужных местах кода.
Подход 3: Аннотация @Where
(устаревший)
Третий подход – использовать аннотацию Hibernate @Where
на сущности, задав в ней условие фильтрации. Раньше (до Hibernate 6.3) это был популярный способ для soft delete:
@Entity
@SQLDelete(sql = "UPDATE users SET is_deleted = true WHERE id = ?")
@Where(clause = "is_deleted = false")
public class User {
// ... поля ...
}
Здесь @Where(clause = "is_deleted = false")
аналогично добавляет ко всем запросам условие на “неудаленность”. В паре с @SQLDelete
это дает поведение, схожее с Подходом 1. Однако начиная с Hibernate 6.3 аннотация @Where
помечена как Deprecated (устаревшая) и вместо нее рекомендуется использовать @SQLRestriction
. Таким образом, новый код лучше писать с использованием @SQLRestriction
, как показано в Подходе 1, а @Where
упоминается лишь для полноты картины и поддержки легаси-кода.
(Примечание: В старых версиях Hibernate для полной поддержки soft delete иногда дополнительно использовали аннотацию @Loader
с кастомным запросом SELECT, чтобы даже прямой findById
читал только “живые” записи. В современных версиях Hibernate при использовании @SQLRestriction
этого не требуется – Hibernate сам фильтрует findById
вызовы.)
Как работают JPA-методы с soft delete
Рассмотрим, что происходит при выполнении типичных операций с сущностями, для которых настроен soft delete (например, с помощью @SQLDelete
и @SQLRestriction
из Подхода 1). Предположим, у нас есть репозиторий UserRepository
для сущности User
.
- Получение по ID (
userRepository.findById(1L)
) – Spring Data JPA сформирует JPQL-запрос вида:select u from User u where u.id = :id
Hibernate при обработке добавит условие из@SQLRestriction
. В итоге фактический SQL будет примерно таким:select * from users where id = ? and is_deleted = false
То есть к условию по ID добавляетсяAND is_deleted = false
. Если запись с таким ID существует и не помечена удаленной, она вернется. Если же запись помечена как удаленная (is_deleted=true
), запрос не найдет строк и метод вернетOptional.empty
(пустой результат). В логах Hibernate можно увидеть это дополнение условия:Hibernate: select u1_0.id, u1_0.deleted, u1_0.name from users u1_0 where u1_0.id = ? and u1_0.deleted = false binding parameter [1] as [BIGINT] - [1]
- Получение всех записей (
userRepository.findAll()
) – благодаря@SQLRestriction
ко всем выборкам поUser
автоматически добавляется фильтрis_deleted = false
. SQL запрос в логе будет таким:Hibernate: select u1_0.id, u1_0.deleted, u1_0.name from users u1_0 where u1_0.deleted = false
Таким образом, методfindAll()
вернет только неудаленные записи. - Удаление записи (
userRepository.deleteById(1L)
) – при вызове удаления Hibernate вместоDELETE
выполнит наш SQL из@SQLDelete
. В логе это отразится как:Hibernate: update users set deleted = true where id = ? binding parameter [1] as [BIGINT] - [1]
Таким образом, запись с ID=1 не удалится из таблицы, а будет лишь обновлена – ее полеdeleted
(akais_deleted
) станетtrue
.
Как видим, все стандартные операции продолжают работать, но благодаря настройкам soft delete они прозрачно меняют свою семантику на “логическое удаление”. Это очень удобно, так как не требует повсеместно менять код репозиториев на специальные запросы – Hibernate делает все автоматически.
Включение логирования запросов (SQL + параметры)
Чтобы убедиться, что soft delete работает правильно, и видеть реальные SQL-запросы, полезно включить логирование SQL и параметров. Добавьте в application.properties
следующие настройки:
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql=TRACE
Первая опция включает вывод SQL, вторая форматирует его для читабельности, а последние две настраивают логгер Hibernate на вывод параметров запросов. После этого в консоли приложения будут видны все запросы и передаваемые значения (как в приведенных примерах выше).
Сравнение статического @SQLRestriction
и динамических фильтров @Filter
И @SQLRestriction
, и @Filter
способны фильтровать данные на уровне Hibernate, не показывая помеченные записи. Однако они по-разному работают и подходят для разных случаев. Рассмотрим их отличия:
@SQLRestriction
– всегда активный статический фильтр
Аннотация @SQLRestriction
задает неизменное условие, которое Hibernate будет добавлять ко всем SQL-запросам для данной сущности (и даже к нативным запросам через nativeQuery=true
, что важно учитывать). Например:
@Entity
@Table(name = "users")
@SQLRestriction("is_deleted = false")
public class User {
// поля...
}
С таким декоратором любой запрос к таблице users
через эту сущность получит условие WHERE is_deleted = false
автоматически. Разработчику не нужно помнить о фильтрации – Hibernate сам всегда применит ее. Это отлично подходит для случаев, когда условие одинаково для всех пользователей системы и не требует изменения: например, скрывать записи с is_deleted=true
практически всегда, во всех сценариях.
Минусы: Невозможно временно отключить или изменить это условие – оно захардкожено. Кроме того, нельзя подставить в него динамический параметр (например, разный tenantId для каждого пользователя) – для таких задач @SQLRestriction
не предназначен.
@Filter
– гибкие фильтры с параметрами
Hibernate-фильтры (@Filter
) позволяют задавать условие с параметром, которое можно включать и выключать в runtime. Как мы уже делали выше, фильтр объявляется через @FilterDef
/@Filter
в сущности, а затем активируется через Session.enableFilter(..).setParameter(..)
. В отличие от @SQLRestriction
, фильтр по умолчанию выключен – пока вы его явно не включите, он не влияет на запросы.
Преимущества фильтров:
- Можно задавать разные значения условий. Например, один раз включить
deletedFilter
сisDeleted = false
(показывать только активные), а в другой ситуации – сisDeleted = true
(наоборот, получить только удаленные записи, скажем, для административного просмотра корзины). - Можно комбинировать несколько условий. Например, одновременно фильтровать по удаленности и по полю арендатора, как в примере с
tenantFilter
. - Можно в любой момент отключить фильтрацию (не вызывать
enableFilter
) для особых случаев, вроде административных нужд.
Недостатки:
- Нужно не забывать включать нужные фильтры в каждом нужном месте. Если пропустить – можно случайно получить в выборке лишние (удаленные) данные.
- Фильтры работают только с Hibernate Session. Если вы выполняете запросы минуя Hibernate (например, через JDBC напрямую или через EntityManager без использования Hibernate-сессии), фильтры не применятся. В стандартных методах Spring Data это не проблема, т.к. под капотом все равно используется Hibernate. Но важно понимать это ограничение.
Когда использовать @SQLRestriction
, а когда @Filter
?
Оба подхода можно комбинировать, но чаще имеет смысл выбрать что-то одно для soft delete механизма. Рекомендации следующие:
@SQLRestriction
– применяйте, когда условие фильтрации всегда одно и то же и должно действовать повсеместно. Например, скрывать удаленные записи всегда, во всех частях приложения, без исключений. Статический подход проще и надежнее, так как вы не зависите от правильности включения фильтра в коде – Hibernate сам защитит ваши запросы. Также@SQLRestriction
сработает даже для методов вродеfindById
и для нативных запросов на репозитории, что может быть критично для консистентности.@Filter
– используйте, если требуется гибкость. Когда бывают случаи, что нужно получить и удаленные записи (например, для восстановления или просмотра истории), или когда условие зависит от контекста (ID пользователя, организация, различные уровни доступа). Фильтры отлично подходят для мультитенантных систем, где условие включает переменный идентификатор арендатора. Также, если soft delete – не строгая политика, а опция (скажем, админы могут видеть все, а остальные – с фильтром), то без динамических фильтров не обойтись.
В реальных проектах нередко сочетают подходы: например, простой soft delete (is_deleted) делают через @SQLRestriction
, а более сложные ограничения доступа (по пользователям, организациям) – через фильтры, включая их по необходимости.
Пример: soft delete связанных сущностей (Parent-Child)
Рассмотрим ситуацию, когда у нас есть связанные сущности, например родительская сущность Parent
и дочерняя Child
, связанные отношением @OneToMany
/ @ManyToOne
. Обе имеют поле is_deleted
. Хотелось бы, чтобы при выборке родителя его удаленные дети не попадали в список, и чтобы при удалении родителя дочерние записи тоже можно было пометить удаленными. Как это настроить:
1. Сущность Parent: добавляем к ней soft delete так же, как делали для User. Кроме того, на связь children
тоже повесим @SQLRestriction
, чтобы Hibernate фильтровал коллекцию детей. Например:
@Entity
@Table(name = "parents")
@SQLDelete(sql = "UPDATE parents SET is_deleted = true WHERE id = ?")
@SQLRestriction(clause = "is_deleted = false")
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(name = "is_deleted")
private boolean deleted = false;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
@SQLRestriction("is_deleted = false")
private List<Child> children = new ArrayList<>();
// getters, setters...
}
Обратите внимание: у children
стоит orphanRemoval = true
. Это означает, что при удалении дочернего элемента из списка он будет автоматически удален (что в контексте soft delete может быть тонким моментом, о нем ниже).
2. Сущность Child: настраиваем аналогично:
@Entity
@Table(name = "children")
@SQLDelete(sql = "UPDATE children SET is_deleted = true WHERE id = ?")
@SQLRestriction(clause = "is_deleted = false")
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(name = "is_deleted")
private boolean deleted = false;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
// getters, setters...
}
Поведение: теперь, если мы пометим Parent как удаленного (parentRepository.delete(parent)
), то:
- У родительской записи
is_deleted
станетtrue
(благодаря @SQLDelete). Дочерние записи при этом не изменятся автоматически, поскольку мы не реализовали каскадный soft delete. (CascadeType.ALL +@SQLDelete
на Parent не означают, что дети тоже пометятся; каскад на удаление с @SQLDelete не работает, он срабатывает только на настоящее удаление.) То есть при удалении Parent дети останутся в базе как были (со значениемis_deleted = false
, если их отдельно не трогали). Если требуется помечать на удаление сразу и дочерние сущности, придется это делать вручную (или написать кастомный метод, проходящийся по children). - Если же удалить конкретного Child (
childRepository.delete(child)
), то ему выставитсяis_deleted = true
. Родитель при этом не затрагивается. - Запрос
parentRepository.findAll()
вернет только родителей сis_deleted = false
(благодаря@SQLRestriction
на Parent). - У каждого полученного Parent вызов
parent.getChildren()
вернет список детей фильтруя удаленных: т.е. вchildren
не попадут те Child, у которыхis_deleted = true
. Это обеспечено@SQLRestriction
на списке детей.
Так достигается soft delete на уровне связанных сущностей: удаленные родители и дети скрываются, но остаются в базе для сохранения целостности (например, чтобы не нарушились внешние ключи, если они есть, или для возможного восстановления).
Важно про каскады и orphanRemoval: при использовании
@SQLDelete
каскадное удалениеCascadeType.ALL
не помечает автоматически дочерние записи как удаленные – Hibernate попытается выполнить реальное удаление каскадно. Чтобы избежать этого, у всех каскадных сущностей нужно тоже определять@SQLDelete
. Кроме того, флагorphanRemoval = true
может приводить к физическому удалению “осиротевших” записей, минуя наш soft delete. Будьте аккуратны с этой опцией – возможно, в сценариях soft delete от нее лучше отказаться или тщательно протестировать ее влияние.
Производительность и индексы
При добавлении условия is_deleted = false
ко всем запросам важно убедиться, что это не замедлит работу базы данных. Рекомендуется создать индекс на колонку-флаг:
CREATE INDEX idx_users_active ON users (is_deleted);
Наличие индекса ускорит фильтрацию по is_deleted
. Если условие объединяется с другими (например, WHERE is_deleted = false AND tenant_id = 123
), имеет смысл создавать составные индексы по соответствующим колонкам, чтобы запросы выполнялись быстрее. Оптимизация индексов особенно актуальна, когда таблицы растут, а доля “мягко удаленных” записей в них значительна.
Централизованное управление фильтрами (опционально)
Для удобства можно обернуть включение фильтров в отдельный сервисный класс – чтобы не дублировать код session.enableFilter
по всему проекту. Например:
@Component
public class HibernateFilterService {
@Autowired
private EntityManager entityManager;
public void applyDefaultFilters(Long tenantId) {
Session session = entityManager.unwrap(Session.class);
session.enableFilter("activeFilter").setParameter("isDeleted", false);
session.enableFilter("tenantFilter").setParameter("tenantId", tenantId);
}
}
Здесь метод applyDefaultFilters
включает сразу фильтр активности (скрыть удаленные записи) и фильтр арендатора, принимая идентификатор арендатора в параметрах. Вы можете вызывать этот метод в нужных местах (например, в @ControllerAdvice
перед выполнением каждого запроса, исходя из контекста текущего пользователя). Такой подход помогает централизовать логику применения фильтров и уменьшить риск забыть их включить где-то в коде.
Подводные камни и полезные советы
Напоследок, несколько рекомендаций и моментов, на которые стоит обратить внимание при реализации soft delete:
- Всегда включайте фильтр, когда он нужен. Если используете
@Filter
, не забывайте вызыватьsession.enableFilter(...)
для каждого сеанса, в котором хотите скрывать данные. Иначе можно неожиданно получить “лишние” записи. - Настройка каскадов: если у вас есть связанные сущности, и вы используете
@SQLDelete
, позаботьтесь, чтобы и у дочерних сущностей был свой@SQLDelete
. В противном случае при удалении родителя Hibernate может попытаться физически удалить дочерние записи каскадно (что нежелательно). - Orphan Removal: как отмечалось, опция
orphanRemoval = true
при soft delete может приводить к физическому удалению орфанов. Проверьте ее работу или избегайте, если это противоречит логике сохранения данных. - Работа через EntityManager: фильтры
@Filter
применяются только при использовании Hibernate Session. Если вы вдруг выполняете запросы, минуя Hibernate (например, через JDBC Template), помните, что там придется вручную учитыватьis_deleted
. В случае же Spring Data JPA + Hibernate эти детали за вас обрабатываются фреймворком.
Лучшие практики soft delete
Внедряя soft delete в приложении на Spring Boot с JPA, можно сочетать разные техники. Опираясь на опыт, можно резюмировать следующие лучшие практики:
- Для простого сценария (когда достаточно всегда скрывать удаленные записи) отлично подходит связка
@SQLDelete
+@SQLRestriction
. Она обеспечивает глобальное статичное поведение soft delete, не требуя дописывать условия вручную в каждом запросе. - Когда нужна настройка и динамика – используйте Hibernate Filters. Они идеально подходят для мультитенантности, сложных правил доступа и случаев, когда иногда нужно видеть и удаленные данные (например, для восстановления или анализа).
- Включайте фильтры на уровне сервиса, бизнес-логики. Не рекомендуется делать это в контроллере или, наоборот, в репозитории – ответственность за ограничения доступа обычно лежит на сервисном слое.
- Осторожнее с каскадными удалениями: помните, что
CascadeType.ALL
в сочетании с физическим удалением может разрушить идею soft delete. Удостоверьтесь, что каскад не удаляет дочерние записи без флага – при необходимости настройте soft delete для всех связанных сущностей. - Полезно сохранять не только флаг, но и время удаления (например, поле
deletedAt
с аннотацией@UpdateTimestamp
или устанавливаемое вручную). Это поможет в аудите – знать, когда и кем была помечена запись. - Если вам нужна максимальная гибкость, фильтры предпочтительнее, чем @Where/@SQLRestriction. Статические условия хороши для простых случаев, но при усложнении требований лучше переключиться на динамические механизмы.
Заключение
В этой статье мы рассмотрели подходы и лучшие практики реализации soft delete в приложениях на Spring Boot с использованием JPA и Hibernate. Как вы могли убедиться, soft delete является мощным инструментом для обеспечения целостности данных, удобного аудита и соблюдения требований законодательства.
При реализации soft delete лучше всего использовать аннотации Hibernate:
- Для простых и неизменных сценариев оптимально подходят аннотации
@SQLDelete
и@SQLRestriction
, которые прозрачно интегрируются в существующий код и автоматически обеспечивают фильтрацию удаленных записей. - В более сложных случаях, когда требуется гибкость (например, мультитенантные системы или ролевой доступ), стоит использовать Hibernate-фильтры (
@Filter
), позволяющие динамически включать или отключать фильтрацию по мере необходимости.
Всегда помните о важных деталях:
- Не забывайте создавать индексы на поля-флаги удаления для оптимизации производительности.
- Внимательно работайте с каскадным удалением и опцией
orphanRemoval
, чтобы избежать нежелательных физических удалений. - Дополнительно храните информацию о времени удаления записей, что будет полезно для последующего аудита и восстановления данных.
Грамотное использование soft delete позволит вам сохранить данные в целости, обеспечить их доступность и избежать ошибок, которые могут привести к потере ценной информации.