Чай, Java, покодим? Разрабатываем микросервис на Spring+Java+PostgreSQL. 1 часть

На этом стриме я покажу свой архитектурный подход к разработке микросервиса на стеке Spring+Java+PostgreSQL.
В 1 части этого цикла мы организуем наш сервис в соответствии с подходом MVC. Для этого создадим DTO представление наших сущностей базы данных, и стандартные контроллеры для возможности работы с ними.
Поддержать автора - www.donationalerts.com/r/maycode
Мой Telegramm - t.me/may_code
Мой GitHub - github.com/FriMay
Отличная группа для Java разработчика (фичи, туториалы, статьи) - javatutorial

Пікірлер: 77

  • @user-br4gt7xu2j
    @user-br4gt7xu2j3 жыл бұрын

    Интересное видео: тебя хорошо обучили писать сервисы, но есть пара замечаний: 1. FieldDefaults может нормально так сбивать с толку, особенно в крупных классах с кучей описаний. Не советовал бы его использовать, тем более прилюдно - а то будут молодые умы к плохому привыкать) 2. Snake case в коде только для SQL и констант, - в контроллерах, Jackson-описаниях и пр., разумеется, ему не место. Насчет schoolClass вместо class - отличное решение (вообще на будущее "класс" по-английски grade) 3. Пакеты всегда именуются в ед. числе 4. Никакой логики в пакете entity(model) - это прямое нарушение конвенций - ожидаешь увидеть там POJO, максимум модифицированные аксессоры (и то спорно) - очень сбивает с толку 5. В Response стоит возвращать более информативные коды (при создании 201, при удалении 204 и т.д.) 6. Сервис - слой придумали не идиоты. Он необходим для описания логики(маппинг, преобразования, обработка), а слой контроллеров только для приема, валидации и передачи ГОТОВЫХ request и response. Пренебрегать им - ужасная практика. Если будешь писать крупный проект - увидишь огромное количество Swagger, OpenAPI и JavaDoc описаний и поймешь, посему писать там малейшую логику - крайне неверный подход 7. По схожей причине @Transactional неразумно указывать на уровне класса (тем более на уровне контроллера), только на уровне необходимых методов сервисного слоя, где можно задать частные настройки транзакций для частных операций. Частая ошибка новичка - пытаться сделать код компактнее. Не надо так делать. Код в 1 очередь должен быть формализован, иначе - это будет бардак(один пишет так, вотрой так, третий так и каждый разбирается в том, что нагородил другой) 8. При частичном поиске в базе (через like) не обязательно писать JPQL запрос: у Spring Data Jpa генерируются такие запросы через ключевую часть слова -like прямо в методе (например findAllByNameLike(String name)), иначе зачем тогда юзать Data Jpa, если вручную хочется пописать запросы, хотя я и сам так грешу: часто можно оптимизировать запрос) 9. Хранить и передавать сущности в БД стоит строго по ID, не нарушая НФ и здравый смысл. Опять же передавать по имени школу - практика неформальная, а значит неверная. Неразумно стремиться упростить себе написание кода на полчаса-час и тем самым усложнить весь процесс программирования в разы (в том числе себе в будущем), распространяя такое неформальное поведение среди бэкэнда. Это не говоря уже о том, что получая школы по name эти самые name надо бы проиндексировать.. 10. При удалении в общем случае возвращается код 204 и сообщение о том, что школа с таким то ID удалена, либо код 404 и сообщение, что не найдена и не надо ничего выдумывать. Также существует не hard удаление, а soft удаление (когда просто полю deleted в БД устанавливается true) для возможности восстановления в дальнейшем, но это опционально обычно, а инициатива наказуема) 11. Букву и цифру класса было бы удобнее сделать отдельными переменными при необходимости получения определенного уровня (н-р только 8-мые классы и т.п.) 12. Для маппинга dto удобнее пользовать mapstruct, а не фабрики. Чаще всего он и пользуется спросом. 13. Стоит подтянуть знания в работе Hibernate ибо без этого никуда, а у тебя с простейшими описаниями траблы. Т.е. их нет. Плюс в твоем коде вообще неверно описаны свзи OneToMany (помимо аннотации Join надо описывать правила добавления и удаления элементов в коллекциях, например в сеттерах, а у тебя сеттер ломбоковский, который не будет работать, если описать просто чуть более сложную логику с этими сущностями). Любой контроллер update или patch может сейчас поломать твой сервис) 14. Стоит разобраться с DataTime API. Везде ж используется. Instant - аналог старого Date - используется для работе с системным временем, аналогом старого Calendar является ZonedDateTime(метка времени с указанием временной зоны, временных сдвигов и правил), тоже самое, но без зон и сдвигов выполняют временные кортежи LocalDate, LocalTime и LocalDateTime - вот они тебе тут и подходят(точнее LocalDate, т.к. для даты рождения достаточно кортежа даты). Instant использовать тут некорректно 15. Именования url - отдельная история. Все не по ReST) можно же почитать про это) придерживайся стандартов!) 16. Насчет optional уже писали, не буду повторяться, но в if-else конструкциях все же лучше стараться вместо != null не использовать. Есть же методы Objects.nonNull() и т.п.. 17. Слов типа user, test, class и прочих в коде не стоит использовать в качестве именований без контекста - плохая практика. Если нет контекста, то стоит конкретизировать по возможности, т.к. более наглядно и меньше конфликтов. 18. В качестве ID стоит использовать UUID, а не Long. Без комментариев - тип для этого создан Вцелом из грубых ошибок только отсутствие описаний для работы со связями сущностей и отсутствие индексации целевых полей БД. Видно, что не сталкивался в такими проблемами, не писал крупных проектов, но имхо программист правильный) я бы взял в команду себе;)

  • @user-br4gt7xu2j

    @user-br4gt7xu2j

    3 жыл бұрын

    По поводу плясок с кодировками тоже стоило бы разобраться. Там все просто, есть 3 типа ошибок кодировки: 1) прямоугольники вместо символов(кол-ао равно кол-ву символов) - декодирование вх. потока байтов было успешным, но шрифт не содержит символов, к-ые требуется отобразить. Решение: добавление соответствующего unicode-шрифта 2) вопросительные знаки вместо символов - ошибка декодера вх. потока(н-р при попытке получить текст windows-1251 в кодировке UTF-8). Решение: указание соответствующего декодера для соответствия схем кодирования 3) твой случай (символы есть, но не те, причем их в 2-3 раза больше, чем надо) - декодер отработал без ошибо, но 2-4 байтовые символы интерпретированы, как однобайтовые (например русский текст в кодировке UTF-8 прочитали с использованием схемы windows-1251, отчего декодер каждый байт воспринимает как символ). Решение: либо использовать корректную схему при декодировании, либо произвести обратную операцию(получить исходный массив байтов с использованием той же кодировки и декодировать заново с использованием правильной схемы) удачи ;)

  • @frimay1

    @frimay1

    3 жыл бұрын

    2 пункт. В нашей компании принята практика передавать все данные клиенту в Snake case, это более наглядно разделяет передаваемые поля. В этой же серии стримов я просто пренебрег этим правилом, так как это учебный пет-проект, потому и не сильно принципиален был вопрос нейминга полей по кейсу. 6 пункт. Согласен, не идиоты, но и что идиоты я не говорил) В моей компании главный разработчик спустя годы работы над крупными проектами, которыми пользуется миллионы юзеров предпринял это решение и я с ним соглашусь, иногда сервисная логика бывает излишней, но вот с обработкой транзакций согласен, иногда когда её нужно кастомно обработать, без этого никак. 7 пункт. Собственно, частично ответил на этот вопрос в 6, у нас редко возникают проблемы с транзакционностью, чтобы их разрешать на уровне метода, отсюда и вытекает использование транзакционности на уровне класса. 8 пункт. Like позволяет работать регистронезависимо? Если да, тогда я думаю я откажусь от запроса типа LOWER() LIKE LOWER(), если нет, тогда тут очевидно, почему я использую именно такую конструкцию. 9 пункт. Я бы не назвал способ передачи школы по имени простым, так как добавляется дополнительная логика обработки, с точки зрения разработчика клиента это выглядит логичнее, и ему не приходится путаться в куче айдишников, да, клиент сам может обо всём позаботиться, но в нашей компании мы пытаемся комбинировать грань разумной простоты, и дичи, поэтому в случаях если можно обойти без id и упростить этим логику работы с контроллером мы так и делаем, в ином случае используем id. 10 пункт. Знаю про soft удаление, но здесь спокойно можно юзать hard. Так же согласен с кодами выдачи (201/204) думаю выкачу в дальнейшем это предложение руководству и мы слегка поменяем структуру работы под этот формат, звучит правильно. 11 пункт. Такой детализации не предусматривалось, потому я и не стал добавлять лишнюю логику, если будет необходимо такое разделение не сложно сделать. 12 пункт. Я пришел к этой структуре недавно, и когда тебе нужно использовать репозитории для формирования DTO, в таком случае удобнее использовать компоненты типа DTOFactory, в остальных случаях можно обойтись билдерами. 18 пункт. Здесь вряд ли требуется UUID, записей будет немного отсюда и большая уникальность не требуется, согласен с UUID при частой генерации данных. 19 пункт (доп). Опять таки, т.к. записей будет мало, индексацию и не встроил, уже были случаи медленной обработки данных из-за её отсутствия:) Спасибо огромное за ответ, какие смог пункты попробовал обосновать, всё таки я не просто так некоторые вещи опускал, над остальным попробую задуматься)

  • @user-br4gt7xu2j

    @user-br4gt7xu2j

    3 жыл бұрын

    @@frimay1 Ну сам -like не позволяет(это просто условие сравнения), а Spring Data Jpa, - разумеется. Например, если хочешь, чтоб поиск был прямо как у тебя (по частичному совпадению в любой части слова независимо от регистра), то тебе поможет такой шаблон генерации: findAllByFilterContainingIgnoreCase(String filter); Вообще эти шаблоны генерации - это самые азы работы механизма Spring Data.. Совсем не ознакамливаетесь с технологией, используя её?) это напрасно: самое интересное всегда находится чуть ниже поверхности ;) Советую все же ознакомиться с Mapstruct опционально(превосходно и изящно решает большинство вопросов по маппингу). А вот с Hibernate - настоятельно(в частности с нюансами настройки связей между сущностями), иначе работоспособность таких севисов иногда может быть под вопросом) И надеюсь тебе помогла моя ремарка относительно проблем с кодировками) там все просто на самом деле, если знаешь, как кодируются и декодируются символы)

  • @maycode0

    @maycode0

    3 жыл бұрын

    Хорошо, попробую в этом разобраться детальнее) спасибо за рекомендации

  • @ram0973

    @ram0973

    2 жыл бұрын

    а у вас нет какого-то проектика типа шаблона, со всеми лучшими практиками?

  • @nikitadevaev554
    @nikitadevaev5542 жыл бұрын

    Будто подкаст смотрю, отлично получается! Спасибо за контент!

  • @maycode0

    @maycode0

    2 жыл бұрын

    Спасибо)

  • @denisnurdinov9400
    @denisnurdinov9400 Жыл бұрын

    Автору большое спасибо за контент 👍 +подписка

  • @nikzim2132
    @nikzim2132 Жыл бұрын

    класс) спасибо учусь по твоим видио)

  • @dolor9096
    @dolor90962 жыл бұрын

    Благодарю, много чего нового узнал

  • @maycode0

    @maycode0

    2 жыл бұрын

    Рад, что видео помогло чем-то)

  • @LuckOnlY
    @LuckOnlY2 жыл бұрын

    Лично для меня такой формат очень полезен и интересен.

  • @maycode0

    @maycode0

    2 жыл бұрын

    Рад что было полезно, а можно узнать что именно понравилось?)

  • @LuckOnlY

    @LuckOnlY

    2 жыл бұрын

    @@maycode0 В целом про формат - мне, как новичку в java любопытно посмотреть как размышляет и решает задачу разработчик с опытом. Что касается технической части, то опять же, лично для меня супер полезно подсмотреть хотя бы те же аннотации, например FieldDefaults ) Общее впечатление - видео приятно смотреть, хорошее качество картинки, отличное содержание и приятный голос

  • @maycode0

    @maycode0

    2 жыл бұрын

    @@LuckOnlY спасибо за отзыв)

  • @eugenz2334
    @eugenz23347 ай бұрын

    53:20 Частенько раньше встречались на сайтах такие сговоры хитрых бэкендеров с фронтовиками, когда после удаления объекта обновляешь страницу, а он снова там)) Т.к. из-за глюка в бэкенде удаление не произошло, а фронт отрисовал удаление, не ориентируясь на подтверждение бэка)

  • @kirillkostinokei2
    @kirillkostinokei23 жыл бұрын

    Жду 2 часть!

  • @maycode0

    @maycode0

    3 жыл бұрын

    Постараюсь организовать

  • @maycode0

    @maycode0

    3 жыл бұрын

    А вот и вторая часть) Ссылка на анонс - kzread.info/dash/bejne/YoV5uNdphdfaYaw.html

  • @vovaslipchik6834
    @vovaslipchik68343 жыл бұрын

    с докером было бы интересно посмотреть

  • @user-hm8wx2us8l
    @user-hm8wx2us8l3 ай бұрын

    про вечер пятницы угарнул)

  • @maycode0

    @maycode0

    3 ай бұрын

    А что там было?) Освяжи память))))

  • @user-hm8wx2us8l

    @user-hm8wx2us8l

    3 ай бұрын

    То, что нормальные люди в пятницу вечером идут отдыхать, а не писать микросервисы 😁

  • @Incog05
    @Incog05 Жыл бұрын

    А где видел по разработке школы?

  • @user-de2dj8nu4g
    @user-de2dj8nu4g3 жыл бұрын

    Премного благодарен Автору!!! Жаль конечно, что код не выложен в репо((( Скринов уже 100500 наделал((( полно полезных плюшек для себя подчеркнул.

  • @maycode0

    @maycode0

    3 жыл бұрын

    Думаю я рано или поздно залью это в репозиторий)

  • @user-de2dj8nu4g

    @user-de2dj8nu4g

    3 жыл бұрын

    Жду с нетерпением)

  • @maycode0

    @maycode0

    2 жыл бұрын

    @@user-de2dj8nu4g немного поздноватое, но посмотри последний стрим, 2 часть, там все лежит в репозитории)

  • @maycode0

    @maycode0

    2 жыл бұрын

    @@user-de2dj8nu4g ссылка на гит с репозиторием в описании

  • @user-de2dj8nu4g

    @user-de2dj8nu4g

    2 жыл бұрын

    @@maycode0 я уже давно нашёл и разобрал на молекулы. Спасибо БОЛЬШОЕ:)

  • @PitPRG
    @PitPRG Жыл бұрын

    Попробуй Acessors(chain=true) использовать для entity и dto). По мне удобнее AllArgsConstructor, хотя и можно забыть проинициализировать отдельные поля

  • @maycode0

    @maycode0

    Жыл бұрын

    Я это большее использую в случаях когда идут шаблонные сеттеры, удобно объединять.

  • @krab9512
    @krab95123 жыл бұрын

    А исходники можно с GitHub?

  • @maycode0

    @maycode0

    3 жыл бұрын

    Скорее всего нет, этот проект хоть и разрабатывается как открытый, но он больше для частного пользования)

  • @o_qbert_o603
    @o_qbert_o6032 ай бұрын

    А почему вместо factory классов не использовать MapStruct? Там же идет преобразование entity в dto, и мне кажется что лучше повесить аннотацию, чем каждый раз описывать логику руками.

  • @maycode0

    @maycode0

    2 ай бұрын

    Привычка все контролировать вручную. Так все механизмы конвертации становятся намного прозрачнее

  • @user-sj6hl1ye4r
    @user-sj6hl1ye4r2 жыл бұрын

    39:00. Я думаю, что там ничего не оптимизируется, судя по коду, который был описан ранее. Выборка данных по второму условию LOWER(s.name) LIKE CONCAT(('%', :filter, '%')) будет выполняться всегда при совершении запроса. Если isFiltered равен FALSE (а ранее было написано, что оно будет равно false, когда строка пустая), то значение filter будет пустым (или содержать пробелы\невидимые символы, но я думаю, что она всегда будет равняться пустоте). А значит, выборка будет происходить по %% (все данные будут выведены). В ином случае все понятно. Если исходить из этого, то первая проверка (:isFilterted = FALSE) является бессмысленной. Не гарантирую, что это так, но мне кажется это логичным.

  • @maycode0

    @maycode0

    2 жыл бұрын

    Мне кажется все таки будет некая оптимизация, просто не будет происходить проверки по фильтру так как стоит оператор OR, и при успешном условии isFiltered=:FALSE оно просто не будет переходит к вычислению следующего оператора, но я тоже не могу гарантировать такое поведение) Проще сделать тесты и посмотреть время работы запросов, и думаю разница хоть и маленькая, но будет

  • @maycode0

    @maycode0

    2 жыл бұрын

    Просто мне кажется, если выражения в PostgreSQL подчиняются законам мат логики, то по сути у нас есть выражение: “A|B”, и в случае если выражение “A” истина, выражение “A|B” будет свернуто до “A”

  • @user-gy3ki4kp3o

    @user-gy3ki4kp3o

    2 жыл бұрын

    @@maycode0 а нельзя условие B вообще не добавлять в запрос если не используется фильтр? как-то криво с этими OR выглядит - зачем создавать лишние проблемы планировщику?

  • @maycode0

    @maycode0

    2 жыл бұрын

    Ну, можно, но если у тебя есть помимо этого критерии у тебя будет два запроса, разве это лучше?) А если ты ещё и считываешь количество записей, то уже целых 4

  • @maycode0

    @maycode0

    2 жыл бұрын

    Вообще лучше использовать для этого всего Example API

  • @champion_of_the_light
    @champion_of_the_light8 ай бұрын

    Transactional на уровне контроллера это конечно мем. А что не на уровне сразу сервиса?

  • @maycode0

    @maycode0

    8 ай бұрын

    Захотелось :)

  • @user-iu6yz6ck6h
    @user-iu6yz6ck6h3 жыл бұрын

    А продолжение будет?)

  • @maycode0

    @maycode0

    3 жыл бұрын

    Постараюсь организовать

  • @maycode0

    @maycode0

    3 жыл бұрын

    А вот и вторая часть) Ссылка на анонс - kzread.info/dash/bejne/YoV5uNdphdfaYaw.html

  • @user-ib8rv1vr4r
    @user-ib8rv1vr4r Жыл бұрын

    Вопрос такой: а это вообще частая практика выносить URL в константу? 59:49 Просто не видел такого раньше, если честно.

  • @maycode0

    @maycode0

    Жыл бұрын

    Вообще я часто это делаю. Очень полезно при интеграционном тестировании, ты уже вводишь не строку в MockMvc, а константу.

  • @user-ib8rv1vr4r

    @user-ib8rv1vr4r

    Жыл бұрын

    @@maycode0 Забавно. Должно быть удобно.)

  • @maycode0

    @maycode0

    Жыл бұрын

    Именно)

  • @javastream9414
    @javastream94143 жыл бұрын

    На 2:29:00 можно было использовать конструкцию ifPresentOrElse: testName.ifPresentOrElse( test::setName, () -> { throw new BadRequestException("Имя теста не может быть пустым!"); } );

  • @maycode0

    @maycode0

    3 жыл бұрын

    Она доступна с 11 вроде версии, или даже выше, я использовал 8) Про эту конструкцию недавно узнал, но не смог использовать, спасибо за идею)

  • @javastream9414
    @javastream94143 жыл бұрын

    Чать 2 будет?

  • @maycode0

    @maycode0

    3 жыл бұрын

    Думаю да, вроде есть интересующиеся люди)

  • @maycode0

    @maycode0

    3 жыл бұрын

    А вот и вторая часть) Ссылка на анонс - kzread.info/dash/bejne/YoV5uNdphdfaYaw.html

  • @javastream9414

    @javastream9414

    3 жыл бұрын

    @@maycode0 Спасибо, ждем)

  • @AB-ku1su
    @AB-ku1su Жыл бұрын

    Почему в базе нет таблицы для enum (школьник учитель родитель)

  • @maycode0

    @maycode0

    Жыл бұрын

    Потому что в ней нет особого смысла?) Для базовых примеров достаточно базовой функциональности и enum как строк в таблице, зачем выделять отдельную(таблицу)?

  • @PitPRG

    @PitPRG

    Жыл бұрын

    потому, что для быстродействия иногда используют денормализацию баз данных) Что быстрее - вытащить одну запись из БД или приджойнить ещё одну таблицу к каждой записи?

  • @maycode0

    @maycode0

    Жыл бұрын

    Хорошая мысль)

  • @maycode0

    @maycode0

    Жыл бұрын

    Но если данных много, потенциально это может выродится в проблемы с памятью. У всего своя цена)

Келесі