34. Провести код-ревью переименования файла
Условие задачи:
Система хранит файлы на файловой системе, а их метаданные — в PostgreSQL.
Необходимо провести код-ревью метода, который переименовывает файл и обновляет его имя в базе данных.
Код:
@Transactional
public void process(String oldName, String newName) {
Long id = exec(
"select id from file where name='"
+ oldName + "'"
);
processFile(oldName, newName);
exec(
"update file set name=\""
+ newName
+ "\" where id = "
+ id
);
}
Спойлеры к решению
Подсказки
@Transactional управляет транзакцией базы данных, но не файловой системой.💡 SQL нельзя собирать конкатенацией строк.
💡 Переименовывать файл надёжнее по идентификатору, а не по имени.
💡 Необходимо проверить новое имя и возможные коллизии.
💡 Между чтением записи и её обновлением возможны конкурентные изменения.
💡 Для сбоя между операциями нужен откат, повторная обработка или механизм сверки.
Решение
Основные проблемы:
- SQL-инъекция из-за конкатенации параметров:
"where name='" + oldName + "'"
Нужно использовать параметризованные запросы или Spring Data Repository.
Аннотация
@Transactionalраспространяется только на PostgreSQL. Если файл уже переименован, а обновление базы завершилось ошибкой, файловая система автоматически не откатится.Поиск по имени ненадёжен. Имя может быть неуникальным или измениться конкурентным запросом. Метод лучше принимать
fileId.Необходимо проверять
newName:
пустое значение;
запрещённые символы;
../,/и\;превышение допустимой длины;
существование файла с таким именем.
- Уникальность имени должна обеспечиваться ограничением в базе данных, например по паре
(directory_id, name).
Минимальный вариант с блокировкой записи и компенсацией:
@Transactional
public void rename(long fileId, String newName) {
validateFileName(newName);
FileMetadata file = repository.findByIdForUpdate(fileId)
.orElseThrow(() ->
new IllegalArgumentException("File not found")
);
String oldName = file.getName();
if (oldName.equals(newName)) {
return;
}
storage.rename(oldName, newName);
try {
file.setName(newName);
repository.saveAndFlush(file);
} catch (RuntimeException exception) {
try {
storage.rename(newName, oldName);
} catch (RuntimeException rollbackException) {
exception.addSuppressed(rollbackException);
}
throw exception;
}
}
Репозиторий с блокировкой:
public interface FileMetadataRepository
extends JpaRepository<FileMetadata, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("""
select file
from FileMetadata file
where file.id = :id
""")
Optional<FileMetadata> findByIdForUpdate(long id);
}
Такой вариант уменьшает риск рассинхронизации, но не обеспечивает абсолютную атомарность. Например, процесс может завершиться аварийно после переименования файла, но до обновления базы.
Для production-решения лучше использовать последовательность состояний:
READY → RENAMING → READY
↘ ERROR
Запрос создаёт операцию переименования в базе. Отдельный обработчик выполняет операцию на файловой системе, поддерживает повторные попытки и после успеха обновляет метаданные.
Дополнительно нужна фоновая сверка базы данных и файловой системы, которая обнаруживает и исправляет незавершённые операции.