Код выглядит безопасно: сохраняем запись в БД, затем переименовываем файл на диске, всё внутри одного @Transactional-метода. Если после переименования бросится исключение — транзакция БД откатится. Файл — нет.

Файловая система не знает о транзакции Spring. У неё нет понятия commit/rollback, которое синхронизировано с JDBC-соединением. Переименование — это системный вызов, который либо случился, либо нет, и откатить его так же, как UPDATE, нельзя.

В результате после отката у вас в БД старое имя файла, а на диске — новое. Или наоборот, если порядок операций другой. Расхождение находят не сразу: оно вылезает, когда кто-то пытается открыть файл по пути из БД и получает FileNotFoundException.

@Transactional public void renameDocument(Long id, String newName) { Document doc = repo.findById(id).orElseThrow(); doc.setFileName(newName); repo.save(doc); Path oldPath = storage.resolve(doc.getOldFileName()); Path newPath = storage.resolve(newName); Files.move(oldPath, newPath); // не откатится при rollback }

Второй вопрос следом: что если newName пришёл от пользователя напрямую. Без валидации это путь к path traversal — "../../etc/passwd" или просто недопустимые для файловой системы символы валят Files.move с исключением на проде, которое не ловится явно. Имя файла нужно санитизировать и генерировать отдельно от пользовательского display-имени, которое хранится только в БД.

Если операция должна быть атомарной end-to-end, нужен компенсирующий шаг: временный файл + переименование в самом конце, либо отдельная таблица с outbox-записью на файловую операцию, которую применяет фоновый воркер после успешного коммита.

Тренажёр: 900+ вопросов и задач, мок с таймером, план повторов

senior·base — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки