Комментарии 107
https://github.com/br0mberg/SupportDesk-IncidentService
и вторая часть этой статьи вышла в профиле
Почему все crud-операции возвращают Dto, но create возвращает entity?
Привет, клиенту может быть важно получить ресурс без обёрток, например, id для отслеживания состояния созданного инцидента в будущем или привязать его к другим сущностям
Id может быть и в DTO, а выставлять наружу сущность базы - антипаттерн.
На самом деле, вашему мнению имеет место быть и не только, я бы сказал, что вы правы. Простой пример: возвращать User без обёртки с хэшом пароля - так себе идея.
В моём случае не было конфиденциальной информации, которую нужно сокрыть, но безусловно нужно принять ваше замечание во внимание. Спасибо, я доработаю этот момент!
дело не столько в том, что возвращаются какие-то лишние данные, а в том - что так внешний контракт зависит от внутренних нюансов реализации (того как мы маппим сущность на хранилище и т.д.)
Спасибо, это определённо внесло ясности. Ради таких комментариев, я и пишу статьи, чтобы обратить на проблему внимание с других сторон
а в том - что так внешний контракт зависит от внутренних нюансов реализации
Одно из наружений этого, которое мне встречалось: чтобы переименовать поле в интерфейсе пользователя, нужно переименовать колонку в базе данных.
Ну, конкретно переименование можно сделать на уровне сериализатора/десериализатора.
Впрочем, в примере и так почему-то про DTO знает сервис, хотя фактически это часть только публичного интерфейса, а не бизнес-логики. Но в примере вообще некоторая путаница с изоляцией слоев в приложении, увы.
Да знает, поскольку маппинг не следует производить на контроллере. Контроллеры должны быть максимально простыми, отвечая за прием запросов и формирование ответов. Путаницы с изоляцией слов в приложении нет. Действительно, можно добавить ещё слой презентатора, например, но наличие этого в статье для начинающего разработчика избыточно, увы). Рад что вы так серьёзно подошли к обсуждению материала)
Хм, а с чего бы маппинг не делать именно на том уровне, который служит для предоставления внутренних сущностей для внешнего потребителя, тем более что маппинг из Entity в DTO - это как раз и есть формирование ответов, это же просто один из адаптеров в Onion Architecture.
А вот протягивание формата и методов из публичного API в модель (уровень сервисов) является довольно популярной проблемой, зачем модели вообще знать, какие там entrpoints наружу торчат и какой там протокол висит. Впрочем, все это в идеальном мире, в реальности, конечно, учитывать особенности взаимодействия приходится (про это как раз идет речь в DDD Trilema)
С чего бы это антипаттерн ?
Всегда палка о двух концах - именовать IncidentDTO или IncidentDto. Второй вариант логичное продолжение, но первый более читаемый. По какой логике в голове Вы решаете процесс именования? И как используете именования в маппере, когда нужно свой какой-то добавить?
Статья отличная, большую часть применяю уже, но в таком хорошо и явно описанном виде впервые встречаю. Хорошо кирпичики в голове сложены, делайте tg-канал - Ваши мысли многим будут интересны не только по Java разработке.
IncidentResponse :)
Чаще всего не нужно смешивать объекты запроса и ответа
Привет, большое спасибо за положительный комментарий! Помню, когда писал свой самый первый pet-проект, колебался на выборе между Dto и DTO. Пришёл к тому, что IncidentDto
больше подходит под единообразие стиля. Стараюсь придерживаться одного общепринятого, не считая специфичных вещей, таких как именования proto-файлов, например. Насчёт именования в мапперах — тот же стиль, в духе: toEntity
, toDto
, toDtoWithDetails
.
Всегда палка о двух концах - именовать IncidentDTO или IncidentDto
Это договорённость о стиле. Просто решите с командой как оно будет и старайтесь следовать.
А не проще использовать генератор, типа jhipster? А дальше уже кастомизировать по запросу бизнес требований?
Привет! С JHipster пока не знаком, но видел, как API генерируют с помощью Amplicode — выглядит довольно удобно. Думаю, использование таких инструментов действительно ускоряет разработку, особенно в руках опытного разработчика.
Однако, в статье я ставлю перед собой другую цель: показать, как писать REST API вручную, с нуля. Это особенно полезно для тех, кто только знакомится с основами, или хочет сравнить свой подход с реализованным способом в статье. Такой процесс помогает лучше понять тонкости разработки и получить полный контроль над архитектурой.
Спасибо за идею! Возможно, стоит рассмотреть генераторы в будущих статьях для сравнения.
Я бы отметил такой момент: лучше не использовать классы LocalDateTime в сущностях и DTO. То, как они сериализуются зависит от таймзоны сервера или настроек jdbc драйвера + позволяет без явного указания таймзоны получать дату, что чревато ошибками в приложениях которые работают в нескольких таймзонах (как правило, вы сперва думаете что ваше приложение маленькое и всё ок, а потом возникает необходимость поддержать пользователей из разных таймзон и вылезают разные неприятные ошибки).
Лучше всего использовать Instant или OffsetDateTime. Для передачи по REST и стерилизации использовать стандарт ISO-8068, а для хранения в БД тип TIMESTAMP WITH TIMEZONE, тогда драйвер тоже сам разберётся в как передавать дату и время и не будет ошибок.
А если нужно получить только дату или только время - явно конвертировать в целевую таймзону которая имеет значение с точки зрения бизнес-логики.
Отличные рекомендации, спасибо
ISO-8601 только. А то у вас про турбинное масло)
Никогда не сталкивался с описанными вами проблемами связанными с LocalDate/LocalDateTime, хотя часто работаю с ними, базы умеют работать с такими типами данных. Ещё есть ISO-8601, как уже указали. Важно понимать, что такое LocalDate и OffsetDate, для чего они предназначены и когда надо использовать один тип, а когда другой. В некоторых случаях категорически нельзя LocalDate заменить на OffsetDate или конвертировать в конкретную зону, как вы рекомендуете всегда делать.
Никогда не сталкивался с описанными вами проблемами связанными с LocalDate/LocalDateTime, хотя часто работаю с ними, базы умеют работать с такими типами данных.
А чего там работать то? Это же просто число. Главное чтобы таймзона серевера и приложения совпадала, а то объекты будет создаваться в прошлом или будущем.
В некоторых случаях категорически нельзя LocalDate заменить на OffsetDate или конвертировать в конкретную зону, как вы рекомендуете всегда делать.
В каких именно?
А чего там работать то? Это же просто число.
Почитайте подробнее или послушайте подкасты про то, как работать с типами дата/время, там можно в течении нескольких часов обсуждать нюансы и особенности. И на самом деле с точки зрения дат это не совсем просто число.
В каких именно?
Кто плотно работает с датами, тот сразу выдаст примеры. Дата выдачи паспорта, например. Если вы ее переведёте в offset, то в разных регионах может быть разная дата, а юридически дата должна быть одна и та же.
Дата выдачи паспорта, например. Если вы ее переведёте в offset, то в разных регионах может быть разная дата, а юридически дата должна быть одна и та же.
1. Я бы сказал, что дата выдачи паспорта это что-то среднее между датой и строкой. Она не особо используется как дата.
Тут и дата с таймзоной и дата без таймзоны, не подходящие типы.
2. Перевод даты в offset это операция которую нельзя проводить если вы не знаете правильную зону. Не важно как она хранится внутри с таймзоной или без, это работа разработчика положить её туда правильно. Ну и когда вы достаёте, тоже нужно отдать в правильном формате.
...что-то среднее между датой и строкой...
...дата с таймзоной и дата без таймзоны, не подходящие типы...
Что за бред? Дата выдачи это - ДАТА в григорианском исчислении. Без таймзоны. Для хранения обычно используется LocalDate. Вы начинаете юлить, вместо того, чтобы использовать реальные аргументы.
...явно конвертировать в целевую таймзону...
...Перевод даты в offset это операция которую нельзя проводить...
Противоречие сами себе. Если нет таймзоны в дате, то и конвертировать нечего!
Ещё раз повторюсь: используйте LocalDate там, где не нужна зона и OffsetDate там, где нужна. Требования зависят от доменной области.
Что за бред? Дата выдачи это - ДАТА в григорианском исчислении. Без таймзоны. Для хранения обычно используется LocalDate. Вы начинаете юлить, вместо того, чтобы использовать реальные аргументы.
С точки зрения операций которые с ней можно проводить, от даты понадобятся только форматирование.
Подумайте сами, что это за дата выдачи, которую печатают еще до выдачи вам паспорта. Он потом полгода может еще до вас идти. В паспорте это печатается, чтобы вы знали когда его менять.
LocalDate это не идеальный способ хранение именно этой даты. Потому, что он создаёт иллюзию, что с ней можно работать как с датой. И может так случиться, что кому-то захочется её пихнуть в апи, где нужна таймзона и произойдёт то, чего вы так боитесь.
Не понимаю, что вы хотите сказать.
Для меня, моих коллег, которые тоже работают в моей доменной области (финтех) - это именно дата, и хранится как дата. Дата документа, контракта, ордера, соглашения и т.д. это LocalDate. И в базе он хранится соответствующим образом. По этим полям сортируется по времени, фильтруется по периодам, форматируется и т.д., все что обычно делается с датами. Все эти операции были бы невозможны, если делать, как вы предлагаете.
Есть ещё даты, таймстемпы с таймзоной - это логи, транзакции, события, которые могут происходить в разных регионах, и которые надо фиксировать соответствующим образом.
То, о чем вы говорите, это ваше воображение, которое не относится к первоначальной теме обсуждения, и вообще к айти.
Есть кончено, случаи, когда даты - не даты, например год рождения человека, который надо хранить. Но это другая тема.
Вообще, в финтехе дат гораздо больше одной, так как есть даты календартные (LocalDate), есть банковские даты (для которых неплохо бы заводить отдельный тип-алиас для LocalDate, так как их нельзя смешивать с простыми датами), есть моменты во времени (Instant).
А вот даты с часовым поясом - я не встречал еще ни разу, обычно в этом случае имеется в виду именно Instant.
Т.е. для какой-нибудь транзакции будет отдельно Instant(несколько разных), когда она была проведено, будет банковская дата обработки (не вычисляемая по Instant), будет дата отчетности (LocalDate, может отличаться от банковской даты). И все это, вообще говоря, разные типы, не предполагающие конвертации.
В общем, как вы правильно заметили, с датами нужно всегда обходиться очень аккуратно )
Вот, например, в подлодке обсуждали "Дата и время" выпуск называется https://music.yandex.ru/album/7570122/track/123897310?activeTab=track-list&ysclid=m5i7akp8im824672124
Там узнаете, что время может прыгать, ускоряться, замедляться, изменяться и т.д.
А версионирование API?
RESTful Service - сервис на основе REST, который соблюдает ограничения REST.
Будьте острожнее с терминами.
Например одно из ограничений RESTful это https://en.wikipedia.org/wiki/HATEOAS.
То что вы сделали это не RESTful, это RPC похожий на REST. Иногда его называют RESTish.
Такой подход часто используют, просто не надо его называть RESTful.
Причем Филдинг говорил, что REST подходит для гипер-медиа систем, к которым работа с инцидентами никак не относится. И зачем использовать недоREST вместо RPC - не понятно.
При том, что RPC нормально накладывается на OOP (в отличии от REST-style, который с ООП очень плохо соотносится).
Так что в данном сценарии выбор REST-style - является явной ошибкой дизайна и категорически "неидеальным" API.
HATEOAS часто называют “идеологическим требованием” REST, но его применение оправдано только в определённых случаях. Да и с чего вы решили, что гипер-медиа не сочетаемы с инцидентами? Например, в следующих итерациях микросервиса добавлена работа с вложениями и изображениями, которые являются важной их частью.
В данной статье REST выбран как пример из-за его популярности и удобства для начинающих. Это позволяет изучить ключевые концепции API, такие как принципы идемпотентности, стандарты HTTP-методов и коды ответов.
Напомню, что статья называется идеальный REST API. В любом случае, спасибо за то, что заставляете глубже окунуться в тематику.
Хм, гипермедиа системы - это про очень специфический набор систем, при чем тут инциденты, где есть просто задачи на базовые работы с сущностями? И наличие вложений или изображений не делает систему "гипермедиа", там нет про набор внутренних связей. Даже не всякий документооборот является гипермедиа.
REST проектировался для очень узкого класса систем, к которым да, относится статический web на html, но к которому никак не относится отдельный сервис в распределенной системе.
И да, поэтому в рамках данной статьи выбор REST - явная архитектурная ошибка (впрочем, их в этой статье вообще довольно много). И ошибочный выбор стиля API - не может быть про "идеальное",
Если бы статья называлась "как быстро сделать хоть какой-нибудь API" - не было бы вопросов. Но вот "идеальным" результат назвать никак нельзя.
абсолютно бесполезный комментарий, который выглядит как, - вот вам мое, фи, тут все неправильно !
собственно, где в нем вопросы и подсвеченные проблемы для того, чтобы улучшить ?
Основная проблема - что для поставленной задачи использовать REST-like вообще не нужно, поэтому решение на REST уже не идеальное, а неправильное. Если уж реализовывать, то paсh обычно нужен для всей сущности, а не для отдельных полей (и да, его непросто реализовывать, но в нормальном API он нужен).
Если уж говорить о проблемах:
1) В ..Service зачем-то копируется API, хотя сервис обычно не маппится на entrypointы один к одному (например, patch обычно реализуют через update).
Зачем вообще patch-методы внутри Service - не понятно.
2) Классы Entity зачем-то сделаны mutable, еще и с конструкторами. Это не лучшая идея.
3) Если уж говорить про идеальный API, то стоит подумать про DDD, про это вообще ни слова.
В общем, статья написана явно человеком без большого практического опыта и пользы (кроме перечисления пунктов документации по Spring/Lombok) не содержит. Использовать ее как базу для реализации - не стоит.
И зачем использовать недоREST вместо RPC - не понятно.
А чем он реально хуже в данном пример?
При том, что RPC нормально накладывается на OOP (в отличии от REST-style, который с ООП очень плохо соотносится).
А разве должно быть не наоборот? PRC это вызов функции на удалённом сервисе, а REST это работа с сущностью.
Со стороны бэкенда код будет одинаковым, что для POST api/deleteIncedent/1
что для DELETE api/incidents/1
Хуже сложностью развития и уходом от реальных бизнес-задач. В рамках REST ты не можешь сделать метод "закрыть инцидент", в лучшем случае сделать patch на какое-то поле. И так далее.
Собственно, поэтому RPC проще позволяет реализовывать ООП (который про сокрытие стейта и явный набор имеющих бизнес-смысл сообщений к объекту). В RPC ты можешь добавить специфические create, убрать patch (которые, обычно, не соответствуют реальным бизнес-задачам), разделить close и archive (два разных действия с тем же инцедентом).
Но тут вообще проблема подхода в статье. Хороший API идет от конкретных ФТ и НФТ, от конкретных бизнес-сценариев. А в статье даже user stories не описаны, не говоря уж о выделении реальных методов, о выделении пользовательских сценариях - т.е. про все то, что реально нужно для проектирования API.
Продублирую сюда тот факт, что статья написано для начинающих разработчиков, как способ поделиться опытом в написании простых REST API. Я вкладываю в это понятие тот же смысл, что и рядовые джуны.
Здесь нет решения конкретной бизнес-задачи, нет пользовательских сценариев по описанной выше причине.
Я бы обязательно рассказал о проектировании API в чистом виде, без демонстрации реализации, объяснения полезных аннотаций и подходов, будь у меня на то соответствующие компетенции.
Предлагаю вам поделиться своими глубокими познаниями в этой области в рамках статьи. Будет очень интересно ознакомиться
Хм, а зачем начинающим разработчикам сразу давать плохие советы, еще и называть статью "идеальный API"?
Если бы статья называлась "как быстро набросать REST API на спринге не приходя в сознание" - не было бы вопросов. Но в статье нигде не говорится о том, что изначальная постановка - плохая, что если вас просят написать REST API как в статье - то нужно убегать из компании с таким низким уровнем качества и таким плохим проектированием.
И, кстати, вот подобные советы были бы джуниорам гораздо полезнее, так как быстрее сделали бы их миддлами.
что если вас просят написать REST API как в статье - то нужно убегать из компании с таким низким уровнем качества и таким плохим проектированием.
Вы неплохо в прошлом посте задвинули про необходимость изучения требования перед тем как, что-то делать. А тут делаете общие утверждения без привязки к бизнес требованиям. Это неправильно.
Если задачу дают джуну, мидлу или аутсорсеру, то возможно просто не хотят тратить время на его ознакомление с бизнес требованиями.
Ну, если джуну дают задачу без контекста - это и есть повод убегать из компании. Так как роста не будет, процессы в компании довольно сомнительные, онбординга нет - и так далее.
При том, что разработка с известным контекстом гораздо эффективнее и экономичнее в большинстве кейсов (сложно придумать обратные).
Ну, про проектирование микросервисов и про проблемы разных видов API у меня достаточно много докладов. Из более-менее последних:
https://www.youtube.com/watch?v=Sidqt7IqMFk - про разные стили API
https://www.youtube.com/watch?v=F-6e6sfLvSc - про версионирование API и связанные проблемы
https://www.youtube.com/watch?v=hXuyT6T3fNU - про проектирование микросервисов вообще
Про дизайн API у меня докладов нет, зато есть доклады от Ромы Елизарова, от Doug Lea и от других хороших специалистов. А про ООП неплохо написано и у Буча и у Эванса.
Поэтому не совсем понятно, зачем транслировать довольно странные представления о REST в обучающих статьях (
Хороший API идет от конкретных ФТ и НФТ, от конкретных бизнес-сценариев. А в статье даже user stories не описаны, не говоря уж о выделении реальных методов, о выделении пользовательских сценариях - т.е. про все то, что реально нужно для проектирования API.
Абсолютно согласен.
Ну и обычно для API микросервисов не нужно кэширование на уровне GET, скорее наоборот, с возможным кэшированием на промежочном слое нужно бороться. Увы, в статье про это вообще ни слова.
Отличная статья. Спасибо Вам!
Хотел бы услышать ваше мнение по следующему вопросу: целесообразно ли всю логику про выбору статус кода ответа выносить в @ControllerAdvise или же лучше что то оставлять непосредственно в самом методе контроллера? В своей практике я чаще встречаю первый подход, однако самому больше нравится когда контроллер сам может выбрать статус код в простых ситуациях, например сервисный слой может возвращать Optional что даст нам возможность в самом контроллере выбрать между 200 и 404. Работать с таким кодом по моему мнению легче и приятнее. Что Вы думаете по этому поводу?
Спасибо за положительный комментарий. Довольно часто встречаю как сторонников подобного использования advice, так и любителей обрабатывать такие вещи локально. Тут нет единого мнения, да и в большинстве проектов комбинируют оба подхода. Я предлагаю специфичные ошибки для прозрачности обрабатывать локально, а рядовые и повторяющиеся в глобальной вариации. Это обычно обговаривается в команде
Интересно, что использовали @RestController
, но при этом обработчик ошибок обычный? Есть также @RestControllerAdvice
, где не нужно оборачивать ответ в ResponeEntity
и управлять статусами можно через аннотацию @ResponseStatus
Огромное вам спасибо, вы мне сильно облегчили понимание некоторых моментов
Напомнило чем-то отчёт лабораторной работы в универе. Много воды.
А код случайно не залили в таком виде на гитхаб?
Привет, позднее приложу ссылку к этой статьей или к следующей части. В принципе, приведённого кода должно хватить для самостоятельного воспроизведения.
https://github.com/br0mberg/SupportDesk-IncidentService
и вторая часть этой статьи вышла в профиле
Сервис и правда идеальный 😉.
Одно замечание: пагинация с помощью offset не очень хороша для базы данных, так как она не индексируемая. Базе придётся вытащить несколько первых страниц, но отдать только последнюю.
Лучше для этого использовать номер последней записи из текущей страницы.
Либо ограничивайте допустимый номер страницы.
Спасибо за положительный комментарий, согласен что необходимо доработать этот момент, но в текущей итерации не стал, поскольку для понимания базовых принципов такого подхода достаточно.
Лучше для этого использовать номер последней записи из текущей страницы.
Только надо не забыть индекс в базе по этому полю построить.
Вообще, реально пейджинг в API нужен крайне-крайне редко и всегда вызван недостаточным анализом предметной области. Человек все равно не может проанализировать более сотни строк, ему нужны инструменты для уточнения поиска, поэтому пейджинг на API для клиента обычно бесполезен. Для межсервисных взаимодействий пейджинг тоже не нужен, так как эффективнее отдать все данные одним потоком (или пересмотреть взаимодействие сервисов так, чтобы не было сверхбольших выборок).
Обычно наличие пейджинга, тем более в указанном в статье виде - признак плохого проектирования и плохой реализации, совсем не про идеальность.
Ну и, как правильно заметили, пейджинг довольно дорогой, особенно с сортировкой.
Человек все равно не может проанализировать более сотни строк, ему нужны инструменты для уточнения поиска,
Единственное достоинство пагинации - это простота реализации. Пагинация с простыми фильтрами и сортировками может быть сделана за час джуном. Если данных немного то и работать быстро будет без индексов. И большую часть проектов это устроит.
Множественные фильтры, фасеты, годный UI для этого это уже совсем другие трудозатраты.
Ну, тут речь шла о миллионах - там уже не будет работать быстро. Но вообще, если к разработчику пришло требование "сделай пейджинг", то первой реакцией должно быть "укажи user story, в которой нужен именно пейджинг - т.е. "пользователь решает свою проблему просматривая больше 1000 строк, кликая на следующую страницу". Если таких user story нет - то задачу просто не нужно делать, просто была ошибка в постановке. И демонстрация таких ошибок в постановке - нормальная работа даже для джуна.
А уж сделать нормальный фильтр на небольшой базе - не сложнее, чем пейджинг. При этом будет гораздо удобнее. Ну а на большой (от единиц миллионов записей) все равно нужны другие архитектурные паттерны.
Реализация GeneralLogMessages
ну совсем уж кустарная и неправильная. Функция getFormatted принимает какие то разные параметры, хрен сразу пойми какие, не залезая в реализацию. Зачем там вообще enum, если используется только функционал формирования сообщения. Можно просто utils класс со статическими методами типа String buildNotFoundError(String resourceName, String path) и т.д.
Я привёл довольно простую реализацию, как вариант решения - типизировать Object до String, но речь идет не только об этом, я понимаю. Ваш подход звучит хорошо, пригодится тем, кто захочет воспроизвести статью на практике)
Спасибо большое за публикацию. Она оказалась достаточно полезной для меня. В частности узнал про аннотацию Lombok @FieldDefault, которая дает возможность не прописывать уровень доступа переменных.
Собираетесь ли вы при получении списка инцидентов делать фильтрацию и сортировку? Я считаю, что это также должно быть актуально при реализации сервиса.
@Enumerated(EnumType.STRING)
IncidentStatus status;
вариант не очень хороший, потому что строки занимают много места в базе + рефакторинг кода потребует миграции данных. лучше под такие enum заводить AttributeConverter
который сконвертирует его в число.
Использование enum для статусов - вообще очень стремная идея, так как очень сложно потом будет расширять список (а список статусов часто меняется).
а что еще использовать? ;) чтобы не было проблем используем AttributeConverter
как я писал и таким образом кастомизируем сопоставление того что в базе с энамом
Ну вот смотри, у тебя добавился еще один статус. Как и что нужно сделать в системе, чтобы обновить сервисы (желательно без останова)?
Обычно вместо enum в API - просто строчки (коды), при этом отдельная обвязка, которая как-то реализует контроль этих строчек с учетом конкретной бизнес-логики (просто сохранить что пришло; подменить неизвестное на default; выкинуть ошибку, но все равно сохранить; использовать внешний маппинг) и т.п.
Формально, изменение состава enum - это несовместимое изменение в API. Для RPC нужно заводить новый entrypoint, а в REST-like, увы, вообще нет нормальных подходов для версионирования.
Ну вот смотри, у тебя добавился еще один статус. Как и что нужно сделать в системе, чтобы обновить сервисы (желательно без останова)?
Суровый энтерпрайз не везде нужен. Добавил поле перезагрузил сервер, дел на пару минут. Убрать правда так просто не выйдет.
А вот если надо добавлять часто, то тут действительно стоит подумать над другими решениями.
Тогда нельзя говорить о "микросервисах", так как МСА - это и про независимый деплой, а тут приходится всю систему останавливать и все сервисы правильных версий накатывать и запускать.
Хотя, конечно, мало кто умеет в независимый деплой, это достаточно сложно. Потому и большинство, кто пишет "микросервис" - на самом деле имеют в виду "кусочек кода, запускаемый в отдельном процессе", даже не про сервис в смысле SOA.
Я так и не понял что тут меняет наличие enum и на что вы его собрались менять. в третий раз пишу про AttributeConverter который занимается логикой конвертирования enum в число (и обратно) любым способом, базе лучше давать числа. Собственно речь шла про базу и то, как в нее пишется enum, автор пишет строкой имя enum от чего имеет множество проблем, начиная с того, что он в своем коде не может просто переименовать этот enum.
Обновление без даунтайма делается в 2 очевидных шага (релиза), вне зависимости что вы меняете. (а в клауде наверно можно просто поднять рядом новую версию и за раз переключиться на нее)
Если говорить про api, то тогда заводите версионирование через mysvice.com/v_/... чем это не нормальный подход к версионированию??! Или шлите версию в заголовке запроса. Ну или опять деплойте всю систему в 2 релиза, когда сначала сервисы умеют читать новое значение и вторым релизам начинают писать, тогда система не должна знать про версии api в принципе. Этот абзац опять не специфичен только для enum
Хм, базе на малых объемах (а тут речь идет явно о небольших базах, так как предлагаемые в статье решения не работают даже на миллионах строк) нет особой разницы - числа или строки. Хотя, конечно, писать enum в базу строкой - не всегда хорошая идея, в основном при десериализации из БД - и тут вполне можно использовать какой-то собственный парсер (тот же AttributeConverter - это не принципиально).
Но так как статья про API - меня больше смущает использование enum в API.
Да, если полностью контролировать и сам сервис и его пользователей и продакшен - то добавление нового статуса можно сделать через несколько связанных релизов. Если хотя бы одно условие не выполнено - то придется иметь две версии класса с разными Enum, что для REST-like API будет стоить довольно дорого (нужно переписать довольно много методов, фактически добавить сущность /incidents2/).
Для RPC есть, к счастью, возможность версионировать не сущность (и все-все методы работы с ней) и не весь API (как предлагают в подходах вида /api/v3/entity/), а конкретную ручку (/api/method/v1, например - там много вариантов).
И да, проблема не только в enum, но в любых несовместимых изменениях в API. Просто часто забывают, что добавление элемента в enum - как раз несовместимое изменение в протоколе.
Ну и про то, как делать выкладки без останова у меня целый доклад был на последнем Highload, кстати, уже опубликовали. Там, увы, дофига подводных камней (
Если спрингом базу делать, то да. Но если немного поколдовать со схемой то можно использовать Enum тип самой базы.
"Строки занимают много места" - это аргумент мало значимый в данном контексте. Строки могут занимать даже меньше, например число в Oracle всегда занимает 19 байт, даже если вы укажете допустимый диапазон 1..10, а строка CREATED - всего 7 байт.
Во-вторых, если предполагается только добавлять статусы, то никакого рефакторинга/миграции БД не потребуется.
В-третьих читать текстовые коды в сырых данных приятнее, чем запоминать числовые коды.
Поэтому AttributeConverter советую использовать только там, где это действительно необходимо - частые изменения статусов в кодовой базе.
Надо в pagination реализацию добавить sort. Иначе рискуете получить не то, например скипнуть записи. База не всегда хранит порядок
Угу, pagination без явной сортировки не работает, так как реляционная база вообще никогда не гарантирует порядок без явного указания режима сортировки.
Но так как pagination с сортировкой - очень дорогая операция, лучше бы вообще ее не делать.
Но так как pagination с сортировкой - очень дорогая операция, лучше бы вообще ее не делать.
От базы, схемы и существующих индексов зависит. Может быть дешёвой, может быть дорогой.
Ну, даже индексы не сильно спасают, увы (если не покрывающие)
Вот если база небольшая совсем и все равно будет table scan - тогда нормально. Но зачем тогда вообще пейджинг - на 1000 строчках-то?
Ну что-то делать всё равно где-то придётся. Или на клиенте или на сервере. Страницы с 1000 результатов это не очень удобно для пользователя.
Для маленьких проектов я бы делегировал это туда, где быстрее сделать.
Проще отправить эти 1000 строк на клиента - и не заставлять API зависеть от UXа клиентского приложения (которое может завтра поменяться).
Но тут уже нужно говорить о паттерне bff, о проектировании системы целиком, о качестве API - все то, что явно не попадает в контекст данной статьи (которая вообще не про качество API и не про качество реализации)
Обычно все просто - если есть разлапистый фронтенд, на котором отдельная команда лепит всякие графики, градусники и прочие статусбары, в отрыве от всего остального мира, то имеет смысл отдавать на фронт весь датасет, и пусть уже фронт разбирается какую его часть и в каком виде показать пользователю. Если же данных много, и пропихивать их через сеть все разом не рационально, то обработка происходит на сервере.
В реальной жизни, как правило, я сталкивался с чем-то средним - на сервере датасет грубо ограничивается до каких-то вменяемых размеров, а тонкая фильтрация происходит уже на клиенте, за счет чего пользователь испытывает счастье от отзывчивого интерфейса и ощущения "информации на кончиках пальцев".
Звучит хорошо.
Работал "рядом" с проектом, где так и декларировали. Проблема была, что бэкенд был, без модных наваротов, типа перекладываения данных по DTO. Берёшь из базы передаёшь на фронтенд. Поменял поле, фронт отвалился.
С фильрацией до вменяемых данных тоже были проблемы.
Реальных пользователей на той стадии еще небыло как и ощущения счастья тоже. До релиза я не дождался. Бэк поправили, с фильтрацие скорее всего тоже, там долго все возможные значения для автокомлита загружались.
Работал "рядом" с проектом, где так и декларировали. Проблема была, что бэкенд был, без модных наваротов, типа перекладываения данных по DTO. Берёшь из базы передаёшь на фронтенд. Поменял поле, фронт отвалился.
Но это не "модные навороты", уже сто лет есть конвертеры, которые все это делают за программиста. Например тут: https://modelmapper.org/getting-started/ Эта либа как раз предназначена для безпроблемного перегона entity в dto, хотя ее можно использовать буквально что б что угодно в что угодно конвертить. То есть в процитированном случае был не "бэк без новомодных фишек", а, простите, "джунов посадили писать проект и они сделали, как показывали на курсах от скиллбокса" со всеми вытекающими.
Как простой пример проектирования rest api статья вполне неплохая, но в реальных продакшн-условиях гораздо проще и эффективнее использовать Spring Data Rest - все, что реализовано в этой статье, там будет умещаться в реализации репозитория, т.е. без кода вообще.
Честно говоря не пойму почему возврат сущностей из БД в rest выдаче для CRUD сервисов - это антипаттерн. CRUD он на то и CRUD - просто набор данных без какой либо логики. Это то, что называется anemic domain model, и я не понимаю какой профит в перегоне одних структур данных в точно такие же, но с DTO в названии.
Честно говоря не пойму почему возврат сущностей из БД в rest выдаче для CRUD сервисов - это антипаттерн
Потому что переименование поля в таблице - сугубо внутреннее действие - ломает внешнее api. Потому что добавление поля в таблицу - сугубо внутреннее действие - делает его доступным внешнему api. Иными словами любое действие над нутрянкой может негативно сказаться на внешнем контракте - потому и антипаттерн.
Мапинг можно делать сразу, не перегоняя типы.
https://www.javaguides.net/2023/07/jpa-column-annotation.html
И как аннотация @Columnспасет от проблем при, например, переименовании поля в базе? Никак - либо поле в entity переименуется вслед за полем таблицы, что поломает внешнее апи в том случае если энтити выдается потребителю наружу, либо поле в энтити будет называться не так, как называется поле в БД, что вызовет массу вопросов у тех, кто будет поддерживать код. Эй, ребят, а вы не знаете, почему у нас поле в котором лежит отчество называется name? А, это что б потребителям контракт не сломать... Эх, была бы промежуточная dto на которой можно было бы разрулить проблему маппинга!
И как аннотация @Columnспасет от проблем при, например, переименовании поля в базе? Никак - либо поле в entity переименуется вслед за полем таблицы, что поломает внешнее апи в том случае если энтити выдается потребителю наружу, либо поле в энтити будет называться не так, как называется поле в БД, что вызовет массу вопросов у тех, кто будет поддерживать код.
Да будет называться не так как в ДБ. Значительно лучше, чем ломать АПИ.
Статья уровня Beginner Tutorial, а где среднего уровня контент?
Спасибо большое за твою большую проделанную работу. Подскажи, пожалуйста, когда будет продолжение?)
Спасибо, к началу февраля планировал
https://habr.com/ru/companies/ruvds/articles/912502/
Вышла вторая часть с небольшой задержкой)
Спасибо за статью! А будет ли ссылка на репозиторий?)
https://github.com/br0mberg/SupportDesk-IncidentService
Привет, и вышла вторая часть в профиле
Часть 1: Как я создал идеальный REST API — микросервис инцидентов на Java и Spring