1) Навіщо взагалі потрібен Service (а не “все в контролері”)
На малих прикладах здається, що можна робити все в контролері: прийняв запит → викликав репозиторій → повернув відповідь. Але в реальному проєкті так швидко починається хаос.
- Controller має бути тонким: HTTP-вхід/вихід (Request/Response).
- Repository має бути простим: CRUD і доступ до даних.
- Service має бути розумним: правила, перевірки, логіка.
Правило для студентів: якщо ти відчуваєш, що в контролері з’являються перевірки, if-и, додаткова логіка — значить це треба винести в Service.
2) Схема шарів (як це працює в Spring Boot проєкті)
Ідея: кожен шар робить своє. Якщо потім ти додаси нові правила (наприклад “email має бути унікальним”) — ти змінюєш Service, а не переписуєш весь проєкт.
3) Розбір @Service: що це за анотація і що вона дає
@Service — це “мітка”, що клас є сервісом (Spring Bean).
- Spring автоматично знаходить цей клас під час сканування пакетів.
- Створює об’єкт (bean) і керує його життєвим циклом.
- Дозволяє іншим класам “вколоти” (inject) цей сервіс.
Технічно @Service — це спеціалізація @Component. Але @Service читається людиною: “це бізнес-логіка”.
4) Dependency Injection: чому ми передаємо repository в конструктор
Ось ключова частина:
- Spring сам створює об’єкт UserRepository (проксі).
- Потім, коли створює UserService, передає репозиторій у конструктор.
- Ти не пишеш new UserRepository() — це робить контейнер Spring.
Чому саме конструктор: це найбільш “чистий” і надійний варіант. Поле стає обов’язковим, сервіс не може існувати без репозиторію.
5) Чому поле зроблено final
- final означає: після ініціалізації поле не можна переприсвоїти.
- це зменшує кількість помилок (хтось випадково не затре репозиторій).
- це хороший стиль для DI: залежності мають бути незмінні.
У нормальному проєкті “залежності” сервісу — це константа: сьогодні userRepository, завтра він не має стати іншим.
6) Розбір методів сервісу (що робить кожен)
getAllUsers()
- повертає список всіх користувачів з таблиці users
- під капотом findAll() → SQL типу SELECT * FROM users
- це “читання” (read) з БД
Тут логіки поки мало — але це правильне місце, щоб додати її потім (фільтри, сортування, пагінацію).
createUser(User user)
- зберігає користувача в БД
- save() зробить INSERT, якщо id немає
- після збереження об’єкт може повернутись вже з заповненим id
Саме тут найчастіше з’являються перевірки: валідність email, унікальність, мінімальна довжина імені.
7) Де саме має бути “реальна логіка” (приклади для пояснення студентам)
Зараз сервіс просто викликає репозиторій. Але в реальності тут буде, наприклад:
- перевірка, що email не порожній і має нормальний формат;
- перевірка, що такого email ще немає: userRepository.findByEmail();
- нормалізація: trim(), toLowerCase() для email;
- обробка помилок і повернення зрозумілої відповіді клієнту;
- логування: хто створив користувача, коли, з яким email;
- транзакції: якщо треба виконати кілька дій як один “атомарний” крок.
Висновок: Service — це місце, де система стає “розумною”. Repository лише зчитує/пише, а Service вирішує “можна чи не можна”.
8) Анотація @Transactional (важливо, але можна підключати пізніше)
Часто сервісні методи позначають @Transactional. Це означає: виконати всі дії всередині методу як одну транзакцію.
- якщо всередині методу кілька операцій — вони або всі успішні, або всі відкотяться;
- це критично для грошей, статусів, складних сценаріїв.
На цій практичній можна не заглиблюватися, але важливо, щоб студенти знали: транзакції — це відповідальність Service-шару.
9) Типові помилки (попередження)
- Controller напряму викликає Repository → логіка роз’їжджається по всьому проєкту.
- Service починає будувати HTTP-відповідь (ResponseEntity) → це вже зона Controller.
- Валідації немає ніде → в БД з’являються “сміттєві” дані.
Підсумок: ми створили UserService як правильний шар між Controller і Repository. Поки він простий, але саме тут потім з’являться перевірки, правила і “реальна логіка”.