Комментарии 25
Полнстью согласен. когда РЕСТ ендпоинт вырождается в
@GetMapping
public OperationDto getOperationById(@PathVariable("id") Long id) {
return operationService.getById(id);
}
то потом оказывается, что это больно когда для Веб клиента надо отдать одно, а для мобильного чуть другое.
Сервис отдает свою структуру а уже презентационый слой, который в данном случае ендпоинт, сам решает как ему отображать данные или ошибки.
REST сервис вообще должен выставлять данные как данные, а не что там дизайнеры понапридумывали в очередной версии аппликейшна. Если важно дать потребителям возможность уточнять "вот это мне надо, а вот это — нет", API должен позволять указывать всякие fields=id,name&filter=age>20.
А наделять API знанием о том, что бывают веб клиенты, а бывают — мобильные, это по-моему кривизна дизайна. Ну или можно не называть это словом REST просто.
@GetMapping
public OperationDto getOperationById(@PathVariable("id") Long id) {
return operationService.getById(id);
}
по сравнению вот с этим:
@GetMapping
public OperationDto getOperationById(@PathVariable("id") Long id) {
return operationMapper.toDto(operationService.getById(id));
}
Да и контроллеры вовсе не должны быть «тонкими» или «плоскими». Это Вы из своих личных предпочтений так решили. В контроллере можно и агрегацию из нескольких сервисов сделать, а потом еще и маппер вызвать, и даже в этом случае код можно написать коротко и понятно, и даже уложиться в условные 10-15 строк, что совсем не является критичным для методов контроллера.
Лучше вообще не начинать
- Оставляем контроллеры тонкими
@GetMapping
public OperationDto getOperationById(@PathVariable("id") Long id) {
return operationService.getById(id);
}
...
public OperationDto getById(Long id) {
Optional<Operation> operationOptional = ... //логика получения operation
return operationOptional
.map(operation -> mapperFacade.map(operation, OperationDto.class))
.orElse(EMPTY_OPERATION_DTO);
}
- Обычно контроллеру нужно больше контекста, чтобы дать хороший ответ. Нашлось там что запрашивали и есть доступ — 200, нашлось, но нет доступа — 403, не нашлось — 404.
- Многие REST API позволяют потребителю указать набор полей, которые он хочет получить: ?fields=id,name. Это знание можно использовать чтобы построить более оптимальный запрос для вытягивания данных из БД (не делать лишний join например).
- В случае ресурсов-коллекций конечно же потребитель хочет фильтровать: GET /orders?filter=date<2019-01-01, сортировать: GET /orders?sort=date, и паджинацию: GET /orders?skip=10&take=100. И конечно это всё вместе тоже GET /orders?filter=date<2019-01-01&sort=date&skip=10&take=100&fields=id,name.
- PUT (который "создать или обновить") тоже свои нюансы добавляет — там и тривиальная валидация ("name" не может быть пустым) — 400, и "глобальная" ("name" обязано быть уникальным) — 409, и разница между 201 created и 204 no content.
- Аж уж PATCH так вообще.
Моё мнение такое, что если делаешь "REST API фасад к сервису", совершенно всё равно как это делать — получится нечто жёсткое и нерасширяемое (но этого вполне может хватать). Если делаешь "REST API", то лучше с самого начала код писать в терминах моделей HTTP-ресурсов, маппингов между полями данных в API и полями/таблицами в БД.
Поддерживаю. REST это об общей архитектуре, а не об API. Если начинать делать HTTP API фасад к сервису, который архитектурно основан не на передаче представлений ресурсов, то лучше подумать почему этот HTTP API должен быть непременно REST и не лучше ли использовать JSON RPC или просто разумный набор HTTP ендпоинтов и принимаемых/отдаваемых данных, соответствующий юзкейсам, типа POST /contract/1/approve без тела, а не делать попыток представлять всё как ресурсы и передавать PATCH /contract/1 {status:"approved" } а потом парсить тело на конкретные значения чтобы определить approve метод сервиса вызывать или cancel, если сервис оперирует понятиями бизнес-логики, а не REST ресурсов.
Кконтроллер не должен выбрасывать 403, так как не авторизованный запрос должен быть остановлен фильтрами аутентификации в Spring Security.
В общем случае — нет. Spring Security — это скорее про аутентификацию и роли — когда хочется сказать "если не админ, то точно нет". Логика типа "если вы не друзья с Бобби и у вас нет общих друзей, то фотку Бобби мы вам 403" намного органичнее пишется "прямо по месту".
Не совсем понятно что вы подразумеваете под общим случаем. Есть фрэимворк и он отвечает за аутентификацию на уровне фильтров которые будут делегировать ее authenticationManager, а он в свою очеред выберет нужного authentificationProvider. Далее этот фрэимворк представляет нам возможности авторизации при входе в метод, с обширными возможностями SpEL и иерархией ролей. И мне не совсем понятно в каком случае, кроме "Hello World" мы не должны им пользоваться и делегировать это все нашему контроллеру.
А если там права на уровне записи надо проверить, типа что текущий пользователь является автором комментарий, который он пытается отредактировать?
Можно-то можно, но не понятно зачем.
@PutMapping("/comments/{id}")
@PreAuthorize("canPutComment(commentId, userId)")
public ResponseEntity<?> putComment(@PathVariable("id") commentId) {
...
}
Т.е. canPutComment()
должен будет уметь загружать коммент, т.е. там будет вся эта логика — если коммент есть, то сравниваю автора с текущим пользователем, если коммента нет, то разрешаю, потому что создаётся новый. Аналоничная логика также будет и в putComment()
— попытаться найти коммент, если он есть то редактируем, если нет, то создаём. А добились-то чего этим разделением?
Можно просто не использовать Spring, и жить спокойно контролируя свой код самому.
Изначально не надо тащить такие связи, если вы знаете что они вам понадобятся, вы делаете заррос с Entity Graph и получаете их в один запрос, а не несколько. Если же они вам не нужны вы мапите сущность в дто где их уже не будет.
Как начать писать микросервис на Spring Boot, чтобы потом не болела голова