diff --git a/.gitignore b/.gitignore index 73e6d9e3dd1..b4860155a09 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,4 @@ out target *.iml log -*.patch - - +*.patch \ No newline at end of file diff --git a/README.md b/README.md index 4e07fe385b2..7eba7e8652f 100644 --- a/README.md +++ b/README.md @@ -1,251 +1,148 @@ -[Java Enterprise Online Project](https://javaops.ru/view/topjava) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/bee16f3145654047a0505c62aeefd8a2)](https://app.codacy.com/gh/JavaWebinar/topjava/dashboard) +Java Enterprise Online Project =============================== -Разработка полнофункционального Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей с использованием наиболее популярных инструментов и технологий Java: Maven, Spring MVC, Security, JPA(Hibernate), REST(Jackson), Bootstrap (css,js), DataTables, jQuery + plugins, Java 8 Stream and Time API и хранением в базах данных Postgresql и HSQLDB. -![topjava_structure](https://javaops.ru/static/images/projects/top-scheme.jpg) - - Когда вы слышите что-то, вы забываете это. - Когда вы видите что-то, вы запоминаете это. - Но только когда вы начинаете делать это, - вы начинаете понимать это - - Старинная китайская поговорка - -## Описание и план проекта -### Демо разрабатываемого приложения -### [Изменения проекта (Release Notes)](ReleaseNotes.md) -### Требования к участникам, Wiki -### Составление резюме, подготовка к интервью, поиск работы - -Обновленное вводное занятие (обязательно смотреть все видео) -=============== -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. [Вступление, история, команда, источники](doc/video1.md) - -## Обзор наиболее востребованных технологий, которые будут изучаться на курсе TopJava -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2.1. [Часть 1: инфраструктура](doc/video2.1.md) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2.2. [Часть 2: frameworks Spring, ORM](doc/video2.2.md) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2.3. [Часть 3: тренды](doc/video2.3.md) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2.3. [Часть 4: обзор разрабатываемого приложения](doc/video2.4.md) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. [Рекомендуемые подходы к обучению на курсе](doc/video3.md) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. [Структура приложения (многоуровневая архитектура)](doc/video4.md) -### [Демо приложения](http://javaops-demo.ru/topjava) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. [Системы управления версиями. Git](doc/video5-vcs-git.md) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Работа с проектом (выполнять инструкции) -- **ВНИМАНИЕ: выбирайте для проекта простой пусть без пробелов и русских букв, например, `c:\projects\topjava\` (Windows). Иначе впоследствии будут проблемы** -- **Плагин Git Intergation уже не требуется, а вкладку `Version control` в IDEA переименовали в `Git`** - -Для переключения режима отображения изменений из вкладки `Commit` в `Git: Local Changes` нужно переключить `Settings/Preferences | Version Control | Commit | Use non-modal commit interface` или в контекстном меню вкладки `Commit`: - -![image](https://user-images.githubusercontent.com/13649199/105491518-72d8f300-5cc7-11eb-8b79-c46382562deb.png) ![image](https://user-images.githubusercontent.com/13649199/105488663-05c35e80-5cc3-11eb-962e-30f403d623e8.png) - -### Патч [prepare_to_HW0.patch](https://drive.google.com/file/d/1LNPpu9OkuCpfpD8ZJHO-o0vwu49p2i5M) (скачать и положить в каталог вашего проекта) - -> Проект постоянно улучшается, поэтому видео иногда отличается от кода проекта. Изменения указываю после видео: -> - переименовал класс `UserMealWithExceed` и его поле `exceed` в `UserMealWithExcess.excess` -> - в `UserMeals/UserMealWithExcess` поля изменились на `private` -> - обновил данные `UserMealsUtil.meals` и переименовал некоторые переменные, поля и методы -> - добавил `UserMealWithExcess.toString()` и метод для выполнения _Optional домашнего задания_ -> - метод фильтрации в `TimeUtil` переименовал в `isBetweenHalfOpen` (также изменилась логика сравнения: `startTime` включается в интервал, а `endTime` - не включается) - -### GitHub поменял политику: теперь пушить нужно через токен. IDEA предложит его сгенерировать при пуше, или можно [создать токен в настройках](https://www.jetbrains.com/help/idea/github.html#register-account) -- [Способы авторизации в GitHub](https://topjava.ru/blog/vvedeniye-v-git-github-ustanovka-i-nastroyka#6) +Наиболее востребованные технологии /инструменты / фреймворки Java Enterprise: +Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + plugins. + +- [Вступительное занятие](https://github.com/JavaOPs/topjava) +- [Описание и план проекта](https://github.com/JavaOPs/topjava/blob/master/description.md) +- [Wiki](https://github.com/JavaOPs/topjava/wiki) +- [Wiki Git](https://github.com/JavaOPs/topjava/wiki/Git) +- [Wiki IDEA](https://github.com/JavaOPs/topjava/wiki/IDEA) +- [Демо разрабатываемого приложения](http://javaops-demo.ru/topjava) + +### 25.01: Старт проекта +- Начало проверки [вступительного задания HW0](https://github.com/JavaOPs/topjava#-Домашнее-задание-hw0) + +#### 30.01 Дедлайн на сдачу HW0 +### 01.02: 1-е занятие +- Разбор домашнего задания вступительного занятия (вместе с Optional) +- Обзор используемых в проекте технологий. Интеграция ПО +- Maven +- WAR. Веб-контейнер Tomcat. Сервлеты +- Логирование +- Уровни и зависимости логгирования. JMX +- Домашнее задание 1-го занятия (HW1 + Optional) + +#### 08.02 Дедлайн подачи заявки на [дипломную программу](https://javaops.ru/view/register/diploma) +### 08.02: 2-е занятие +- Разбор домашнего задания HW1 + Optional +- Библиотека vs Фреймворк. Стандартные библиотеки Apache Commons, Guava +- Слои приложения. Создание каркаса приложения +- Обзор Spring Framework. Spring Context +- Пояснения к HW2. Обработка Autowired +- Домашнее задание (HW2 + Optional) + +### 15.02: 3-е занятие +- Разбор домашнего задания HW2 + Optional +- Жизненный цикл Spring контекста +- Тестирование через JUnit +- Spring Test +- Базы данных. Обзор NoSQL и Java persistence solution без ORM +- Установка PostgreSQL. Docker +- Настройка Database в IDEA +- Скрипты инициализации базы. Spring Jdbc Template +- Тестирование UserService через AssertJ +- Логирование тестов +- Домашнее задание (HW3 + Optional) + +### 22.02: 4-е занятие +- Разбор домашнего задания HW3 + Optional +- Методы улучшения качества кода +- Spring: инициализация и популирование DB +- Подмена контекста при тестировании +- ORM. Hibernate. JPA +- Поддержка HSQLDB +- Домашнее задание (HW4 + Optional) +#### Начало выполнения [выпускного проекта](https://github.com/JavaOPs/topjava/blob/master/graduation.md) + +### 29.02: 5-е занятие +- Обзор JDK 9/17. Миграция Topjava с 1.8 на 17 +- Разбор вопросов +- Разбор домашнего задания HW4 + Optional +- Транзакции +- Профили Maven и Spring +- Пул коннектов +- Spring Data JPA +- Spring кэш +- Домашнее задание (HW5 + Optional) + +### 07.03: 6-е занятие +- Разбор домашнего задания HW5 + Optional +- Кэш Hibernate +- Spring Web +- JSP, JSTL, internationalization +- Динамическое изменение профиля при запуске +- Конфигурирование Tomcat через maven plugin. Jndi-lookup +- Spring Web MVC +- Spring Internationalization +- Домашнее задание (HW6 + Optional) + +#### Большое ДЗ + выпускной проект + начинаем [курс BootJava](https://javaops.ru/view/bootjava) + подтягиваем "хвосты". + +### 21.03: 7-е занятие +- Разбор домашнего задания HW6 + Optional +- Автогенерация DDL по модели +- Тестирование Spring MVC +- Миграция на JUnit 5 +- Принципы REST. REST контроллеры +- Тестирование REST контроллеров. Jackson +- jackson-datatype-hibernate. Тестирование через матчеры +- Тестирование через SoapUi. UTF-8 +- Домашнее задание (HW7 + Optional) + +### 28.03: 8-е занятие +- Разбор домашнего задания HW7 + Optional +- WebJars. jQuery и JavaScript frameworks +- Bootstrap +- AJAX. Datatables. jQuery +- jQuery notifications plugin +- Добавление Spring Security +- Домашнее задание (HW8 + Optional) + +### 04.04: 9-е занятие +- Разбор домашнего задания HW8 + Optional +- Spring Binding +- Spring Validation +- Перевод DataTables на Ajax +- Форма login / logout +- Реализация собственного провайдера авторицазии +- Принцип работы Spring Security. Проксирование +- Spring Security Test +- Cookie. Session +- Домашнее задание (HW9 + Optional) + +### 11.04: 10-е занятие +- Разбор домашнего задания HW10 + Optional +- Кастомизация JSON (@JsonView) и валидации (groups) +- Рефакторинг: jQuery конверторы и группы валидации по умолчанию +- Spring Security Taglib. Method Security Expressions +- Интерсепторы. Редактирование профиля. JSP tag files +- Форма регистрации +- Обработка исключений в Spring +- Encoding password +- Миграция на Spring 5 +- Защита от межсайтовой подделки запросов (CSRF) +- Домашнее задание (HW10) + +### 18.04: 11-е занятие +- Разбор домашнего задания HW10 + Optional +- Локализация datatables, ошибок валидации +- Защита от XSS (Cross Site Scripting) +- Обработка ошибок 404 (NotFound) +- Доступ к AuthorizedUser +- Ограничение модификации пользователей +- Деплой приложения [на собственный выделенный сервер](https://github.com/JavaOPs/startup) +- Домашнее задание (HW11): сокрытия полей в Swagger +- Составление резюме. Собеседование. Разработка ПО. Возможные доработки приложения -## Инструкция по шагам (из видео): -- Установить ПО (Git, JDK8, IntelliJ IDEA, Maven) -- Создать аккаунт на GitHub -- Сделать Fork **ЭТОГО** проекта (https://github.com/JavaOPs/topjava) -- Сделать локальный репозиторий проекта: -
git clone https://github.com/[Ваш аккаунт]/topjava.git
- -> Вместо Fork можно сделать [клонирование проекта](https://github.com/JavaOPs/topjava/wiki/Git#user-content-Клонирование-проекта): он не будет привязан к исходному https://github.com/JavaOPs/topjava и у него не будет истории. - -- Открыть и настроить проект в IDEA - - Выставить кодировку UTF-8 в консоли - - Поставить кодировку UTF-8 - - Опционально: поменять шрифт по умолчанию на DejaVu или на **новый [JetBrains Mono](https://habr.com/ru/company/jugru/news/t/484134/)** -- По ходу видео сделать `Apply Patch...` скачанного патча `Prepare_to_HW0.patch` -- Закоммитить и запушить изменения (`commit` + `push`) -- Сделать ветку домашнего задания -- Выполнить задание и залить на GitHub (`commit` + `push`) -- Переключиться в основную ветку проекта `master`. - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. [Maven](https://drive.google.com/file/d/1qEJTwv9FNUQjx-y9MSydH01xaAne0-hu) -- [Как установить Maven 3 на Ubuntu или Windows](https://devcolibri.com/как-установить-maven-3-на-ubuntu-или-windows/) -- [Руководство по Maven](https://topjava.ru/blog/apache-maven-osnovy-1) -- Wiki: [Apache Maven](https://ru.wikipedia.org/wiki/Apache_Maven) -- [The Central Repository](http://search.maven.org) -- Дополнительно: - - [Мой Wiki по Maven](https://github.com/JavaOPs/topjava/wiki/Maven) - - [Основы Maven](https://www.youtube.com/watch?v=0uwMKktzixU) - - JavaRush: [Основы Maven](https://javarush.ru/groups/posts/2523-chastjh-4osnovih-maven) - - Инструмент сборки проектов [Maven](https://www.examclouds.com/ru/java/java-core-russian/lesson20) - - [Maven Getting Started Guide](https://maven.apache.org/guides/getting-started/index.html) - - [Видео: Maven vs Gradle vs SBT (Архипов, Борисов, Садогурский)](https://www.youtube.com/watch?v=21qdRgFsTy0) - - [Build Lifecycle](http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html) - - [Dependency Mechanism](http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. [Как правильно относиться к техзаданию (ТЗ). Полуоткрытый интервал.](https://drive.google.com/file/d/1BpTzjNFjS0TSekCyt_xvt6YoLvuw5KTZ) -- [Типы промежутков](https://ru.wikipedia.org/wiki/Промежуток_(математика)) - -## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW0 - -### ВНИМАНИЕ: НЕ НАДО в репозиторий делать Pull Request со своими решениями! См. видео выше ("Работа с проектом") - -Реализовать метод `UserMealsUtil.filteredByCycles` через циклы (`forEach`): -- должны возвращаться только записи между `startTime` и `endTime` -- поле `UserMealWithExcess.excess` должно показывать, превышает ли сумма калорий за весь день значение `caloriesPerDay` - -Т. е. `UserMealWithExcess` - это запись одной еды, но поле `excess` будет одинаково для всех записей за этот день. - -> - Проверьте результат выполнения ДЗ (можно проверить логику в [http://javaops-demo.ru/topjava](http://javaops-demo.ru/topjava), список еды) -> - Оцените Time complexity алгоритма. Если она больше O(N), например O(N*N) или N*log(N), сделайте O(N). -> **Внимание: внимательно прочитайте про O(N). O - это любой коэффициент, 2*N это тоже O(N).** - -- Java 8 Date and Time API -- Алгоритмы и структуры данных для начинающих: сложность алгоритмов -- [Сложность алгоритмов и Big O Notation](https://threadreaderapp.com/thread/1470666237286010881) -- [Головач: сложность алгоритмов в теме коллекций](https://www.youtube.com/watch?v=Ek9ijOiplNE&feature=youtu.be&t=778) -- Time complexity -- Временная сложность алгоритма -- Вычислительная сложность - -#### ВНИМАНИЕ: варианты Optional делайте в одной ветке в разных методах `UserMealsUtil`. Проще делать, проще проверять - -### Optional (Java 8 Stream API) -``` -Реализовать метод `UserMealsUtil.filteredByStreams` через Java 8 Stream API. -``` -- Видео: Доступно о Java 8 Lambda -- Java 8: Lambda выражения -- Java 8: Потоки -- Pуководство по Java 8 Stream -- [Полное руководство по Java 8 Stream API в картинках и примерах](https://annimon.com/article/2778) -- [7 способов использовать groupingBy в Stream API](https://habrahabr.ru/post/348536) -- Лямбда-выражения в Java 8 -- A Guide to Java 8 -- Шпаргалка Java Stream API -- Алексей Владыкин: Элементы функционального программирования в Java -- Yakov Fain о новом в Java 8 -- stream.map vs forEach` - - без циклов по другим коллекциям/массивам (к ним также относим методы коллекций `addAll()/removeAll()`) -- через Stream API за 1 проход по исходному списку `meals.stream()` - - нельзя использовать внешние коллекции, не являющиеся частью коллектора - - возможно дополнительные проходы по частям списка, при этом превышение должно считаться один раз для всего подсписка. Те например нельзя разбить список на на 2 подсписка с четными и нечетными датами и затем их объединить, с подсчетом превышения для каждого элемента. - -Временная сложность реализации должна быть O(N) (обратите внимание на п. 13 замечаний) -Решение должно быть рабочим в общем случае (должно работать в приложении с многими пользователями, не только при запуске `main`) -Нельзя 2 раза проходить по исходному списку (в том числе по его отфильтрованной или преобразованной копии) - -Ресурсы: -- [Baeldung: Custom Collectors](https://www.baeldung.com/java-8-collectors#Custom) -- [Руководство по Java 8 Stream API: Collector](https://annimon.com/article/2778#collector) -- [Хватит писать циклы! Топ-10 лучших методов для работы с коллекциями из Java 8](https://javarush.ru/groups/posts/524-khvatit-pisatjh-ciklih-top-10-luchshikh-metodov-dlja-rabotih-s-kollekcijami-iz-java8) -- [Понять Java Stream API](https://vc.ru/u/604567-yerlan-akzhanov/194409-ponyat-java-stream-api) - -### Замечания по использованию Stream API: -- Когда встречаешь что-то непривычное, приходится перестраивать мозги. Например, переход с процедурного на ООП-программирование дается непросто. Те, кто не знает шаблонов (и не хотят учить), также их встречают плохо. Хорошая новость в том, что если это принять и начать использовать, то начинаешь получать от этого удовольствие. И тут главное не впасть в другую крайность: - - [Используйте Stream API проще (или не используйте вообще)](https://habrahabr.ru/post/337350/) -- Если вас беспокоит производительность стримов, обязательно прочитайте про оптимизацию - - ["Что? Где? Когда?"](http://optimization.guide/intro.html) - - [Перформанс: что в имени тебе моём?](https://habrahabr.ru/company/jugru/blog/338732/) - - [Performance это праздник](https://habrahabr.ru/post/326242/) - -При использовании Stream API производительность улучшится только на больших задачах, где возможно распараллеливание. -Еще: просто так запустить и померить скорость JVM нельзя (как минимум надо дать прогреться и запустить очень большое число раз). Лучше использовать какие-нибудь бенчмарки, например [JMH](http://tutorials.jenkov.com/java-performance/jmh.html), который мы используем на другом проекте (Mastejava). - -## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Замечания к HW0 -- 1: Код проекта менять можно! Одна из распространенных ошибок как в тестовых заданиях на собеседовании, так и при работе на проекте, что ничего нельзя менять. Конечно, при правках в рабочем проекте обязательно нужно проконсультироваться/проревьюироваться у авторов кода (находятся по истории VCS) -- 2: Наследовать `UserMealWithExcess` от `UserMeal` нельзя, т. к. это разные сущности: Transfer Object и Entity. Мы будем их проходить на 2-м уроке. Это относится и к их зависимости друг от друга. -- 3: Правильная реализация должна быть простой и красивой, можно сделать 2-мя способами: через стримы и через циклы. Сложность должна быть O(N), т. е. без вложенных стримов и циклов. -- 4: При реализации через циклы посмотрите в `Map` на методы `getOrDefault` или `merge` -- 5: **При реализации через `Stream` заменяйте `forEach` оператором `stream.map(..)`** -- 6: Объявляйте переменные непосредственно перед использованием (если возможно - сразу с инициализацией). При объявлении коллекций в качестве типа переменной используйте интерфейс (Map, List, ..) -- 7: Если IDEA предлагает оптимизацию (желтым подчеркивает), например, заменить лямбду на ссылку на метод (method reference), соглашайтесь (Alt+Enter) -- 8: Пользуйтесь форматированием кода в IDEA: `Alt+Ctrl+L` -- 9: Перед check-in (отправкой изменений на GitHub) просматривайте внесенные изменения (Git -> [Log](https://www.jetbrains.com/help/idea/log-tab.html) -> курсор на файл и Ctrl+D): не оставляйте в коде ничего лишнего (закомментированный код, TODO и пр.). Если файл не меняется (например только пробелы или переводы строк), не надо его чекинить, делайте ему `revert` (Git -> Revert / `Ctrl+Alt+Z`). -- 10: `System.out.println` нельзя использовать нигде, кроме как в `main`. Позже введем логирование. -- 11: Результаты, возвращаемые `UserMealsUtil.filteredByStreams`, мы будем использовать [в нашем приложении](http://javaops-demo.ru/topjava) для фильтрации по времени и отображения еды правильным цветом. -- 12: Обращайте внимание на комментарии к вашим коммитам в Git. Они должны быть короткие и информативные (лучше на english) -- 13: Не полагайтесь в решении на то, что список еды будет подаваться отсортированным. Такого условия нет. ------ - -> - ДЗ первого урока будет связано с созданием небольшого [CRUD](https://ru.wikipedia.org/wiki/CRUD)-приложения (в памяти, без базы данных) на JSP и сервлетах -> - основы JavaScript необходимы для понимания проекта, начиная с 8-го занятия - -### Полезные ресурсы -#### HTML, JavaScript, CSS -- [Basic HTML and HTML5](https://learn.freecodecamp.org/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/) -- [Справочник по WEB](https://developer.mozilla.org/ru/) -- [Видео по WEB-технологиям](https://www.youtube.com/user/WebMagistersRu/playlists) -- [Изучение JavaScript в одном видеоуроке за час](https://www.youtube.com/watch?v=QBWWplFkdzw) -- HTML, CSS, JAVASCRIPT, SQL, JQUERY, BOOTSTRAP -- Введение в программирование на JavaScript -- Стандарты кодирования для HTML, CSS и JavaScript’a -- Основы работы с HTML/CSS/JavaScript -- JavaScript - Основы -- Основы JavaScript -- Bootstrap 3 - Основы -- jQuery для начинающих - -#### Java (базовые вещи) -- [Сборник видео "Изучаем Java"](https://www.youtube.com/playlist?list=PLyxk-1FCKqockmP-fXZmHQ7UlYP3qvZRa) -- 1-й урок MasterJava: Многопоточность -- [Основы Java garbage collection](http://web.archive.org/web/20180831013112/https://ggenikus.github.io/blog/2014/05/04/gc) -- Размер Java объектов -- Введение в Java Reflection API -- Структуры данных в картинках -- Обзор java.util.concurrent.* -- Синхронизация потоков -- String literal pool -- Маленькие хитрости Java -- A Guide to Java 8 - -### Туториалы, разное -- [Открытый курс: Spring Boot + HATEOAS](https://javaops.ru/view/bootjava) -- [Что нужно знать о бэкенде новичку в веб-разработке](https://tproger.ru/translations/backend-web-development) -- [Туториалы: Spring Framework, Hibernate, Java Core, JDBC](http://proselyte.net/tutorials/) - -#### Сервлеты -- Как создать Servlet? Полное руководство. -- [Сервлеты](https://metanit.com/java/javaee/4.1.php) - -#### Туториалы по Spring -- [Юрий Ткач: Spring Framework - The Basics](https://www.youtube.com/playlist?list=PL6jg6AGdCNaWF-sUH2QDudBRXo54zuN1t) -- [Java Brains: Spring Framework](https://www.youtube.com/playlist?list=PLC97BDEFDCDD169D7) -- [Тимур Батыршинов: Spring Core - основы фреймворка, ядро](https://www.youtube.com/watch?v=CfHDr-19WWY&list=PL8X2nqRlWfaYYP1-qXjdPKE7bXYkl6aL4) -- [alishev: Spring Framework](https://www.youtube.com/playlist?list=PLAma_mKffTOR5o0WNHnY0mTjKxnCgSXrZ) - -#### JDBC, SQL -- Основы SQL на примере задачи -- Уроки по JDBC -- Learn SQL -- Интуит. Основы SQL -- Try SQL -- Курс "Введение в базы данных" - -#### Разное -- Вопросы по собеседованию, ресурсы для подготовки -- Эффективная работа с кодом в IntelliJ IDEA -- Quizful- тесты онлайн -- Введение в Linux - -#### Книги -- Джошуа Блох: Java. Эффективное программирование. Второе издание -- Гамма, Хелм, Джонсон: Приемы объектно-ориентированного проектирования. Паттерны проектирования -- Редмонд Э.: Семь баз данных за семь недель. Введение в современные базы данных и идеологию NoSQL -- Brian Goetz: Java Concurrency in Practice -- G.L. McDowell: Cracking the Coding Interview +### 22.04: Миграция на Spring-Boot +- Ревью вашего резюме +- Основы Spring Boot. Spring Boot maven plugin +- Lombok, база H2, ApplicationRunner +- Spring Data REST + HATEOAS +- Миграция приложения подсчета калорий на Spring Boot + +### 12.05.24: Дедлайн на сдачу [выпускного проекта](https://github.com/JavaOPs/topjava/blob/master/graduation.md) +### 22.05.24: Получение дипломов для участников [Дипломной программы](https://javaops.ru/view/register/diploma) diff --git a/lessons/Lesson_11/lesson12.md b/lessons/Lesson_11/lesson12.md new file mode 100644 index 00000000000..d782c6a1db1 --- /dev/null +++ b/lessons/Lesson_11/lesson12.md @@ -0,0 +1,45 @@ +# Стажировка Topjava + +## [Патчи занятия](https://drive.google.com/drive/u/1/folders/1sizknxR29Yu7XXjaVIBdS88ffXiVuqiB) + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Финальные правки: + +Один из вариантов сокрытия полей в примерах Swagger - сделать специальный TO класс. Но можно сделать проще через специальные аннотации: [Hide a Request Field in Swagger API](https://www.baeldung.com/spring-swagger-hide-field) +- Скрываем необязательные поле `id` при POST и PUT запросах через `@ApiModelProperty(hidden = true)` в примерах запроса Swagger. При этом передавать поле в запросе можно. +- `Meal.user` отсутствует в REST API, можно игнорировать: `@JsonIgnore` +- `User.meals` можно было сделать `JsonProperty.Access.READ_ONLY`, но при этом не пройдут тесты `getWithMeals` (maels не будет сериализоваться из ответа сервера для сравнения). Скрыл также через `@ApiModelProperty(hidden = true)` +- Также можно было скрыть нулевое поле `User.meals` при выводе через `@JsonInclude(JsonInclude.Include.NON_EMPTY)`. Но при этом поле исчезнет при запросе `getWithMeals` пользователя с пустым списком еды (например для Guest). Все зависит от бизнес-требований приложения (например насколько API публично и должно быть красивым). Можете попробовать самостоятельно скрыть это поле из вывода для запросов без еды через `View` (или отдельный TO). + +#### Apply [11_16_HW_fix_swagger.patch](https://drive.google.com/file/d/1A76XXvZdZCKxeKnVjZ2VkrWAHEQ1iof2) + +## Миграция на Spring Boot +За основу взят **[финальный код проекта BootJava с миграцией на Spring Boot 3.2, 8-й урок](https://javaops.ru/view/bootjava/lesson08)** +Вычекайте в отдельную папку (как отдельный проект) ветку `spring_boot` нашего проекта (так удобнее, не придется постоянно переключаться между ветками): +``` +git clone --branch spring_boot --single-branch https://github.com/JavaWebinar/topjava.git topjava_boot +``` +Если будете его менять, [настройте `git remote`](https://javaops.ru/view/bootjava/lesson01#project) +> Если захотите сами накатить патчи, сделайте ветку `spring_boot` от первого `init` и в корне **создайте каталог `src\test`** + +---- + +#### Apply 12_1_init_boot_java +Оставил как в TopJava название приложения `Calories Management` и имя базы `topjava` + +#### Apply 12_2_add_calories_meals + +Добавил из TopJava: +- Еду, калории +- Таблицы назвал в единственном числе: `user_role, meal` (кроме `users`, _user_ зарезервированное слово) +- Общие вещи (пусть небольшие) вынес в сервис : `MealService` +- Проверку принадлежности еды делаю в `MealRepository.getBelonged` +- Вместо своих конверторов использую `@DateTimeFormat` +- Обратите внимание на `UserRepository.getWithMeals` - он не работает с `@EntityGraph`. Зато работает с обычным `JOIN FETCH` и `DISTINCT` больше не нужен: + - [forget about DISTINCT](https://vladmihalcea.com/spring-6-migration/#Auto-deduplication) + - [it is no longer necessary to use distinct in JPQL and HQL](https://docs.jboss.org/hibernate/orm/6.0/migration-guide/migration-guide.html#query-sqm-distinct) +- Мигрировал все тесты контроллеров. В выпускном проекте столько тестов необязательно! Достаточно нескольких, на основные юзкейсы. +- Кэширование в выпускном желательно. 7 раз подумайте, что будете кэшировать! **Максимально просто, самые частые запросы, которые редко изменяются**. +- **Добавьте в свой выпускной OpenApi/Swagger - это будет большим плюсом и избавит от необходимости писать документацию**. + +### За основу выпускного предлагаю взять этот код миграции, сделав свой выпускной МАКСИМАЛЬНО в этом стиле. +### Успехов с выпускным проектом и в карьере! diff --git a/lessons/lesson01.md b/lessons/lesson01.md new file mode 100644 index 00000000000..087ea3cddb9 --- /dev/null +++ b/lessons/lesson01.md @@ -0,0 +1,263 @@ +# Стажировка Topjava + +### Правила похождения стажировки +- Не стоит стремиться прочитать все ссылки урока, их можно использовать позднее как справочник. Гораздо важнее **пройти основной материал урока и сделать Домашнее Задание** +- Обязательно посмотри правила работы с патчами на проекте + - Делать Apply Patch лучше по одному, непосредственно перед видео на эту тему, а при просмотре видео сразу отслеживать все изменения кода проекта по изменению в патче (`Git-> Local Changes-> Ctrl+D`) + - **При первом Apply удобнее выбрать имя локального ченджлиста Name: Change**. Далее все остальные патчи также будут в него попадать. +- **Код проекта обновляется и не всегда совпадает с видео (можно увидеть как развивался проект). Изменения в проекте указываю после соответствующего патча.** +- Если ссылка не открывается, попробуй [включить VPN](https://github.com/JavaOPs/topjava/wiki/VPN) +- **ОСНОВНОЕ, чему мы учимся на проекте: мыслить и работать как Java разработчики уже сейчас**, потом это будет гораздо сложнее и стоить дороже. Вот на мой взгляд [хорошие советы новичкам](http://blog.csssr.ru/2016/09/19/how-to-be-a-beginner-developer). От себя я добавлю: + - Учись **грамотно формулировать проблему**. Проблема "у меня не работает" может иметь тысячи причин. В процессе формулирования очень часто приходит ее решение. + - что я делаю (подробно, чтобы понял человек, который не был занят этой проблемой несколько часов) + - что получаю (обычно верх самого последнего эксепшена) + - что я сделал для решения, какие результаты получил + - Учись исследовать проблему. Внимательное чтение логов и [умение дебажить](http://info.javarush.ru/idea_help/2014/01/22/Руководство-пользователя-IntelliJ-IDEA-Отладчик-.html) - основные навыки разработчика. Обычно самый верх самого нижнего эксепшена- причина ошибки, туда нужно ставить брекпойнт. + - Грамотно **уделяй время каждой проблеме**. Две крайности - сразу бросаться за помощью и биться над ней часами. Пробуй решить ее сам и в зависимости от проблемы выделяй на это разумное время. + - Наконец, уровень участников у всех разный. Бывают синьоры, бывают начинающие. Не стесняйтесь задавать вопросы, иначе стажировка пройдет впустую! **Глупых вопросов не бывает**. +---------------------------------------------------- +- **Обязательно и как можно чаще пользуйтесь `Ctrl+Alt+L` - отформатировать код класса** +- **При изменениях на UI не забываетй сбрасывать кэш браузера - `Ctrl+F5`** +- **При удалении классов не забывате чистить target - в окошке Maven -> clean или `mvn clean`** +- **При проблемах с IDEA пользуйтесь `Refresh` в окошке Maven** +- **При проблемах с выполнением проверьте (и удалите) лишние java процессы (например в Task Manager)** + + +## Материалы занятия (скачать все патчи можно через `Download/Скачать` папки patch) +![image](https://cloud.githubusercontent.com/assets/13649199/18330295/5f2ca214-7560-11e6-8e1e-c0494f798c37.png) + +### ![correction](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Рефакторинг проекта + +#### Apply 1_0_rename.patch +- переименовал классы `UserMeal*` в более красивые `Meal*` +- преименовал `MealWithExceed` transfer object класс ([что это такое](https://ru.wikipedia.org/wiki/DTO) пройдем позже) в `MealTo` ([data transfer object naming convention](https://stackoverflow.com/questions/1724774/java-data-transfer-object-naming-convention)) +- обновил зависимость `maven-compiler-plugin` + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW0: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. Optional: реализация getFilteredMealsWithExcess через Stream API +- В патче `prepare_to_HW0.patch` вступительного задания метод фильтрации в `TimeUtil` переименовали в `isBetweenHalfOpen` (также изменилась логика сравнения - `startTime` включается в интервал) + +#### Apply 1_1_HW0_streams.patch + +- [Презентация Java 8](https://docs.google.com/presentation/d/1oltLkHK60FqIdsXjUdm4pPLSeC6KpNYjDsM0ips-qBw) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. Работа с git в IDEA. Реализация через цикл. +### ВНИМАНИЕ! Патчей `1_opt_2_HW0_cycles` и `1_opt_3_HW0_opt2` не будет в проекте! +Делаем в отдельной ветке (у меня `MealsUtil_opt`). Это варианты решений, которые не идут в `master` + +![image](https://user-images.githubusercontent.com/13649199/83656711-8b758b00-a5c8-11ea-9de4-c2ade77d4598.png) + +#### Apply 1_opt_2_HW0_cycles.patch + +### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопросы по HW0 + +> Почему не использовать в `TimeUtil` методы `isBefore/isAfter` ? + +это строгие (excluded) сравнения, а нам также нужно краевые значения + +> В `MealsUtil` у нас где-то есть ключевое слово `final`, где-то нет. В чем разница? + +Я участвовал в одном проекте, где `final` был обязательным (в сеттингах IDEA галочка стояла). Но это скорее исключение, чем правило в проектах java (в Java 8 вообще ввели эффективный final, те по факту). Во всех новомодных языках переменные final по умолчанию, а в java нужно помнить и везде добавлять, утомительно. Но если приучитесь - хуже не будет. Я обычно ставлю там, где важно по смыслу (если не забываю). + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. [HW0 Optional 2: реализация в один проход циклами и Stream API](https://drive.google.com/file/d/1dSt3axySxu4V9dMnuR1wczerlI_WzCep) + +#### Apply 1_opt_3_HW0_opt2.patch +- Дополнительно: + - [Первое занятие MasterJava: многопоточность](https://github.com/JavaOPs/masterjava) + - [Обзор java.util.concurrent.*](https://web.archive.org/web/20220427140138/https://habr.com/ru/company/luxoft/blog/157273/) + +## Занятие 1: + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. [Интервью: Java разработка. Обучение. Трудоустройство](https://javaops.ru/view/news/javaInterview) +- [JetBrains devecosystem 2022](https://www.jetbrains.com/lp/devecosystem-2022/java/) +- [Сontinuum Java Ecosystem 2022 – Survey results](https://www.continuum.be/en/blog/the-java-ecosystem-2022-survey-results/) +- [JRebel 2022 Java Developer Productivity Report](https://drive.google.com/file/d/1txLeRsNNR7EqYEeIvYmuyQi9hknBeR9G) + +## Внимание - обновил все патчи ниже 01.06 в 20:17. Если уже забрали, пожалуйста [скачайте заново](https://drive.google.com/drive/u/2/folders/0B9Ye2auQ_NsFfm5hSHEtbmxmN2kxb0NocVRwWl9KanowWXVCVXRZTlhaM09wQUswZkRidTA?resourcekey=0-H0PjelYVCYFR_1ANk-we5w) +### ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) 5. [Servlet API. Apache Tomcat. JSP](lesson01/tomcat_servlet_war.md) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. [Логирование](https://www.youtube.com/watch?v=mo8z3zRVV1E) +#### Последние версии _logback / slf4j_ не работают с JDK 8, понизил версии до совместимых с ней. Поднимем на 5м занятии при миграции на JDK 17+ + +#### Apply 1_5_simple_logging.patch + +- [Зачем нужно логирование](https://javarush.ru/groups/posts/2293-zachem-nuzhno-logirovanie) +- [Logback Project](https://logback.qos.ch/) + +> А зачем мы использовали logback? Почему SLF4J нас не устроило? Почему реализация логирования не log4j? + +`SLF4J-API` это API. В нее включена только пустая реализация `org.slf4j.helpers.NOPLogger` (можно посмотреть в исходниках). Logback для новых проектов стал стандарт, *Spring Boot* используют его по умолчанию. +[Reasons to prefer logback over log4j](http://logback.qos.ch/reasonsToSwitch.html) + +> Почему `private static final Logger log` а не `LOG/LOGGER` ? + +Это [правило именования констант, которые не "deeply immutable"](https://google.github.io/styleguide/javaguide.html#s5.2.4-constant-names), те если их содержимое можно изменить. + +#### Apply 1_6_logging_config.patch + +- [Java Logging: история кошмара](http://habrahabr.ru/post/113145/) +- [Project dependencies for logging](https://www.slf4j.org/manual.html#projectDep) +- [Добавление зависимостей логирования](http://www.slf4j.org/legacy.html) в проект +- Не делать конкатенацию строк: [форматирование в логах через {}](https://www.slf4j.org/faq.html#logging_performance) +- Дополнительно: + - [Logback configuration](https://logback.qos.ch/manual/configuration.html) + - [Ведение лога приложения](http://www.skipy.ru/useful/logging.html) + - [Владимир Красильщик – Что надо знать о логировании прагматичному Java‑программисту](https://www.youtube.com/watch?v=qzqAUUgB3v8) + +**Установите переменную окружения на TOPJAVA_ROOT на корень проекта и перезапустите IDEA. Слеши в пути должны быть в стиле unix (/)** + +Проверить, видит ли Java вашу переменную можно через `System.getenv("TOPJAVA_ROOT")` + +- [Set environment for Win/Mac/Unix](https://chlee.co/how-to-setup-environment-variables-for-windows-mac-and-linux/) +- [Set environment for UNIX (advanced)](https://askubuntu.com/a/849954) + - [Определить, какой Login или Non-Login Shell](https://tecadmin.net/difference-between-login-and-non-login-shell) + - [Порядок запуска скриптов при старте](https://www.baeldung.com/linux/bashrc-vs-bash-profile-vs-profile) +- Или простой вариант (не забудте добавить и в Run, и в Debug) + +![image](https://user-images.githubusercontent.com/13649199/76862707-8af21180-686f-11ea-9f8c-2bb30ef4c3b2.png) + +**Иногда антивирусы блокируют логирование (например Comodo). Если не работает и стоит антивирус- добавьте исключение.** + +> Изменения в проекте, которым могут встретиться в других видео: +> - убрал `LoggerWrapper` и логирую напрямую в логгер SLF4J. +> - удалил зависимости `jul-to-slf4j` и `jcl-over-slf4j`. Spring 5 напрямую использует `slf4j` без `common-logging` + +--------- + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW01 (делаем ветку HW01 от последнего патча в master) +-------------------------------------------- +#### 1. Реализовать сервлет с отображением в таблице списка еды (в памяти и БЕЗ учета пользователя) + +> Деплоиться в Tomcat лучше как `war exploded`: нет упаковки в war и при нажатой кнопке `Update Resources on Frame Deactivation` можно обновляться css, html, jsp без передеплоя. При изменении `web.xml`, добавлении методов, классов необходим redeploy. + +- 1.1 По аналогии с `UserServlet` добавить `MealServlet` и `meals.jsp` + - Задеплоить приложение (war) в Tomcat c `applicationContext=topjava` (приложение должно быть доступно по http://localhost:8080/topjava) + - Попробовать деплои в Tomcat как WAR в запушенный вручную Tomcat и через IDEA. +- 1.2 Сделать отображения списка еды в JSP [в таблице](http://htmlbook.ru/html/table), цвет записи в таблице зависит от параметра `excess` (красный/зеленый). + - 1.2.1 Список еды захардкодить (те проинициализировать в коде, желательно чтобы в проекте инициализация была только в одном месте). Норму калорий (caloriesPerDay) сделать в коде константой + - 1.2.2 Время выводить без 'T' + - 1.2.3 Список выводим БЕЗ фильтрации по `startTime/endTime` + - 1.2.4 С обработкой исключений пока можно не заморачиваться, мы будем красиво обрабатывать в конце стажировки + - 1.2.5 Вариант реализации: + - из сервлета преобразуете еду в `List`; + - кладете список в запрос (`request.setAttribute`); + - делаете `forward` на jsp для отрисовки таблицы (при `redirect` атрибуты теряются). + - **JSP работает через геттеры: `meal.dateTime` в JSP вызывает `meal.getDateTime()`** + - в `JSP` для цикла можно использовать `JSTL tag forEach`. Для подключения `JSTL` в `pom.xml` и шапку JSP нужно добавить: +``` + + javax.servlet + jstl + 1.2 + + + <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + ... +``` + + - Интернет-приложения на JAVA + - JSP + - [Как создать Servlet? Полное руководство](https://devcolibri.com/как-создать-servlet-полное-руководство) + - [JSTL для написания JSP страниц](https://devcolibri.com/jstl-для-написания-jsp-страниц/) + - JSTL: Шаблоны для разработки веб-приложений в java + - JSTL LocalDateTime format + +### Optional +#### 2. Реализуем в ПАМЯТИ (любая коллекция) CRUD (create/read/update/delete) для еды +**Пример: [Simple CRUD using Servlet/JSP](https://danielniko.wordpress.com/2012/04/17/simple-crud-using-jsp-servlet-and-mysql)** +> - Пример нужно САМОСТОЯТЕЛЬНО переделать: вместо хранения в MySql нужно хранить в коллекции ПАМЯТИ (задание упрощается). +> - Классы: сервлет, **интерфейс хранения**, его реализация для хранения в памяти +- 2.1 Хранение в памяти будет одна из наших CRUD реализаций (позже будет JDBC, JPA и DATA-JPA). +- 2.2 Работать с реализацией CRUD через интерфейс, который не должен ничего знать о деталях реализации (Map, DB или что-то еще). +- 2.3 Добавить поле `id` в `Meal/ MealTo`. Реализовать генерацию счетчика, УЧЕСТЬ МНОГОПОТОЧНОСТЬ СЕРВЛЕТОВ. В качестве первичного ключа используют UUID, Long, Integer. На нашем проекте будем использовать **Integer**. + - [обзор java.util.concurrent](https://web.archive.org/web/20220427140138/https://habr.com/ru/company/luxoft/blog/157273/) +- 2.4 Предлагаю сейчас делать максимально просто (без защиты от CSRF, JavaScript и пр., это будет позже): где можно через [ссылки GET, метод сервлета doGet()](http://htmlbook.ru/html/a/href), редактирование через [форму POST, метод doPost()](http://htmlbook.ru/html/form/method) +- 2.5 Для ввода дат и времени можно использовать html5 типы, хотя они поддерживаются не всеми браузерами (протестировать свой браузер). В конце курса мы добавим DateTimePicker jQuery plugin, который будет работать на всех браузерах. +- 2.6 Форму на create-update предлагаю не дублировать, сделать одну (хотя это не ошибка сделать разные). + +## После выполнения ДЗ обязательно проверьте решение на ошибки + +### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопросы по HW1 + +> Не попадаю на брекпойнт в сервлете или на страничку JSP. + +- внимательно проверь url и applicationContext (Application Context должен быть тот же, что и url приложения: wiki IDEA) +- посмотрите в task manager: возможно запущено несколько JVM и они мешают друг другу. Лишние java приложения убить. +- предлагаю пока размещать JSP в корне `webapp` и делать по аналогии с моим кодом для `user`, иначе придется бороться с путями Application context и Servlet context. Мы будем работать с ними позже. Пути бывают: + - относительными: от текущего положения, без '/' спереди. Если обращение к сервлету идет вложенное (нарпимер `/meals/....`), то `Servlet context=meals` и относительные пути формируются от него (в результате в браузере можно увидеть в браузере `/meals/meals/...`) + - абсолютными (от корня `localhost:8080`). Тогда [в путь необходимо добавлять Application context](https://stackoverflow.com/questions/4764405/548473). + +> Приложение не видит TOPJAVA_ROOT. + +**После выставления переменной окружения IDEA нужно рестартовать. Слеши в пути должны быть в стиле unix (/)**. Проверить, видит ли java переменную окружения можно так: `System.getenv("TOPJAVA_ROOT")`. Еще вариант: добавить `-DTOPJAVA_ROOT=...` в опции запуска приложения, тогда она доступна из java как `System.getProperty("TOPJAVA_ROOT")`. + +> Нужно ли разработчику знание HTML. + +Веб разработчику (а большинство Java приложений это веб-приложения) основы знать обязательно. Толстых книжек сейчас читать НЕ рекомендую (это общий совет по любой технологии на всю стажировку). Достаточно [посмотреть основы](https://github.com/JavaWebinar/topjava/blob/doc/doc/entrance.md#html) и того материала, что мы пройдем на курсе. + +> Проблемы с кодировкой в POST (кракозябры). + +Возможное решение - выставьте кодировку ДО первого чтения из request: +``` +protected void doPost(HttpServletRequest request, ...) { + request.setCharacterEncoding("UTF-8"); +``` + +> Если сервлет тыкают несколько пользователей / несколько браузеров, какого должно быть поведение? Нужно ли что-то делать с сессиями? + +В Optional нужно делать реализацию хранения потокобезопасной. Cессии пока НЕ используем (начнутся, когда будет прикручивать авторизацию). + +> Для чего нам нужна потокобезопасная реализация коллекции, если каждый пользователь видит только себя? + +Реализация хранения в памяти у нас одна на всех. Те коллекция шарится между пользователями, они в разных потоках ее дергают. Если несколько потоков одновременно будут изменять коллекцию без учета потокобезопасная (например один будет удалять, второй вставлять), без учета потокобезопасности мы получим `ConcurrentModificationException` + +> Предпочтительнее ли создавать новый объект `Meal` при каждом update? + +Если при обновлении не создавать копию, то сохраненный в памяти объект может кто-то попортить. Вопрос скорее доверия к коду- если проект большой и людей над ним трудится много, то вероятнее нужно копировать. + +> Почему теряются атрибуты при передаче на сервлет: `http://localhost:8080/topjava/meals?action=add&...` и `req.getAttribute("action")` = null ? + +См. Difference between getAttribute() and getParameter(). Отсюда также следует, что при редиректе атрибуты теряются. + +> Зачем нужен в jsp `` ? + +[jsp:useBean](http://java-online.ru/jsp-actions.xhtml#useBean) нужен IDEA для автодополнений - она понимает тип переменной, которая уже доступна в JSP (например через setAttribute). И еще эта переменная становится доступной в java вставках. Для вывода в JSP это тэг не обязателен. Если тип переменной JSP не совпадает с тем, что в `jsp:useBean`, будет ошибка. + +---------------------------- +### Итоги занятия после выполнения ДЗ: +Мы создали CRUD веб-приложение для управления едой (создание-чтение-обновление-удаление) с использованием сервлетов и логированием. Пока в памяти, и пока еда никому не принадлежит. +Пример выполнения ДЗ (не надо сложного интерфейса, Bootstrap css будем проходить на 8-м занятии): + +![image](https://user-images.githubusercontent.com/13649199/94701494-6100c800-0345-11eb-9907-2a0099305710.png) +![image](https://user-images.githubusercontent.com/13649199/94701688-9a393800-0345-11eb-9c2d-dd53ba55724c.png) + +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Типичные ошибки +- 1 **Если в названии класса есть `Meal`, не нужно использовать слово meal в методах класса.** +- 2 Привыкайте писать комментарии к чекину: одной фразой что вы сделали в нем. Например: *Meals CRUD implementation*. См. +[Как писать сообщения коммитов](https://habr.com/ru/post/416887/) +- 3 Хранение в памяти и операции с ней должны выполняться просто и эффективно +- 4 Хранить нужно `Meal` и конвертировать ее в `MealTo` когда отдаем список на отображение в JSP. + - excess нужно пересчитывать каждый раз перед отображением + - форматирование должно находится в JSP! Именно он заведует отображением. Повторяем паттерн [MVC](https://ru.wikipedia.org/wiki/Model-View-Controller) +- 5 Стили `color` можно применять ко всей строке таблицы `tr`, а не каждой ячейке. +- 6 `DateTimeFormatter` можно сделать один заранее (он потокобезопасный в отличие от `SimpleDateFormatter`), а не создавать новый при каждом запросе. +- 7 Работать с CRUD надо через интерфейс. +- 8 Реализаций хранения будет несколько, нужно учитывать это в названии класса имплементации работы в памяти. +- 9 В `web.xml` принято группировать сервлет со своим маппингом +- 10 Не размещайте никакую логику (форматирование, счетчики) в бинах, где хранятся только данные (`Meal, MealTo`) +- 11 Еще раз: детали реализации в памяти не должны быть никому видны. Те НЕ НАДО счетчик размещать в `Meal` или `MealServlet` или `MealsUtil`, в базе же он будет по другому генерится. +- 12 `volatile` при ++ не помогает от многопоточности. Почему? +- 13 Обратите также внимание на то, чтобы реализация вашей коллекции для хранения еды была также многопоточной. +- 14 Попробуйте не делайть дублирование кода `MealsUtil`. Простой вариант - использовать то, что отсутствие фильтрации - это частный случай фильтрации (когда ничего не отсеивается). +- 15 Не дублируйте строки в `jsp`. Посмотрите на тернарный оператор. +- 16 После операции `delete` в браузере должен быть url `http:\\localhost:8080\topjava\meals` +- 17 Перед чекином проверяйте свой ченджлист (`Ctrl+D` на файле из `Local Changes` - посмотреть что поменялось). Если там только пробелы/переводы строк, не надо его комитить - делайте файлу `Git->revert`. +- 18 Учтите в названии реализации CRUD, что + - 18.1 у нас будет несколько реализаций (не только в памяти) + - 18.2 у нас будет 2 CRUD (для еды и пользователей). А в реальном проекте их намного больше. +- 19 Сессии НЕ использовать! При редиректе все атрибуты (`req.getAttribute()`) теряются (см. вопрос выше). Сценарий редиректа: + - 1 из сервлета делаем редирект (снова на сервлет, не на JSP!) + - 2 снова заходим в сервлет + - 3 кладем нужные атрибуты и делаем forward на jsp + - 4 если очень хочется передать параметры из 1. в 2. можно сделать их через параметры запроса (например `meals?id=5`) и доставать через `reg.getParameter(id)`. В моей реализации такого не потребовалось. +- 20 Для cancel в JSP можно использовать код: `` diff --git a/lessons/lesson02.md b/lessons/lesson02.md new file mode 100644 index 00000000000..7d47dd537eb --- /dev/null +++ b/lessons/lesson02.md @@ -0,0 +1,274 @@ +# Стажировка Topjava + +## [Материалы занятия](https://drive.google.com/drive/folders/0B9Ye2auQ_NsFfkpsWE1uX19zV19IVHd0bTlDclc5QmhMMm4xa0Npek9DT18tdkwyLTBZdXM) (скачать все патчи можно через Download папки patch) + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW1: + +- **Перед сборкой проекта (или запуском Tomcat) откройте вкладку Maven Projects и сделайте `clean`** +- **Если страничка в браузере работает неверно, очистите кэш (`Ctrl+F5` в хроме)** + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. Отображения списка еды в JSP +#### Apply 2_1_HW1.patch + +> - Переименовал `TimeUtil` в `DateTimeUtil` +> - Переименовал `mealList.jsp` в `meals.jsp` +> - Изменения в `MealsUtil`: +> - Сделал константу `List meals`. [Правило именования констант, которые не "deeply immutable"](https://google.github.io/styleguide/javaguide.html#s5.2.4-constant-names) +> - Для фильтрации по времени и без нее в метод `filterByPredicate` передаю реализацию `Predicate`, см. паттерн [Стратегия](https://refactoring.guru/ru/design-patterns/strategy) и, если непонятно, [картинку](https://user-images.githubusercontent.com/13649199/95467365-093a1080-0986-11eb-8177-0985456d857a.png) +> - Форматирование даты сделал на основе Custom EL function +> - [Create a custom Function for JSTL через tag library descriptor (TLD)](http://findnerd.com/list/view/How-to-create-a-custom-Function-for-JSTL/2869/) +> - Добавил еще один способ вывести `dateTime` через стандартную JSTL функцию `replace` (префикс `fn` в шапке также надо поменять) + +- [jsp:useBean](http://java-online.ru/jsp-actions.xhtml#useBean) +- [MVC - Model View Controller](http://design-pattern.ru/patterns/mvc.html) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. Optional: реализация CRUD +#### Apply 2_2_HW1_optional.patch +Про использование паттерна Repository будет подробно рассказано в видео "Слои приложения" +> - Согласно ответам на [Java Interfaces/Implementation naming convention](https://stackoverflow.com/questions/2814805/java-interfaces-implementation-naming-convention) +убрал `Impl` в `InMemory` (и всех последующих) реализациях репозиториев. Они не нужны. +> - Поправил `InMemoryMealRepository.save()`. Если обновляется еда, которой нет в хранилище (c несуществующим id), вставка не происходит. +> - В `MealServlet.doGet()` сделал выбор через `switch` +> - В местах, где требуется `int`, заменил `Integer.valueOf()` на `Integer.parseInt()` +> - В `mealForm.jsp` использую параметр запроса `param.action`, который не кладется в атрибуты. +> - Переименовал `mealEdit.jsp` в `mealForm.jsp`. Поля ввода формы добавил `required` +> - Пофиксил багу c `history.back()` в `mealForm.jsp` для **FireFox** (коммит формы при Cancel, сделал `type="button"`). + +Дополнительно: + - HTTP 1.0 vs 1.1 + +### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопросы по HW1 + +> Зачем в `InMemoryMealRepository` наполнять map с помощью нестатического блока инициализации, а не в конструкторе? + +В общем случае так делать не надо. Сделал, чтобы напомнить вам про эту конструкцию, см. [Малоизвестные особенности Java](https://habrahabr.ru/post/133237/) + +> Почему `InMemoryMealRepository` не singleton? + +Начиная с Servlet API 2.3 пул сервлетов не создается, [создается только один инстанс сервлетов](https://stackoverflow.com/questions/6298309). Те. `InMemoryMealRepository` в нашем случае создается тоже только один раз. Далее все наши классы слоев приложения будут создаваться через Spring, бины которого по умолчанию являются синглтонами (в его контексте). + +> `Objects.requireNonNull` в `MealServlet.getId(request)` если у нас нет `id` в запросе бросает NPE (`NullPointerException`). Но оно вылетит и без этого метода. Зачем он нужен и почему мы его не обрабатываем? + +`Objects.requireNonNull` - это проверка предусловия (будет подробно на 4-м занятии). Означает что в метод пришел неверный аргумент (должен быть не null) и приложение сообщает об ошибке сразу на входе (а не "может быть где-то потом"). См. [What is the purpose of Objects#requireNonNull](https://stackoverflow.com/a/27511204/548473). Если ее проглатывать или замазывать, то приложение возможно где-то работает неверно (приходят неверные аргументы), а мы об этом не узнаем. Красиво обрабатывать ошибки будем на последних занятиях (Spring Exception Handling). + +## Занятие 2: + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Многоуровневая(многослойная) архитектура +![Слои приложения](https://javaops.ru/static/images/projects/top-scheme.jpg) +- Многоуровневая(многослойная) архитектура +- Data Access Object +- Паттерн DTO +- Should services always return DTOs, or can they also return domain models? +- [Mapping Entity->DTO goes in which application layer: Controller or Service?](http://stackoverflow.com/questions/31644131/spring-dto-dao-resource-entity-mapping-goes-in-which-application-layer-cont/35798539#35798539) +- Дополнительно: + - Value Object и Data Transfer Object + - Difference between Active Record and DAO + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Создание каркаса приложения для пользователей + +#### Apply 2_3_app_layers.patch +> - Убрал интерфейсы к сервисам. Я всегда предпочитаю писать меньше кода и в случае с одной реализацией можно обходится без них. + По поводу инкапсуляции и отделения API от реализации - интерфейсы к сервисам это внутренняя часть приложения с одной реализацией. Меньше кода, проще поддерживать. +> - Переименовал `ExceptionUtil` в `ValidationUtil` +> - Поменял `LoggedUser` на `SecurityUtil`. Это класс, из которого приложение будет получать данные залогиненного пользователя (пока [аутентификации](https://ru.wikipedia.org/wiki/Аутентификация) нет, он реализован как заглушка). Находится в пакете `web`, т.к. аутентификация/[авторизация](https://ru.wikipedia.org/wiki/Авторизация) происходит на слое контроллеров и остальные слои приложения про нее знать не должны. +> - Добавил проверку id пользователя, пришедшего в контроллер ([treat IDs in REST body](https://stackoverflow.com/a/32728226/548473), "If it is a public API you should be conservative when you reply, but accept liberally"). Считаю это важной частью проверки входных данных в контроллере, не забывайте это делать в ваших выпускных проектах. +> - Удалил в `User` лишнюю инициализацию. Было немножко наперед, добавим при введении конструктора по умолчанию. + + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы + +> Почему у `User.registered` тип `Date`, а `Meal.dateTime` `LocalDateTime `? + +По логике приложения время регистрации - абсолютное (конкретный момент), а время еды по бизнес логике относительно (те не зависит от часового пояса, завтрак и в Африке должен быть завтраком) + +> Какова цель деления приложения на слои? + +Управляемость проекта (особенно большого) повышается на порядок: +- Обеспечивается меньшая связываемость. Допустим если мы меняем что-то в контроллере, то сервис эти изменения не задевают. +- Облегчается тестирование (мы будем тестировать слои сервисов и контроллеров отдельно) +- Четко разделяется функционал - где писать, куда смотреть. Не создаются [God objects](https://ru.wikipedia.org/wiki/Божественный_объект) + +> DTO используются когда есть избыточность запросов, которую мы уменьшаем, собрав данные из разных бинов в один? Когда DTO необходимо использовать? + +(D)TO может быть как частью одного entity (набор полей) так и набором нескольких entities. +В нашем проекте для данных, которые надо отдавать наружу и отличающихся от Entiy (хранимый бин), мы будем делать (Data) Transfer Object и класть в отдельный пакет to. Например `MealsTo` мы отдаем наружу и он является Transfer Object, его пернесем в пакет `to`. +На многих проектах (и собеседованиях) практикуют разделение на уровне maven модулей entity слоя от логики и соответствующей конвертацией ВСЕХ Entity в TO, даже если у них те же самые поля. +Хороший ответ когда TO обязательны есть на stackoverflow: When to Use. + +> Почему контроллеры положили в папку web, а не в controllers? + +То же самое что `domain/model` - просто разные названия, которые устоялись в Java. Не придумывайте своих! + +> Зачем мы наследуем `NotFoundException` от `RuntimeException`? + +Так с ним удобнее работать. И у нас нет никаких действий по восстановлению состояния приложения (no recoverable conditions): checked vs unchecked exception. По последним данным checked exception вообще depricated: Ignore Checked Exceptions + +> Что такое `ProfileRestController`? + +Контроллер, где залогиненный пользователь будет работать со своими данными + +> Зачем в `AdminRestController` переопределяются методы родителя с вызовом тех же родительских? + +Сделано на будущее, мы будем менять этот код. + +> Что лучше возвращать из API: `Collection` или `List` + +Вообще, как правило, возвращают `List`, если не просится по коду более общий случай (например возможный `Set` или `Collection`, возвращаемый `Map.values()`). Если возвращается отсортированный список, то `List` будет адекватнее. + +> **Вопрос вам (очень важный):** можно ли в `MealRestController` контроллере сделать член класса `private int userId = SecurityUtil.authUserId()` и использовать его в методах контроллера? + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. [Что такое Spring Framework](https://www.youtube.com/watch?v=megjriLG35I). +- [Wiki: Spring Framework](https://ru.wikipedia.org/wiki/Spring_Framework) +- [JVM Ecosystem Report 2020: Spring](https://snyk.io/blog/spring-dominates-the-java-ecosystem-with-60-using-it-for-their-main-applications/) +- [2020 Java Technology Report](https://www.jrebel.com/blog/2020-java-technology-report) +- [Spring Framework Documentation](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Что такое Spring Framework? Внедрение зависимостей](https://habr.com/ru/post/490586/) +- [Евгений Борисов — Spring-построитель](https://www.youtube.com/watch?v=rd6wxPzXQvo) +- [Инверсия управления] (https://ru.wikipedia.org/wiki/Инверсия_управления) + +#### Apply 2_4_add_spring.patch +> Сделал рефакторинг конструктора User, чтобы была возможность создавать пользователя без ролей + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. [Запуск Spring Application Context](https://drive.google.com/file/d/1y-3ok-6CzhjnR4Rmv3-z4EV4VsElIDn6) +- [Container Overview](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-basics) +#### Apply 2_5_add_spring_context.patch + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. [Dependency Injection, Annotation_processing](https://drive.google.com/file/d/1Z9cgULTrXxgeaqqnsh7rJtIaD2LSdzHT) +#### Apply 2_6_dependency_injection.patch +- [IoC, DI, IoC-контейнер. Просто о простом](http://habrahabr.ru/post/131993/) +- [Что такое Spring Framework? Внедрение зависимостей](https://habr.com/ru/post/490586/) +- [Перевод "Field Dependency Injection Considered Harmful"](https://habrahabr.ru/post/334636/) +- [Field vs Constructor vs Setter DI](http://stackoverflow.com/questions/39890849/what-exactly-is-field-injection-and-how-to-avoid-it) + +#### Apply 2_7_annotation_processing.patch +- [Spring Auto Scanning Components](http://www.mkyong.com/spring/spring-auto-scanning-components) +- [Difference between @Component, @Repository & @Service annotations in Spring](http://stackoverflow.com/questions/6827752/whats-the-difference-between-component-repository-service-annotations-in) +- [Использование аннотации @Autowired](http://www.seostella.com/ru/article/2012/02/12/ispolzovanie-annotacii-autowired-v-spring-3.html) +- Дополнительное: + - [Подготовка к Spring Professional Certification. Контейнер, IoC, бины](https://habr.com/ru/post/470305/) + - [Spring на GitHub](https://github.com/spring-projects) + - [Spring Annotations](https://dzone.com/refcardz/spring-annotations) + +#### Дополнительно видео по Spring + - [Юрий Ткач: Spring Framework - The Basics](https://www.youtube.com/playlist?list=PL6jg6AGdCNaWF-sUH2QDudBRXo54zuN1t) + - [Java Brains: Spring Framework](https://www.youtube.com/playlist?list=PLC97BDEFDCDD169D7) + - [Тимур Батыршинов: Spring Core - основы фреймворка, ядро](https://www.youtube.com/watch?v=CfHDr-19WWY&list=PL8X2nqRlWfaYYP1-qXjdPKE7bXYkl6aL4) + - [alishev: Spring Framework](https://www.youtube.com/playlist?list=PLAma_mKffTOR5o0WNHnY0mTjKxnCgSXrZ) + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> Что такое схема в spring-app.xml xsi:schemaLocation и зачем она нужна + +XML схема нужна для валидации xml, IDEA делает по ней автозаполнение. + +> Что означает для Spring + + + + ? + +Можно сказать так: создай и занеси в свой контекст экземпляр класса (бин) `UserService` и присвой его проперти `repository` бин `inmemoryUserRepository`, который возьми из своего контекста. + +> `SecurityUtil.authUserId()` и `user.id` это одно и то или это разные вещи? + +`User.id` это уникальный идентификатор юзера, которых в приложении много. +`SecurityUtil.authUserId()` это идентификатор `id` залогиненного юзера. Мы можем, например, получить самого залогиненного юзера, выполнив запрос с `User.id==SecurityUtil.authUserId()` +Когда вы логинитесь в свое почтовое приложение, оно отдает вам именно ваши письма на основе вашего `id`, который она определяет и запоминает во время аутентификации (логина). +У нас пока этого нет и `id` задается константой (хардкодится). Но когда мы сделаем настоящую аутентификацию, все будет работать для любого залогиненного пользователя. + +> Как биндинг происходит для `@Autowired`? Как поступать, если у нас больше одной реализации `UserRepository`? + +`@Autowired` инжектит по типу (т.е. ижектит класс который реализует `UserRepository`). Обычно он один. Если у нас несколько реализаций, Spring не поднимится и поругается - `No unique bean`. + В этом случае можно уточнить имя бина через @Qualifier. `@Qualifier` обычно добавляют только в случае нескольких реализаций. +См. [Inject 2 beans of same type](https://stackoverflow.com/a/2153680/548473) + +> Почему нельзя сервлет помещать в Spring контекст? + +Сервлеты- это исключительно классы `servlet-api` (веб контейнера), они инстанциируются Tomcat. Те технически можно (без `init/destroy`), но идеологически - неверно. Cоздастся два сервлета: один настоящий, Tomcat-ом, и второй - нерабочий, Spring-ом. НЕ надо включать сервлет в контекст Spring. + +-------------------- +- **Еще раз смотрим на [демо приложение](http://javaops-demo.ru/topjava) и вникаем, что такое пользователь и его еда и что он может с ней сделать. +Когда пользователь логинится в приложении, его id и норма калорий "чудесным образом" попадают в `SecurityUtil.authUserId() / SecurityUtil.authUserCaloriesPerDay()`. Как они реально туда попадут будет в уроке 9 (Spring Security, сессия и куки)** +- **Перед началом выполнения ДЗ (если есть хоть какие-то сомнения) прочитайте ВСЕ ДЗ. Если вопросы остаются - то ВСЕ подсказки**. Особенно этот пункт важный, когда будете делать реальное рабочее ТЗ. + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW02 +- 1: Имплементировать `InMemoryUserRepository` по аналогии с `InMemoryMealRepository` (список пользователей возвращать отсортированным по имени) +- 2: сделать `Meal extends AbstractBaseEntity`, `MealTo` перенести в пакет `ru.javawebinar.topjava.to` (transfer objects) +- 3: Изменить `MealRepository` и `InMemoryMealRepository` таким образом, чтобы вся еда всех пользователей находилась в одном общем хранилище, но при этом каждый конкретный аутентифицированный пользователь мог видеть и редактировать только свою еду. + - 3.1: реализовать хранение еды для каждого пользователя можно с добавлением поля `userId` в `Meal` ИЛИ без него (как нравится). Напомню, что репозиторий один и приложение может работать одновременно с многими пользователями. + - 3.2: если по запрошенному id еда отсутствует или чужая, возвращать `null/false` (см. комментарии в `MealRepository`) + - 3.3: список еды возвращать отсортированный в обратном порядке по датам + - 3.4: дополнительно: попробуйте сделать реализацию атомарной (те учесть коллизии при одновременном изменении еды одного пользователя) +- 4: Реализовать слои приложения для функциональности "еда". API контроллера должна удовлетворять все потребности демо приложения и ничего лишнего (см. [демо](http://javaops-demo.ru/topjava)). Поиск и изменение порядка сортировки в таблице демо приложения реализованы на клиенте в браузере (плагин DataTables), сейчас делать не нужно. + - **Смотрите на реализацию слоя для user и делаете по аналогии! Если там что-то непонятно, не надо исправлять или делать по своему. Задавайте вопросы. Если действительно нужна правка - я сделаю и напишу всем.** + - 4.1: после авторизации (сделаем позднее), id авторизованного юзера можно получить из `SecurityUtil.authUserId()`. Запрос попадает в контроллер, методы которого будут доступны снаружи по http, т.е. запрос можно будет сделать с ЛЮБЫМ id для еды + (не принадлежащем авторизированному пользователю). Нельзя позволять модифицировать/смотреть чужую еду. + - 4.2: `SecurityUtil` может использоваться только на слое web (см. реализацию `ProfileRestController`). `MealService` можно тестировать без подмены логики авторизации, поэтому **в методы сервиса и репозитория мы передаем параметр `userId`: id авторизованного пользователя (предполагаемого владельца еды)**. + - 4.3: если еда не принадлежит авторизированному пользователю или отсутствует, в `MealService` бросать `NotFoundException`. + - 4.4: конвертацию в `MealTo` можно делать как в слое web, так и в service ([Mapping Entity->DTO: Controller or Service?](http://stackoverflow.com/questions/31644131)) + - 4.5: в `MealService` постараться сделать в каждом методе только одни запрос к `MealRepository` + - 4.6 еще раз: не надо в названиях методов повторять названия класса (`Meal`). + +![image](https://user-images.githubusercontent.com/13649199/121820224-66ffc480-cc9a-11eb-8abb-d1015ec2cb79.png) + +- 5: включить классы еды в контекст Spring (добавить аннотации) и вызвать из `SpringMain` любой метод `MealRestController` (проверить что Spring все корректно заинжектил) + +### Optional +- 6: в `MealServlet` сделать инициализацию Spring, достать `MealRestController` из контекста и работать с едой через него (**как в `SpringMain`**). `pom.xml` НЕ менять, работаем со `spring-context`. Сервлет обращается к контролеру, контроллер вызывает сервис, сервис - репозиторий. Когда будем работать через Spring MVC, `MealServlet` удалим, останется только контроллер. + +![image](https://user-images.githubusercontent.com/13649199/121820239-71ba5980-cc9a-11eb-8c25-54df6f50f43c.png) + +- 7: добавить в `meals.jsp` и `MealServlet` фильтрацию еды по дате и по времени (см. [демо](http://javaops-demo.ru/topjava)). Сброс фильтра делать не надо (реализуем через ajax в HW8). ВНИМАНИЕ: в приложении фильтрация делается не по интервалу дата-время, а отдельно по датам и затем отдельно по времени. +- 8: добавить выбор текущего залогиненного пользователя (имитация аутентификации, сделать [Select](http://htmlbook.ru/html/option) с двумя элементами со значениями 1 и 2 в `index.html` и `SecurityUtil.setAuthUserId(userId)` в `UserServlet`). От выбора user или admin будет зависеть отображение еды: user-а или admin-а. +Настоящая аутентификация будет через Spring Security позже. + +---------------------------- +### Итоги занятия после выполнения ДЗ: +Мы создали архитектуру нашего приложения с разделением на слои и внедрили в наш проект фреймворк Spring, который их связывает. +Далее мы реализовали функционал нашего приложения для работы с едой, как он сделан в [демо приложении](http://javaops-demo.ru/topjava) (но с фиктивной аутентификацией) + +--------------------- + +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Типичные ошибки и подсказки по реализации + +- 1: **В реализации `InMemoryUserRepository`** + - 1.1: `getByEmail` попробуйте сделать через `stream` + - 1.2: `delete` попробуйте сделать за одно обращение к map (без `containsKey`). При удалении пользователя его еду можно оставить, при реализации в базе будет CASCADE. + - 1.3: при запросе списка юзеров предусмотрите случай одинаковых `User.name` (порядок должен быть зафиксированным). Поле `User.email`у нас уникально - в базе будет ограничение. +- 2: **В реализации `InMemoryMealRepository`** + - 2.1: В `Meal`, которая приходит из браузера в контроллер, нет никакой информации о пользователе (еда приходит в контроллер **БЕЗ `user/userId`, она может быть только от авторизированного пользователя**). По id еды и авторизованному пользователю нужно проверить ее принадлежность (его это еда или чужая) + **Проверьте сценарий: авторизованный пользователь пробует изменить чужую еду (id еды ему не принадлежит).** + - 2.2: `get\update\delete` - следите, чтобы не было NPE (`NullPointException` может быть, если в хранилище отсутствует юзер или еда). + - 2.3: Фильтрацию по датам сделать в репозитории т.к. из базы будем брать сразу отфильтрованные по дням записи. Следите чтобы **первый и последний день не были обрезаны, иначе сумма калорий будет неверная**. + - 2.4: Если запрашивается список и он пустой, не возвращайте NULL! По пустому списку можно легко итерироваться без риска `NullPoinException`. + - 2.5: Не дублируйте код в `getAll` и метод с фильтрацией + - 2.6: попробуйте учесть, что следующая реализация (сортировка, фильтрация) будет делаться прямо в базе данных +- 3: Проверьте, что удалили `Meal.id` и связанные с ним методы (он уже есть в базовом `BaseEntity`) +- 4: Проверку `isBetweenHalfOpen` сделать в `DateTimeUtil`. Попробуйте использовать `LocalDateTime` вместо `LocalDate` с прицелом на то, что в DB будет тип даты `timestamp`. Тогда для `LocalTime` и `LocalDateTime` можно использовать один метод проверки полуоткрытого интервала и дженерики (см. [Generics Tutorials](https://docs.oracle.com/javase/tutorial/extra/generics/morefun.html) и [Погружаемся в Java Generics](https://habr.com/ru/company/sberbank/blog/416413/)) +- 5: **Реализация 'MealRestController' должен уметь обрабатывать запросы**: + - 5.1: Отдать свою еду (для отображения в таблице, формат `List`), запрос БЕЗ параметров + - 5.2: Отдать свою еду, отфильтрованную по startDate, startTime, endDate, endTime + - 5.3: Отдать/удалить свою еду по id, параметр запроса - id еды. Если еда с этим id чужая или отсутствует - `NotFoundException` + - 5.4: Сохранить/обновить еду, параметр запроса - Meal. Если обновляемая еда с этим id чужая или отсутствует - `NotFoundException` + - 5.5: Сервлет мы удалим, а контроллер останется, поэтому возвращать `List` надо из контроллера. И userId принимать в контроллере НЕЛЬЗЯ (иначе - для чего аторизация?). + - 5.6: В концепции REST при update дополнительно принято передавать id (см. `AdminRestController.update`) + - 5.7: Для получения всей своей еды сделайте отдельный `getAll` без применения фильтра +- 6: Проверьте корректную обработку пустых значений date и time (в частности, если все значения пустые, должен выводится весь список) +- 7: `id` авторизированного пользователя получаем так: `SecurityUtil.authUserId()`, cм. `ProfileRestController` +- 8: В `MealServlet` + - 8.1: Закрывать springContext в сервлете грамотнее всего в `HttpServlet.destroy()`: если где-то в контексте Spring будет ленивая инициализация, метод-фабрика, не синглтон-scope, то контекст понадобится при работе приложения. У нас такого нет, но делать надо все грамотно. + - 8.2: Не храните параметры фильтра как члены класса сервлета, это не многопоточно! Один экземпляр сервлета используется всеми запросами на сервер, попробуйте дернуть его из 2х браузеров. + - 8.3: В сервлете нельзя использовать `@Autowired` и `@Component`. См вопрос выше- "Почему нельзя сервлет помещать в Spring контекст?" + +#### Если с ДЗ большие сложности, можно получить итоговые Meal интерфейсы для сверки в личке (`@Katherine`, `@Valeria`). +И напоследок история от Татьяны: +> 2.1 По id еды и авторизованному пользователю нужно проверить ее принадлежность. + +На примере уточню: +Вася Пупкин нашел неименную банковскую карточку, т.е. номер есть, но имени нет. +Т.к. Вася не очень честный человек, то решил снять деньги с чужой карточки. +Наклеил *стикер со своим именем* на карточку и пришел в банк. Дает свой паспорт и карточку операционисту и просит снять всю наличность. +Варианты: +1. операционист сверяет стикер на карточке и паспорт - все ок, Вася получает наличность +2. операционист не обращает внимания на стикер, *а делает запрос в БД по номеру и сверяет ФИО в БД с паспортом* - ФИО не совпали, Вася в пролете + +Кто и так это понимает, тому небольшой спойлер. А кто не понимает, может, переспросят, пообсуждают. diff --git a/lessons/lesson03.md b/lessons/lesson03.md new file mode 100644 index 00000000000..278209ac802 --- /dev/null +++ b/lessons/lesson03.md @@ -0,0 +1,392 @@ +# Стажировка Topjava + +## [Материалы занятия](https://drive.google.com/drive/u/0/folders/0B9Ye2auQ_NsFT1NxdTFOQ1dvVnM) (скачать все патчи можно через Download папки patch) +> **ВНИМАНИЕ! При удалении класса из исходников, его скомпилированная версия все еще будет находиться в target (и classpath). В этом случае (или в любом другом, когда проект начинает глючить) сделайте `mvn clean`.** + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW02 +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. [Реализация репозиториев](https://drive.google.com/file/d/1hZay5jV-mVEByMnDveZ36jUAY0I3WChT) + +#### Apply 3_01_HW2_repositories.patch + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. [Фильтрация в репозитории](https://drive.google.com/file/d/1s1tAsopU60gRvMaL53TP9EjS1gap99s3) +Метод `MealRepository.getBetweenHalfOpen` мы используем в следующем патче для фильтрации по целым дням. +Используем `LocalDateTime` вместо `LocalDate` с прицелом на то, что в DB будем делать тип даты - `timestamp`. + +#### Apply 3_02_HW2_repo_filters.patch +> - Исправил багу в `usersMealsMap.computeIfAbsent(userId, ConcurrentHashMap::new)` - в этом случае создается `new ConcurrentHashMap(userId)`, что неверно + - [Spring `@Nullable` аннотации](https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. [Meals Layers](https://drive.google.com/file/d/1jwd4Yhdy434fUAQyjpsZOO24XT-4lfp8) +> - В `DateTimeUtil` переименовал методы. Название должно описывать, "что метод делает", а не "зачем его вызывают". +> - Переименовал `getBetween` для дат. Для времени у нас полуоткрытый диапазон, для дат - закрытый. + +#### Apply 3_03_HW2_meal_layers.patch +- [Should services always return DTOs, or can they also return domain models?](http://stackoverflow.com/questions/21554977/548473) +- [Mapping Entity->DTO goes in which application layer: Controller or Service?](https://stackoverflow.com/a/35798539/548473) + +### Рефакторинг InMemory репозиториев +#### Apply 3_04_refactor_repository.patch +- сделал базовый `InMemoryBaseRepository` +- наследую от него `InMemoryUserRepository` +- использую его в `InMemoryMealRepository` вместо `Map` + +Обратите внимание на `InMemoryBaseRepository.counter` - счетчик один, общий для всех хранимых объектов + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. [HW2 Optional](https://drive.google.com/file/d/1yzNvGBgjgtuKXDFo983OqtTNoHDbyn1z) +#### Apply 3_05_HW2_optional_MealServlet.patch +> - Заменил `@Depricated StringUtils.isEmpty` на `hasLength` (условие приходится инвертировать) +> - Убрал логирование (уже есть в контроллере) +> - `assureIdConsistent` позволяет в контроллере обновлять еду с `id=null` + +#### Apply 3_06_HW2_optional_filter.patch +- [JSP Implicit Objects](https://stackoverflow.com/a/1890462/548473) +- [Использование data-* атрибутов](https://developer.mozilla.org/ru/docs/Web/Guide/HTML/Using_data_attributes), имена сделал [low-case через дефисы](https://stackoverflow.com/questions/36176474/548473) + +#### Apply 3_07_HW2_optional_select_user.patch + +### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопросы по HW2 + +> Что делает `repository.computeIfAbsent / computeIfPresent` ? + +Всегда пробуйте ответить на вопрос сами. Достаточно просто зайти по Ctrl+мышка в реализацию и посмотреть javadoc и **их дефолтную реализацию** + +> Почему выбрана реализация `Map>` а не `Meal.userId + Map` ? + +В данном случае двойная мапа (мультимапа) - способ хранения, который не требует итерирования (перебора всех значений) и он проще. С другой стороны затраты по памяти в этом решении больше. + +## Занятие 3: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Коротко о жизненном цикле Spring контекста. +#### Apply 3_08_bean_life_cycle.patch +> ** Добавил в pom `javax.annotation-api` для JDK больше 8. Этот пакет [убрали из JDK начиная с версии 9](https://stackoverflow.com/a/46502132/548473). + +- Spring изнутри. Этапы инициализации контекста. +- Ресурсы: + - Евгений Борисов. Spring, часть 1 + - Евгений Борисов. Spring, часть 2 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. [Тестирование через JUnit](https://www.youtube.com/watch?v=wSVg6_iK2Aw) +### ВНИМАНИЕ!! Перед накаткой патча создайте каталог test (из корня проекта путь `\src\test` или `\src\test\java`), иначе часть файлов попадет в `src\main`. + +> - "Deeply mutable" константы `UserTestData.user/admin` [сделал lowercase](https://google.github.io/styleguide/javaguide.html#s5.2.4-constant-names). +> - Добавил для тестирования в данные `User guest` без ролей и без еды. + +- все классы, которые не нужны при работе приложения переносятся в test (и не включаются в сборку) +- в тестах очень частая ошибка - менять местами `expected` (ожидаемое) и `actual` (фактическое) значения. +- [Регрессионное тестирование](https://ru.wikipedia.org/wiki/Регрессионное_тестирование) +- [Разработка через тестирование](https://ru.wikipedia.org/wiki/Разработка_через_тестирование) +- [Unit тестирование с JUnit](https://devcolibri.com/unit-тестирование-с-junit/) +- [maven-surefire-plugin](https://maven.apache.org/surefire/maven-surefire-plugin/usage.html) +- Дополнительно: + - [JUnit 4](http://junit.org/junit4) + - [Тестирование в Java. JUnit](http://habrahabr.ru/post/120101/) + - [Юнит-тестирование для чайников](https://habr.com/ru/post/169381/) + - [Тестирование кода Java с помощью фреймворка JUnit](https://www.youtube.com/watch?v=z9jEVLCF5_w) (youtube) + +#### Apply 3_09_add_junit.patch + +### После патча сделайте `clean` и [обновите зависимости Maven](https://github.com/JavaOPs/topjava/wiki/IDEA#maven_update), чтобы IDEA определила сорсы тестов +#### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопрос: почему проект упадет при попытке открыть страничку еды? (в логе смотреть самый верх самого нижнего исключения). Чинить приложение будем в HW03. + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Spring Test +> - поменял `@RunWith`: `SpringRunner` is an alias for the `SpringJUnit4ClassRunner` +#### Apply 3_10_add_spring_test.patch +- [Spring Testing](https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. Базы данных. Обзор NoSQL и Java persistence solution без ORM. +### Postgres можно установить локально и/или через Docker - пригодиться! + +-------------- +### Установка локально: + +> **Рекомендуется PostgreSQL 13** +- PostgreSQL. +- [PostgreSQL JDBC Driver](https://github.com/pgjdbc/pgjdbc) +- Установка PostgreSQL. +- Чтобы избежать проблем с правами и именами каталогов, [**рекомендуют установить postgres в простой каталог, например `C:\Postgresql`**. И при проблемах создать каталог data на другом диске](https://stackoverflow.com/questions/43432713/548473). Если Unix, [проверить права доступа к папке (0700)](http://www.sql.ru/forum/765555/permissions-should-be-u-rwx-0700). Название ПК и имя пользователя должны быть латиницей (или можно устанавливать сервер [отсюда](https://postgrespro.ru/windows)) + - [Установка PostgreSQL в UBUNTU](https://github.com/JavaOPs/startup/blob/main/postgres.md) + +> Создать в pgAdmin новую базу `topjava` и новую роль `user`, пароль `password` + +![image](https://cloud.githubusercontent.com/assets/13649199/18809406/118f9c48-8283-11e6-8f10-d8291517a497.png) + +> Проверьте, что у user в Privileges есть возможность авторизации (особенно для pgAdmin4) + +или в UNIX командной строке: +``` +sudo -u postgres psql +CREATE DATABASE topjava; +CREATE USER "user" WITH password 'password'; +GRANT ALL PRIVILEGES ON DATABASE topjava TO "user"; +``` + +-------------- +### Установка через Docker (если у вас Windows, рекомендую этот вариант уже после выполнения ДЗ - с ним можно долго провозиться) +Для работы современного ПО часто требуется большая настроенная инфраструктура: RDBMS, NoSQL, Kafka, RabbitMQ и др. Кроме того, концепция микросервисов подразумевает +запуск каждого сервиса в изолированной среде, их быстрое поднятие и масштабирование. +Инструмент [Docker](https://ru.wikipedia.org/wiki/Docker) позволяет «упаковать» приложение со всем его окружением и зависимостями в контейнер, который может быть развёрнут на любой Linux-системе, что делает его незаменимым инструментом разработчика. +Аналогично GitHub, для контейнеров есть репозиторий [DockerHub](https://hub.docker.com/), куда можно публиковать свои контейнеры и откуда можно брать готовые контейнеры со стандартным ПО. +Таким образом Docker служит для: +- Ускорения процесса настройки окружения. Нет нужды в установке сторонних программ вроде PostgreSQL, Redis, Elasticsearch. Они могут быть запущены в контейнерах. +- Удобной инкапсуляции приложений, которые можно представить как единый контейнер, а не набор файлов и инструкций развертывания. +- Одинакового поведения приложений на локальном компьютере и тестовом/прод-сервере. +- Простого и понятного мониторинга. +- Легкого масштабирования. Если вы сделали свое приложение правильно, то оно будет готово к масштабированию не только в Docker. + +[**Основы и руководство по Docker**](https://tproger.ru/translations/how-to-start-using-docker/) +[Docker - понятный туториал](https://badtry.net/docker-tutorial-dlia-novichkov-rassmatrivaiem-docker-tak-iesli-by-on-byl-ighrovoi-pristavkoi/) + +#### Для разных ОС установка Docker отличается. Для Windows часто это достаточно заморочно, возможно лучший вариант - [собственный UNIX хостинг](https://github.com/JavaOPs/startup). Он также пригодиться для практики с терминалом Linux и для деплоя приложения в конце стажировки. +- для [Windows без поддержки Hyper-V](https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/reference/hyper-v-requirements) требуется Docker Toolbox, +для Windows 10 Pro and Enterprise - Docker Desktop. [Инструкция по проверке виртуализации и установке](https://devconnected.com/how-to-install-docker-on-windows-7-8-10-home-and-pro/) +- На Windows Home можно поставить Docker Desktop, предварительно [установив WSL2 (Windows Subsystem for Linux)](https://docs.microsoft.com/ru-ru/windows/wsl/install) + - Убедитесь, что wsl версии 2: возможно понадобится включить в BIOS виртуализацию (гуглится по материнской плате) и [отключить в Windows гипервизор](https://sysadmintips.ru/kak-vykljuchit-virtualizaciju-hyper-v-windows-10.html#Otklucenie_Hyper-V_v_Windows_10_cerez_Programmy_i_komponenty) + - [Upgrade version from WSL 1 to WSL 2](https://docs.microsoft.com/en-us/windows/wsl/install#upgrade-version-from-wsl-1-to-wsl-2) + - [Обновление WSL до версии 2](https://docs.microsoft.com/en-us/windows/wsl/install-manual) + - Команды из cmd/PoswrShell: +``` +wsl -l -v : проверить версию +wsl --set-version Ubuntu-20.04 2 : поставить версию 2 +wsl : запустить Ubuntu + +sudo service docker status <- проверка статуса +sudo service docker start <-старт +sudo service docker restart <-рестарт +``` +- [Установка для Mac и Linux](https://docs.docker.com/get-started/) +- Установка под Ubuntu: ` sudo apt install docker.io` + - [How can I use docker without sudo?](https://askubuntu.com/a/477554/1357134) + +#### Работе Docker могут мешать: DrWeb, firewall, анитивирусы + +После установки у меня не запускался `Docker Quickstart Terminal`, я запустил Docker Toolbox из `Git Bash`: +- Запустил Git Bash +- Перешел в каталог С:\...\DockerToolbox: `cd /C/../DockerToolbox` +- Запустил `start.sh` + +#### Разворачиваем Postgres: +- загружаем [контейнер с последним postgres](https://hub.docker.com/_/postgres): `docker pull postgres` +- если на локальной машине уже запущен сервис postgres, надо его остановить: для Windows я запустил `C:\Windows\System32\cmd.exe as Admin` и остановил командой `net stop postgresql-x64-10` +- запускаем контейнер с postgres: `docker run -p 5432:5432 -d --name topjava_db -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=topjava postgres` + -p: преобразование портов HOST PORT:CONTAINER PORT (или -P: все порты) + -d: флаг запускает контейнер в фоновом режиме (как демон) + -e: задание переменной окружения + --name [имя]: устанавливает имя демона для нового контейнера + +[Основные команды Docker](https://tproger.ru/translations/top-10-docker-commands): +``` +docker ps -a : посмотреть все контейнеры +docker stop topjava_db : остановить наш контейнер +docker start topjava_db : запустить его +docker rm topjava_db : удалить +docker help : справка по командам +``` + +Если Docker установился через Docker Toolbox, он запускается в VirtualBox, необходимо настроить [проброс порта 5432 из VirtualBox на локальную машину](https://losst.ru/probros-portov-virtualbox). Порты можно делать одинаковые: Host Post: 5432, Guets Port: 5432 +Проверить postgres порт 5432 можно через +- Unix: `netstat -an |grep 'LISTENING'` +- Windows: `netstat -an |find /i "listening"` + +Если порт 5432 слушается, можно приконнектиться к нему из IDEA, следующее видео. + +------------ + +- Обзор NoSQL систем. CAP +- DB-Engines Ranking +- JDBC +- Обзор Java persistence solution без ORM: commons-dbutils, + Spring JdbcTemplate, + MyBatis, JDBI, jOOQ +- Основы: + - Реляционная СУБД + - Реляционные базы + - [Руководство по проектированию реляционных баз данных](https://habr.com/ru/post/193136/) + - Уроки по JDBC + - Postgres Guide + - PostgreSQL Tutorial + - Базы данных на Java + - Возможности JDBC — второй этап +- Дополнительно: + - [Документация к PostgreSQL](https://postgrespro.ru/docs/postgrespro) + - [Книги по PostgreSQL](https://postgrespro.ru/education/books) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 9. Настройка Database в IDEA. +Heroku стал платным, вместо него предлагаю зарегистрировать [**собственный выделенный хостинг**](https://github.com/JavaOPs/startup). +По цене от 150р./мес имеем: собственный сайт, практика с Linux, деплой своих приложений, развертывание Docker. + +#### Apply 3_11_add_postgresql.patch +- Настройка Database в IDEA и запуск SQL. + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 10. Скрипты инициализации базы. Spring Jdbc Template. +#### Apply 3_12_db_implementation.patch +> - в `JdbcUserRepository` +> - в `getByEmail()` заменил `queryForObject()` на `query()`. Загляните в код: `queryForObject` бросает `EmptyResultDataAccessException` вместо нужного нам `null`. +> - в `save()` добавил проверку на несуществующей в базе `User.id` +> - в классе `JdbcTemplate` есть настройки (`queryTimeout/ skipResultsProcessing/ skipUndeclaredResults`) уровня приложения (если они будут меняться, то, скорее всего, везде в приложении). + Мы можем дополнительно сконфигурировать его в `spring-db.xml` и использовать в конструкторах `NamedParameterJdbcTemplate` и в `SimpleJdbcInsert` вместо `dataSource`. + +Для запуска скриптов кликнуть правой мышкой на табе скрипта, выбрать Run и в диалоге добавть чз "+" базу + +![image](https://user-images.githubusercontent.com/11200258/155393771-3331289b-64bf-4c33-b804-b25d55ffb298.png) +![image](https://user-images.githubusercontent.com/11200258/155394005-6702bd7b-7fa5-411d-93a3-b933c9605211.png) + +- Дополнительно: + - [alishev: Spring Framework. Урок 27: JdbcTemplate](https://www.youtube.com/watch?v=YozbZQ7PxtQ&list=PLAma_mKffTOR5o0WNHnY0mTjKxnCgSXrZ&index=27&ab_channel=alishevalishev) + +![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) **Вопрос: почему отображение еды все еще не работает? (чинить приложение будете в HW03)** + +- Подключение Spring Jdbc. +- Конфигурирование DataSource. Property Placeholder +- Интеграция `JdbcUserRepository` с DB: [Добавить поддержку DB в JDBC](https://github.com/JavaOPs/topjava/wiki/IDEA#user-content-Добавить-поддержку-db-в-jdbc) + +> Проверьте, что в контекст Spring проекта включены оба файла конфигурации + +![image](https://cloud.githubusercontent.com/assets/13649199/24730713/eb21456a-1a6d-11e7-997c-fb4ad728ba45.png) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 11. Тестирование UserService через AssertJ. +#### Apply 3_13_test_UserService.patch +> - В тестах `delete` и `create` проверяю результат напрямую (не через `getAll`) +> - В `UserTestData` добавил вспомогательные `getNew()` и `getUpdated()` +> - При выполнении функционала `create/update` объекты могут измениться, и мы не можем считать их эталонными. Поэтому при сравнении мы создаем эталон еще раз. +> - Поставил `@Ignore` в inmomory тестах + +- [Tutorial: testing with AssertJ](http://www.vogella.com/tutorials/AssertJ/article.html) +- [Spring Testing Annotations](https://docs.spring.io/spring/docs/4.3.x/spring-framework-reference/htmlsingle/#integration-testing-annotations-spring) +- [The JPA hashCode() / equals() dilemma](https://stackoverflow.com/questions/5031614/548473) +- [Hibernate: implementing equals() and hashCode()](https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#mapping-model-pojo-equalshashcode) +- [Junit Matcher for comparators](https://stackoverflow.com/questions/17949752/548473) +- [AssertJ custom comparison strategy](http://joel-costigliola.github.io/assertj/assertj-core-features-highlight.html#custom-comparison-strategy). [AssertJ field by field comparisons](http://joel-costigliola.github.io/assertj/assertj-core-features-highlight.html#field-by-field-comparison) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 12. Логирование тестов. +#### Apply 3_14_test_logging.patch +> - Новый PostgreSQL JDBC Driver [логирует через java.util.logging](https://github.com/pgjdbc/pgjdbc#changelog). [Направил логирование в SLF4J](http://stackoverflow.com/a/43242620/548473) +> - Поменял формат вывода. См. [Logback Layouts](https://logback.qos.ch/manual/layouts.html) + +- Ресурсы, которые кладутся в classpath, maven при сборке берет из определенных каталогов `resources` ([Introduction to the Standard Directory Layout](https://maven.apache.org/guides/introduction/introduction-to-the-standard-directory-layout.html)). Их можно настраивать через [maven-resources-plugin](https://maven.apache.org/plugins/maven-resources-plugin/examples/resource-directory.html), меняем в проекте Masterjava. + +#### Apply 3_15_fix_servlet.patch +**И снова вопрос: почему отображение еды все еще не работает? (чинить приложение будете в HW03)** + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> Что такое REST? + +Коротко посмотрите тут: [понимание REST](http://spring-projects.ru/understanding/rest/). Более подробно мы будем разбирать этот архитектурный стиль API на 7-ом занятии. + +> Какая разница между @BeforeClass and @Before? + +`@BeforeClass` выполняется один раз после загрузки класса (поэтому метод может быть только статический), `@Before` перед каждым тестом. +Также: для чистоты тестов экземпляр тестового класса [пересоздается перед каждым тестом](http://stackoverflow.com/questions/6094081/548473) + +> Тесты в классе в каком-то определенном порядке выполняются ("сверху вниз" например)? + +Порядок по умолчанию неопределен, каждый тест должен быть автономен и не зависеть от других. [См. также "How to run test methods in specific order in JUnit4?"](http://stackoverflow.com/questions/3693626/548473) + +> Обязательно ли разворачивать postgreSQL? + +Желательно: хорошая и надежная ДБ:) Если совсем не хочется - можно работать со своей любимой RDBMS (поправить `initDB.sql`) или работать c postgresql в heroku (креденшелы к нему есть сверху в `postgres.properties`). На следующем уроке добавим HSQLDB, она не требует установки. + +> Зачем начали индексацию с 100000? + +Так удобно вставлять в базу (если будет потребность) записи вручную не мешая счетчику. Но это не общий принцип, часто нумерация идет с 1. + +> Из 5-го видео - "Логика в базе - большое зло". Можно чуть поподробней об этом? + +- Есть успешные проекты с логикой в базе. Те все относительно. +- Логика в базе - это процедуры и триггеры. Нет никакого ООП, переиспользовать код достаточно сложно, никагого рефакторинга, поиска по коду и других плюшек IDE. Нельзя делать всякие вещи типа кэширования, хранения в сессии - это все для логики на стороне java. Например json можно напрямую отдать в процедуру и там парсить и вставлять в таблицы или наоборот - собирать из таблиц и возвращать. +А затем потребуется некоторая логика на стороне приложения и все равно придется этот json дополнительно парсить в java. +Я на таком проекте делал специальную миграцию, чтобы процедуры мигрировать не как sql-скрипты, а каждую процедуру хранить как класс с историей изменений. Если логика: триггеры и простые процедуры записи-чтения, которые не требуют переиспользования кода или +проект небольшой - это допустимо, иначе проект становится трудно поддерживать. Также иногда используют [View](http://postgresql.men/gruber/ch20.html) для разграничения доступа. Например, для финансовых систем, таблицы проводок доступны только для админ учеток, а View просто не дадут увидеть (тем более изменить) данные обычному оператору на уровне СУБД. + +> У JUnit есть ассерты и у спринга тоже. Можно ли обойтись без JUnit? + +Предусловия Spring `Assert` и JUnit-тесты совершенно разные вещи, один другого не заменит. У нас будут предусловия в следующем уроке. + +> Я так понял, что при работе с небольшими записями, VARCHAR быстрее, чем TEXT. Наши записи будут небольшими (255). Почему вы приняли решение перейти на TEXT? + +В отличие от MySql, в Postgres [VARCHAR и TEXT - тоже самое](https://stackoverflow.com/questions/4848964/548473) + +> Зачем при создании таблицы мы создаем `CREATE UNIQUE INDEX` и `CREATE INDEX`. При каких запросах он будет использоваться? + +UNIQUE индекс нужен для обеспечения уникальности, DB не даст сделать одинаковый индекс. Индексы используется для скорости выполнения запросов. Обычно они используются, когда в запросе есть условия, на которые сделан индекс. Узнать по конкретному запросу можно запросив план запроса: см. Оптимизация запросов. Основы EXPLAIN в PostgreSQL. На измерение производительности с индексами посмотрим в следующем уроке. + +> А это нормально, что у нас в базе у meals есть userId, а в классе - нет? + +Ненормально, когда в приложении есть "лишний" код, который не используется. Для ORM он нам понадобится - добавим `Meal.user`. + +> Почему мы делаем один sequence на разные таблицы? + +Мы будем использовать Hibernate. По умолчанию он делает глобальный sequence на все таблицы. В этом подходе есть [как плюсы, так и минусы](http://stackoverflow.com/questions/1536479/548473). + +> Каким образом попадают в тесты классы, расположенные в каталоге `test`, если в конфигурации спринга нет указание на ее сканирование? + +Сканируются не папки, а пакеты. Обычно тесты классов располагают в том же самом пакете каталога `test`. Таким образом тесты могут видеть поля классов `main` с видимостью по умолчанию (внутри пакета). При этом классы `test` видят `main`, а наоборот нет. Когда приложение деплоится, в коде тестов быть не должно! + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW03 +- 1 Понять, почему перестали работать `SpringMain, InMemoryAdminRestControllerTest, InMemoryAdminRestControllerSpringTest`. Чиним в Optional. +- 2 Дополнить скрипты создания и инициализации базы таблицей `meals`. Запустить скрипты на вашу базу (через Run). Порядок таблиц при DROP и DELETE важен, если они связаны внешними ключами (foreign key, fk). Проверьте, что ваши скрипты работают. + - 2.1 Сделать индексы к таблице `meals`: запретить создавать у одного и того-же юзера еду с одинаковой dateTime. +Индекс на pk (id) postgres создает автоматически: [Postgres and Indexes on Foreign Keys and Primary Keys](http://stackoverflow.com/questions/970562/548473) + - [Индекс_базы_данных](https://ru.wikipedia.org/wiki/Индекс_(базы_данных)) + - [PostgreSQL: индексы](https://postgrespro.ru/docs/postgresql/10/indexes-intro) + - [Postgres Guide: Indexes](http://postgresguide.com/performance/indexes.html) + - [Оптимизация запросов. Основы EXPLAIN в PostgreSQL](https://habrahabr.ru/post/203320/) + - [Оптимизация запросов. Часть 2](https://habrahabr.ru/post/203386/) + - [Оптимизация запросов. Часть 3](https://habrahabr.ru/post/203484/) + +> ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Как правильно придумать индекс для базы? Указать в нем все поля, комбинация которых создает по смыслу уникальную запись, или какие-то еще есть условия? + +Индекс нужно делать по тем полям, по которым будут искаться записи (участвуют в WHERE, ORDER BY). Уникальность - совсем необязательное условие. Индексы ускоряют поиск по определенным полям таблицы. Они не бесплатные (хранятся в памяти, замедляется вставка), поэтому на всякий случай их делать не надо. Также не строят индексы на колонки с малым процентом уникальности (например поле "М/Ж"). Поля индекса НЕ КОММУТАТИВНЫ и порядок полей в описании индекса НЕОБХОДИМО соблюдать (в силу использования B-деревьев и их производных как поисковый механизм индекса). При построении плана запроса EXPLAIN учитывается количество записей в базе, поэтому вместо индексного поиска (Index Scan) база может выбрать последовательный (Seq Scan). Проверить, работают ли индексы можно отключив Seq Scan. Также см. Queries on the first field of composite index + +- 3 Реализовать через Spring JDBC Template `JdbcMealRepository` + - 3.1. сделать каждый метод за один SQL запрос + - 3.2. `userId` в класс `Meal` вставлять НЕ надо, делаем _foreign key_ только в базе (для UI и REST это лишние данные, userId это id залогиненного пользователя) + - 3.3. `JbdcTemplate` работает через сеттеры. Вместе с конструктором по умолчанию их нужно добавить в `Meal` + - 3.4. Список еды должен быть отсортирован (тогда мы его сможем сравнивать с тестовыми данными). Кроме того это требуется для UI и API: последняя еда наверху. +- 4 Проверить работу MealServlet, запустив приложение + +### Optional +- 5 Сделать `MealServiceTest` из `MealService` и реализовать тесты для `JdbcMealRepository`. +> По `Ctrl+Shift+T` (выбрать JUnit4) можно создать тест для конкретного класса, выбрав для него нужные методы. Тестовый класс создастся в папке `test` в том же пакете, что и тестируемый. + - 5.1 Сделать тестовые данные `MealTestData` (точно такие же, как вставляем в `populateDB.sql`). + - 5.2 Сделать тесты на чужую еду (delete, get, update) с тем, чтобы получить `NotFoundException` и `duplicateDateTimeCreate`, аналогичный `duplicateMailCreate`. +- 6 Починить `SpringMain, InMemory*Test`. **InMemory тесты должны использовать реализацию в памяти** + +---------------------------- +### Итоги занятия после выполнения ДЗ: +Наше приложение стало хранить данные в PostgreSQL. +Добавили в приложение JUnit тесты. +Разделили Spring контекст для работы с памятью и с БД. + +---------------------------- + +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Решение проблем + +> Из каталога `main` не видятся классы/ресурсы в `test` + +Все что находится в `test` используется только для тестов и недоступно в основном коде. + +> Из `IDEA` не видятся ресурсы в каталоге `test` + +- Сделайте Reimport All в Maven окне + +![image](https://cloud.githubusercontent.com/assets/13649199/18831806/7e43bedc-83f0-11e6-97db-67d4e1a7599f.png) + +> В UserService и MealService подчеркнуты красным repository, ошибка: Could not autowire. There is more than one bean of 'MealRepository' type. + +- Spring test контекст не надо включать в Spring Facets проекта, там должны быть только `spring-app.xml` и `spring-db.xml`. Для тестовых контекстов поставьте чекбокс `Check test files` в Inspections. + +![image](https://cloud.githubusercontent.com/assets/13649199/18831817/8a858f22-83f0-11e6-837e-bf5317b33b3a.png) + +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Типичные ошибки и подсказки по реализации + +- 1: В `MealTestData` еду делайте константами. Не надо `Map` конструкций! +- 2: SQL case-insensitive, **не надо писать в стиле Camel**. В POSTGRES возможны case-sensitive значения, их надо в кавычки заключать (обычно не делают). +- 3: ЕЩЕ РАЗ: `InMemory` тесты должны идти на `InMemory` репозитории +- 4: **Проверьте, что возвращает `JdbcMealRepository` при обновлении чужой еды** +- 5: В реализации `JdbcMealRepository` одним SQL запросом используйте возвращаемое `update` значение `the number of rows affected` +- 6: При тестировании не портите эталонные тестовые объекты из `MealTestData` +- 7: Проверьте, что все, что относится к тестам, находится в каталоге `test` (не попадает в сборку проекта) +- 8: **Еще раз: в тестах нельзя сравнивать объекты через `JUnit Assert` и `assertThat().isEqualTo`, потому что в этом случае сравнение будет происходить через `AbstractBaseEntity.equals`, который сравнивает объекты только по `id`. Мы не можем переопределять `equals` для объектов модели, тк будем использовать JPA (см. [The JPA hashCode() / equals() dilemma](https://stackoverflow.com/questions/5031614/548473))** +- 9: НЕ делайте склейку SQL запросов вручную из параметров, только через `jdbcTemplate` параметры! См. [Внедрение_SQL-кода](https://ru.wikipedia.org/wiki/Внедрение_SQL-кода) +- 10: Напомню: `BeanPropertyRowMapper` работает через отражение. Ему нужны геттеры/сеттеры и имена полей должны "совпадать" с колонками `ResultSet` (Column values are mapped based on matching the column name as obtained from result set metadata to public setters for the corresponding properties. The names are matched either directly or by transforming a name separating the parts with underscores to the same name using "camel" case). +- 11: Для разделения тестов InMemory и Jdbc можно использовать разные Spring context. В том числе переопределять контест для тестов в `src\test\resources` +- 12: Обязательно протестируйте в `MealServiceTest` нормальное поведение методов (по аналогии с `UserServiceTest`) diff --git a/lessons/lesson04.md b/lessons/lesson04.md new file mode 100644 index 00000000000..a60e11df52e --- /dev/null +++ b/lessons/lesson04.md @@ -0,0 +1,253 @@ +# Стажировка Topjava + +## Материалы занятия + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW3 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. [HW03: meals + JdbcMealRepository](https://drive.google.com/file/d/1_IIYBP5l2aHxHaY_j1bqFWj1maACvNpa) +- [Последовательность столбцов в составном индексе](https://ru.wikipedia.org/wiki/Индекс_(базы_данных)#Последовательность_столбцов_в_составном_индексе) +- Дополнительно: для `JdbcMealRepository.save` можно использовать [CombinedSqlParameterSource](https://stackoverflow.com/questions/13339171/548473) +- В ответе на [Why is SELECT * considered harmful?](https://stackoverflow.com/questions/3639861) есть случаи, когда она допустима (наш случай): `when "*" means "a row"` + +#### `SpringMain, InMemoryAdminRestControllerTest, InMemoryAdminRestControllerSpringTest` починим в патче `4_7_create_inmemory_test_ctx` (видео 4) + +#### **Apply 4_1_HW3.patch** +> Таблицы [принято именовать в единственном числе](https://stackoverflow.com/questions/338156/548473) (`users` - исключение, тк это ключевое слово). +Переименовал таблицы в `user_role` и `meal`. Если у вас остались старая база с множественными именами - удалите их вручную. + +### [Сравнение времени выполнения для разных индексов](../doc/meals_index.md) +- На id как на primary key индекс создается автоматически +- Все запросы в таблицу meals у нас идут с `user_id` +- По полю `date_time` не только есть запросы, но и сортируем результат. То есть это поле - хороший кандидат для индексирования +- Следует иметь в виду, что индексы ускоряют операции чтения, но замедляют вставку и удаление. Поэтому необходим анализ в реальном приложении +- [Оптимизация запросов. Основы EXPLAIN в PostgreSQL](https://habrahabr.ru/post/203320/) +- [Оптимизация запросов. Часть 2](https://habrahabr.ru/post/203386/) +- [Оптимизация запросов. Часть 3](https://habrahabr.ru/post/203484/) +- [Документация Postgres: индексы](https://postgrespro.ru/docs/postgresql/9.6/indexes.html) + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW3 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. [HW03 Optional: Meals tests](https://drive.google.com/file/d/1RfO0Irz8ayw2ivnjffUol20BQrKpu-jg) + +#### **Apply 4_2_HW3_optional.patch** +> Убрал `throws Exception` из тестов. IDEA по умолчанию перестала их добавлять. +> В `MealServiceTest.updateNotOwn` добавил дополнительную проверку, что еда в тесте не модифицировалась. + +#### **Apply 4_3_tests_refactoring.patch** +> - Переименовал класс генерации матчеров в `MatcherFactory` +> - Переименовал статический метод генерации ([Блох Джошуа, "Java. Эффективное программирование."](http://javaops.ru/view/books)) на `usingIgnoringFieldsComparator`. + +#### **Apply 4_4_HW3_fix_logging.patch** +- [Вызов статического метода из конфигурации спринга](https://stackoverflow.com/a/27296470/548473) + +## Занятие 4: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Методы улучшения качества кода + +#### Поменяйте в `readme.md` сверху ссылку на свой `Codacy Badge` с сайта Codacy: `https://app.codacy.com/gh/[github_accaunt]/topjava/settings` + +> - Плагины проверки качества кода теперь объединены в один **QAPlug** +> - Codacy Check code (проверка стиля и поиск багов в коде). +> - добавил [Codacy configuration file](https://support.codacy.com/hc/en-us/articles/360005097654-Ignore-files-from-Codacy-analysis) для исключения из проверок содержимого `webapp` и `READ.me` (на нашем проекте он выдает на них кучу ошибок) +> - после правок паттернов можно сделать [повторный анализ](https://support.codacy.com/hc/en-us/articles/213840489-How-do-I-reanalyze-my-project-), с результатами тормозит + +#### Сделайте `push` для отображения результатов текущего состояния проекта. + +#### **Apply 4_5_improve_code.patch** +> - Добавил проверки предусловий `Assert.notNull` в сервисы +> - Добавил конфигурацию `.codacy.yml` +> - Ввел удобный метод `AbstractBaseEntity.id()` + +- Контрактное программирование, Программирование по контракту +- Comparison Preconditions in Java +- IDEA Settings -> Plugins -> Browse repositories... Add [QAPlug: PMD/FindBugs/Checkstyle/Hammurapi](https://qaplug.com/about/) + - Tools -> QAPlug -> Analyze Code... +- IDEA [Analyze | Inspect Code](https://www.jetbrains.com/help/idea/running-inspections.html) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Spring: инициализация и популирование DB +#### **Apply 4_6_init_and_populate_db.patch** +- [Инициализация базы при старте приложения](https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#jdbc-initializing-datasource-xml) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Подмена контекста при тестировании +#### **Apply 4_7_create_inmemory_test_ctx.patch** +> Переименовал `mock.xml` в `inmemory.xml` + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6.1 [Основные определения ORM. Hibernate. JPA](https://drive.google.com/file/d/1S--FEm2R2LSgjN1ALAj7sN4AAFJ7rt4r) +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6.2 ORM. Hibernate. JPA. +![image](https://user-images.githubusercontent.com/13649199/139490982-0e8d6a6a-f1b6-4db0-8c61-f49f2056c274.png) + +Entity- класс (объект Java), который в ORM маппится в таблицу DB. +> - Убрал дублирование объявления `unique` для `User.email` + +[EntityManager](https://jsehelper.blogspot.com/2016/04/java-persistence-api-jpa-2.html) - это по сути прокси-обертка над Hibernate Session, которая создается каждый раз при открытии транзакции. + +- ВНИМАНИЕ: патч меняет `postgres.properties`, в котором у вас, возможно, свои креденшелы к базе +- `hibernate-core` с 5.2.x включает `hibernate-entitymanager` и `hibernate-java8`, то есть конверторы Time API уже не нужны +> - JPA support for Java 8 new date and time API +> - What's new in Hibernate 5? +> - JPA support for Java 8 new date and time API +- [EL implementation provided by the container. In a Java SE you have to add an implementation as dependency to your POM file](http://hibernate.org/validator/documentation/getting-started/#unified-expression-language-el): добавил `javax.el` зависимость со `scope=provided` + +#### **Apply 4_8_add_jpa.patch** +> - **[Настройка JPA в IDEA](https://github.com/JavaOPs/topjava/wiki/IDEA#jpa). +ПРОВЕРЬТЕ, что у вас не подтянулись Java EE libraries, все зависимости в проект попадают только через Maven! Перед настройкой сначала подтяните его зависимости** +> - `indexes` и `uniqueConstraints` в Entities (у нас `User`) используются только при создании таблицы средствами JPA (автогенерации БД при запуске приложения). + В случае, если таблицы создаются скриптом, эти опции будут проигнорированы. У нас они дублируют ограничения в `initDB.sql` и будут использоваться, когда мы будем смотреть на автогенерацию DDL по модели на 7-м занятии. +> - Тесты и приложение ломаются. `MealServiceTest` починится после выполнения HW04 (`JpaMealRepository`) +> - Если вы используете Java 9 и выше, то возникают проблемы с `JAXBException` (пакет `java.xml.bind`). [См. решение](https://www.concretepage.com/forum/thread?qid=531 + +- [JPA - JPQL](https://www.tutorialspoint.com/ru/jpa/jpa_jpql.htm) +- Дополнительно: + - [ORM](http://ru.wikipedia.org/wiki/ORM) + - [JPA и Hibernate в вопросах и ответах](http://habrahabr.ru/post/265061/) + - [Hibernate — о чем молчат туториалы](https://habr.com/ru/post/416851/) + - [Наследование в Hibernate: выбор стратегии](https://habrahabr.ru/post/337488/) + - [Entity Lifecycle Model in JPA & Hibernate](https://thorben-janssen.com/entity-lifecycle-model/) + - [Field vs property access](http://stackoverflow.com/a/6084701/548473) + - [Hibernate: введение и написания Hello world приложения](https://web.archive.org/web/20200810114404/http://www.quizful.net/post/Hibernate-3-introduction-and-writing-hello-world-application) + - [15 reasons why we need to choose Hibernate over JDBC](https://web.archive.org/web/20211201122631/https://habiletechnologies.com/blog/reasons-to-choose-hibernate-over-jdbc/#fin_form_pop) + - [Hibernate or JDBC](https://stackoverflow.com/questions/1353137/548473) + - [Mapping: описание модели Hibernate (hbm.xml/annotation)](http://en.wikibooks.org/wiki/Java_Persistence/Mapping). + - [used in Playframework](https://ru.wikipedia.org/wiki/Hibernate_(библиотека)">Hibernate). Другие ORM: [TopLink](http://en.wikipedia.org/wiki/TopLink), [EсlipseLink](http://en.wikipedia.org/wiki/EclipseLink) + - [Jakarta Persistence (JPA, english wiki)](https://en.wikipedia.org/wiki/Java_Persistence_API) + - [Стратегии генерации PK](http://en.wikibooks.org/wiki/Java_Persistence/Identity_and_Sequencing) + - [hibernate-validator](http://validator.hibernate.org). + - [Описание связей в модели. Ленивая загрузка объекта.](https://web.archive.org/web/20170514002949/http://java.devcolibri.com:80/post/15) + - [JPA definitions](http://docs.jboss.org/hibernate/entitymanager/3.6/reference/en/html/architecture.html#d0e61) + - [Spring expressions: выражения в конфигурации](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions) + - [HQL](https://proselyte.net/tutorials/hibernate-tutorial/hibernate-query-language), [JPQL](http://ru.wikipedia.org/wiki/Java_Persistence_Query_Language) + - Динамические запросы (которые формируются в коде): [Unified Queries for Java](http://www.querydsl.com/), [JPA Criteria API](http://www.objectdb.com/java/jpa/query/criteria) + - [Using the Java 8 Date Time Classes with JPA](https://web.archive.org/web/20170608194049/https://bitbucket.org/montanajava/jpaattributeconverters) + +#### **Apply 4_9_add_named_query_and_transaction.patch** +> Чтобы посмотреть информацию о транзакциях (открытие/закрытие и пр.), можно выставить в конфигурации logback +`` + +- Транзакция. ACID. Уровни изоляции транзакций. +- Spring Transaction Management +- `@Transactional` в тестах. Настройка EntityManagerFactory +- [Эффективное управление транзакциями в Spring](https://habr.com/ru/company/otus/blog/431508/) + +> ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Зачем надо начинать транзакцию, если речь идет только о чтении данных? Начало транзакции при выполнении операции чтения всего лишь добавит лишних накладных расходов +(см. [Стратегии работы с транзакциями, pаспространенные ошибки](http://web.archive.org/web/20170314073834/https://www.ibm.com/developerworks/ru/library/j-ts1/index.html)) + +Вот ответ от Oliver Drotbohm, автора Spring-Data на предложение работать без транзакций для операций чтения (`propagation=Propagation.SUPPORTS`): [Improve performance with Propagation.SUPPORTS for readOnly operation](https://jira.spring.io/browse/DATAJPA-601). Коротко: +- Статья устаревшая и неверно упрощает многие вещи. Есть множество вещей, которые влияют на производительность +- Без транзакции не будет оптимизации по флагу `readOnly` при выполнении JDBC и в управлении ресурсами Spring's JPA (в том числе выключение `flush`) +См. [Non-transactional data access and the auto-commit mode](https://developer.jboss.org/wiki/Non-transactionalDataAccessAndTheAuto-commitMode) + +Справочник: + - Видео: Вячеслав Круглов — Как начинающему Java-разработчику подружиться со своей базой данных? + - Видео: Николай Алименков — Босиком по граблям Hibernate + - Видео: Николай Алименков — Сделаем Hibernate снова быстрым + - Стратегии работы с транзакциями + - Spring transaction propagation tutorial + - Getting Started with JPA + - Java Persistence + - Разделы по Java Persistence API + - Spring Framework transaction management + - Spring Persistence Tutorial + - Working with JPA Entity Objects + - Принципы работы СУБД. MVCC + - MVCC + + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Добавляем поддержку HSQLDB + +#### **Apply 4_10_add_hsqldb.patch** +> - Переделал `jdbc.initLocation`: IDEA не "тупит", если путь к скрипту полностью прописывать в пропертях. + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы + +> Есть несколько аналогичных "встроенных" баз данных. H2, HSQLDB, Derby, SQLite. Почему был выбран HSQLDB? + +Просто с ней приходилось работать. HSQLDB и H2 наиболее популярны. В миграции на spring-boot будем использовать H2. +Здесь интересное краткое описание встраиваемых баз данных в Java. +В HSQLDB нет репликаций, кластеризации и объем данным ограничен несколькими TB. Для большого количества приложений она подходит и для продакшена. См. +- What is HSQLDB limitations? +- HSQLDB в режиме in-process + +> Чистого JPA не существует, т. е. это всего лишь интерфейс, спецификация? Говорим JPA, подразумеваем какой-то ORM фрэймворк? А что тогда используют чистый jdbc, Spring-jdbc, MyBatis? MyBatis не реализует JPA? + +ORM это технология связывания БД и объектов приложения, а JPA - это JavaEE спецификация (API) этой технологии. +Реализации JPA - Hibernate, OpenJPA, EclipceLink, но, например, Hibernate может работать по собственному API (без JPA, которая появилась позже). Spring-JDBC, MyBatis, JDBI не реализуют JPA - это обертки к JDBC. Все ORM и JPA также реализованы поверх JDBC. + +> В зависимостях maven `hibernate-entitymanager` тянет за собой `jboss-logging`. Как будет происходить логирование? + +How do you configure logging in Hibernate 4 to use SLF4J: в нашем проекте автоматически подхватывается `logback-classic`. + +> В чем преимущество Hibernate? + +Hibernate (как любая ORM) реализует мапинг таблиц в объекты Java. Когда мы добавим роли пользователю, вы увидите, насколько код будет проще, чем в jdbc. Также см. 5 Reasons to Use JPA / Hibernate + +> Чем отличается `@Column(nullable = false)` от `@NotNull` и есть ли необходимость указывать обе аннотации ? + +`@Column(nullable = false)` - это атрибуты колонки таблицы базы. `@NotNull` - это валидация, которая происходит в приложении перед вставкой в базу. Если колонка ненулевая, то `NOT NULL` обязательна. Валидация опциональна. Также см. +@NotNull vs @Column(nullable = false) + +> почему мы в бине `entityManagerFactory` не указали диалект базы данных? + +Он [автоматически определяется из `DataSource` драйвера](http://stackoverflow.com/a/39817822/548473) + +> В чем разница между `persist` и `merge` + +Подробный ответ со Stackoverflow с объяснением разницы. Упрощенно: + - `merge`, в отличие от `persist`, делает запрос в базу данных, если entity нет в текущей сессии + - entity, переданный в `merge`, не меняется. Нужно использовать возвращаемый результат + +> `em.merge` при отсутствии старой записи (несуществующий `id`) создает новую. Т. е. в `JpaUserRepository` нарушается логика + +В Hibernate есть такая бага: https://hibernate.atlassian.net/browse/HHH-1661, https://stackoverflow.com/questions/34249483 +- [Hibernate unexpectedly issues INSERT instead of throwing the javax.persistence.OptimisticLockException, when a nonexistent entity is passed to merge()](https://stackoverflow.com/questions/34249483) +- [Should Hibernate Session#merge do an insert when receiving an entity with an ID?](https://stackoverflow.com/questions/21489300) + +Если это действительно наш критичный бизнес-кейс (например, с многопоточным удалением entity), то нужно искать варианты обходного решения. +Если это не бизнес-кейс (попытка поломать или ошибка UI), то оставляем как есть (обычно на практике не парятся). + +> Почему в проекте транзакционность сделана в слое репозитория, а не сервиса? Транзакциями удобнее пользоваться на слое сервисов, так как здесь реализуется бизнес логика и бывает нужно делать несколько операций в одной транзакции. + +С классической точки зрения все транзакции действительно объявляются на уровне сервиса. Мы будем использовать в логике сервиса несколько запросов и тогда сделаем дополнительную транзакцию на методе сервисе. Новая транзакция при этом не создается (по умолчанию используется `Propagation.REQUIRED`, который поддерживают существующую), поэтому несколько `@Transactional` аннотаций ведут себя как одна. Я использую подход `spring-data-jpa` (будет на следующем занятии): в репозитории транзакции объявлять удобно, тк не надо думать о них в сервисах. + +-------------------- + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW4 + +- 1: Сделать из `Meal` Hibernate entity + - Hibernate Validator: @NotNull, @NotEmpty, @NotBlank + - Реализация ManyToOne +- 2: Имплементировать и протестировать `JpaMealRepository`. Проверьте, нет ли в запросах ненужных данный (лишних `JOIN`) + +### Optional + +- 3: Добавить в тесты `MealServiceTest` функциональность `@Rule`: + - 3.1: вывод в лог времени выполнения каждого теста + - 3.2: вывод сводки в конце класса: имя теста - время выполнения +- JUnit @Rules + +--------------------- +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Типичные ошибки и подсказки по реализации +- 1: Т.к. JPA работает с объектами, мы не можем использовать `userId` для сохранения. Можно сделать, например, так: + + User ref = em.getReference(User.class, userId); + meal.setUser(ref); + + При этом от `User` нам нужен только `id`. Над `id` создается lazy прокси, который обращается к базе при запросе любого поля. Т. е. у нас запроса в базу за юзером не будет: проверьте по логам Hibernate. + +**Внимание: проверять запросы Hibernate нужно через run. Если делаете debug и брекпойнт, то могут делаться лишние запросы к базе (дебаггер дергает `toString`)** + +- 2: В JPQL запросах можно писать: `m.user.id=:userId` +- 3: При реализации `JpaMealRepository` предпочтительно не использовать `try-catch` в логике реализации. Но, если очень хочется, то ловить только специфические исключения (напр. `NoResultException`), чтобы, например, при отсутствии коннекта к базе приложение отвечало адекватно. +- 4: Мы будем смотреть генерацию db-скриптов из модели. Для корректной генерации нужно в `Meal` добавить `uniqueConstraints` +- 5: При записи в базу через `namedQuery` валидация энтити не работает, только валидация в БД. +- 6: Результат `AssertionError` печатает результаты через `toString`, и поля в выводе могут не совпадать с полями сравнения (`toString` одинаковый, при этом сравнение идет по другим полям и вылетает ошибка) +- 7: Если нашему приложению `Meal.user` не требуется, не следует включать его в тесты. В следующем уроке мы потренируемся разными способами доставать зависимости `Meal.user` и `User.meals` + +-------------------------------- +## [Выпускной проект](../doc/graduation.md) +Новая информация плохо оседает в голове, когда дается в виде патчей. Поэтому, чтобы она стала "твоей", нужно еще раз проделать это самостоятельно. Домашнее задание на этом уроке небольшое, а полученных знаний уже достаточно для начала работы над выпускным проектом, основанным на нашем стеке. +Выпускной проект делайте параллельно с нашим: прошли тему занятия - сделали ее в выпускном. Не следует забегать вперед, но и не отставайте! +- Для проекта я взял реальное тестовое задание, поэтому жалоб на неясность формулировок принимать не буду. Сделайте как поняли. Представьте, что это **ваше тестовое задание на работу** +- Общение по выпускному в канале Slack *#graduation* +- **Обязательно проверяйся [по рекомендациям в конце выпускного](../doc/graduation.md#-Рекомендации)** +- По завершению вы сможете занести этот проект в свое портфолио и резюме как собственный без всяких оговорок + +### Успехов в выполнении! diff --git a/lessons/lesson05.md b/lessons/lesson05.md new file mode 100644 index 00000000000..70453a3a97b --- /dev/null +++ b/lessons/lesson05.md @@ -0,0 +1,214 @@ +# Стажировка Topjava +## Материалы занятия + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. [Обзор JDK 9/11. Миграция Topjava с 1.8 на JDK 17+](http://javaops.ru/view/resources/jdk8+) +### [OpenJDK JDK 21](https://jdk.java.net/21/). Обновите IDEA на 2023.x и Tomcat на 9.x +> - Перевел проект на JDK 21. Для запуска Maven или Tomcat переопредели переменную окружения `JAVA_HOME` и переменную `path` на `JAVA_HOME\bin`, чтобы `java -version` тоже было 21. Напомню, что IDEA это java процесс. Чтобы новые переменные окружения в ней увиделись, требуется ее перегрузить. +> - Можно работать на любой версии 17+, поменяв `21` в `pom.xml` + +- [API, ради которых наконец-то стоит обновиться с Java 8 (1)](https://habr.com/ru/post/485750) +- [API, ради которых наконец-то стоит обновиться с Java 8 (2)](https://habr.com/ru/post/487636) +- [Руководство по возможностям Java версий 8-19](https://habr.com/ru/post/719744/) + +#### Apply 5_1_jdk_21.patch +- Сделал создание коллекций через фабричные методы `List.of` +- Как пример в `InMemoryMealRepository` использовал *local variable type inference* `var`. + - [26 рекомендаций по использованию типа var в Java](https://habr.com/ru/post/438206/) +- `switch` в `MealServlet` перевел на новый формат (IDEA сама переводит по Alt+Enter). + - [Новые switch выражения](https://habr.com/ru/post/443464/) +- В `JdbcUserRepository.save` использовал [Text Blocks](https://www.infoq.com/articles/java-text-blocks/) +- В JDK 17 вместо `.collect(Collectors.toList())` можно использовать `.toList()`. Посмотрите его реализацию. + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW4 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. [HW4: Meal / JpaMealRepository](https://drive.google.com/file/d/13JJRhLhkn8_C3-xpkyilRxGe_w9_xTF2) +#### Apply 5_2_HW4.patch + - При сравнении еды тесты падают, т.к. Hibernate делает ленивую обертку к `user`, и если происходит обращение к любому его полю (кроме id) вне транзакции, бросается `LazyInitializationException`. +По логике приложения поле `user` в еде не нужно, и мы не будем его отдавать наружу UI. Более того, включать `user` в запрос будет ошибкой: мы запрашиваем данные, которые приложению не требуются. +В тестах исключаем `user` из сравнения. + - [SQL “between” not inclusive](https://stackoverflow.com/questions/16347649/sql-between-not-inclusive/16347680) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. [Hibernate issue / HW4 Optional](https://drive.google.com/file/d/1kbb2IO15L9ABJ0-2TFJm8XZU_QUXUdni) + +#### Apply 5_3_fix_hibernate_issue.patch + - Из-за [Hibernate bug with proxy initialization when using `AccessType.FIELD`](https://hibernate.atlassian.net/browse/HHH-3718) +в `JpaMealRepository.get()` делался дополнительный запрос в базу для инициализации прокси `User`, и мы делали хак: доступ к полю `AbstractBaseEntity.id` через `AccessType.PROPERTY`. +С версии `5.2.13.Final` загрузка прокси при обращении к `id` управляется флагом `JPA_PROXY_COMPLIANCE` (по умолчанию запрос не делается) + - [Call to id getter initializes proxy when using AccessType( "field" ): HHH-3718](https://hibernate.atlassian.net/browse/HHH-3718) + - [According to JPA, a Proxy should be loaded even when accessing the identifier: HHH-12034](https://hibernate.atlassian.net/browse/HHH-12034) + - Which is better, field or property access? + - Поправил `equals()` с учетом Lazy-проксирования + - JPA hashCode()/equals() dilemma + - Hibernate Proxy Pitfalls + +------------------------ +> Переопределять `equals()/hashCode()` необходимо, если +> - использовать entity в `Set` (рекомендовано для Many-ассоциаций) либо как ключи в `HashMap` +> - использовать _reattachment of detached instances_ (т.е. манипулировать одним Entity в нескольких транзакциях/сессиях). + +> [Implementing equals() and hashCode()](https://docs.jboss.org/hibernate/stable/core.old/reference/en/html/persistent-classes-equalshashcode.html) + +> Оптимально использовать уникальные бизнес-поля, но обычно таких нет, и чаще всего используются PK с ограничением, что он может быть `null` у новых объектов, и нельзя объекты сравнивать через `equals() and hashCode()` в бизнес-логике (например, тестах). + +> [equals() and hashcode() when using JPA and Hibernate](https://stackoverflow.com/questions/1638723) + +------------------------ + +> ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Почему над `AbstractBaseEntity` стоит `@Access(AccessType.FIELD)` ? Почему при запросе `user.id` нам не нужно вытаскивать его из базы? + +`AccessType.FIELD` делает доступ в `AbstractBaseEntity` и всех классах-наследниках по полям. При загрузке `Meal` Hibernate на основе поля `meal.user_id` делает ленивую прокси к `User`, у которой нет ничего, кроме id. + +#### Apply 5_4_HW4_optional.patch +- Stopwatch +- [Logback layouts coloring](https://logback.qos.ch/manual/layouts.html#coloring) +- Дополнительно: [use colored output only when logging to a real terminal](https://stackoverflow.com/questions/31046748) + +## Занятие 5: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Транзакции +- wiki Транзакция +- readOnly и Propagation.SUPPORTS +- [@Transactional In-Depth](https://www.marcobehler.com/guides/spring-transaction-management-transactional-in-depth) +- Ресурсы: + - [Транзакции в Spring Framework](https://medium.com/@kirill.sereda/%D1%82%D1%80%D0%B0%D0%BD%D0%B7%D0%B0%D0%BA%D1%86%D0%B8%D0%B8-%D0%B2-spring-framework-a7ec509df6d2) + - How does Spring @Transactional Really Work + - Стратегии работы с транзакциями: распространенные ошибки + - Spring @Transactional - isolation, propagation + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Профили Maven и Spring +#### Apply 5_5_profiles_connection_pool.patch +> - `SLF4JBridgeHandler` перенес в профиль `postgres` (если логировать драйвер не нужно, то и он не нужен) +> - **Галочка в XML-профиле влияет только на отображение в IDEA и никак не влияет на выполнение кода.** +> - `Profiles.ACTIVE_DB` задает активный профиль базы (postgres/hsqldb) +> - `Profiles.REPOSITORY_IMPLEMENTATION` определяет реализацию репозитория при запуске приложения (для тестов задаются через `@ActiveProfiles`). + +> Для переключения на HSQLDB необходимо: +> - поменять в окне Maven Projects профиль (Profiles) - выключить `postgres`, включить `hsqldb` - и сделать `Reimport All Maven Projects` (1-я кнопка) +> - поменять `Profiles.ACTIVE_DB = HSQLDB` +> - почистить проект `mvn clean` (фаза `clean` не выполняется автоматически, чтобы каждый раз не перекомпилировать весь проект) + +Для корректного отображения неактивного профиля в IDEA проверьте флаг _Inactive profile highlighting_ и сделайте проекту clean + +![image](https://cloud.githubusercontent.com/assets/13649199/25120020/29935958-2425-11e7-8363-1ff027426f64.png) + +> Вопрос: почему после этого патча не поднимается Spring при запуске приложения в Tomcat? (будем чинить в ДЗ, п.6) + +- Using Spring Profiles in XML Config +- Spring Profiles example + +### Автоматический выбор профиля базы: [`ActiveProfilesResolver`](http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver) +#### Apply 5_6_profile_resolver.patch +> Сделал автоматический выбор профиля базы при запуске приложения (тестов) в зависимости от присутствия драйвера базы в classpath (`ActiveDbProfileResolver`) +> В профили аттрибута `@ActiveProfiles(profiles=..)` добавляем `Profiles.getActiveDbProfile()` + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Пул коннектов +![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) [Александр Колесников - JDBC Pools Battle](https://www.youtube.com/watch?v=J9GzE2qlNuM&feature=youtu.be&t=2895) (ссылка на выводы) + +> [BoneCP to be deprecated ](https://stackoverflow.com/a/1662916/548473) +- Выбор реализации пула коннектов: Commons Database Connection Pooling, HikariCP +- Самый быстрый пул соединений на java (читаем комменты) +- Tomcat pool + + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Spring Data JPA + +#### Apply 5_7_spring_data_jpa.patch +> - Переименовал классы _Proxy_ на более адекватные _Crud_, убрал _Impl_ +> - В `spring-framework-bom` мы уже задали версию Spring. Убрал из остальных зависимостей. +> - В spring-data-jpa 2.x поменялся интерфейс: `T CrudRepository.findOne(ID id)` -> `Optional CrudRepository findById(ID id)` +> - [Java Optional — Отец холиваров](http://sboychenko.ru/java-optional) +> - [Optonal от Oracle](https://stuartmarks.files.wordpress.com/2016/09/optionalmotherofallbikesheds3.pdf) +> - [Java 8 Optional In Depth](https://www.mkyong.com/java8/java-8-optional-in-depth/) +> - Не стал переопределять в `CrudUserRepository` методы `JpaRepository` (для явного указания всех используемых методов). Обычно этого не делают. + +### Внимание: при обновлении версий не забудьте обновить зависимости Maven и сделать `clean`. + +- Spring Data JPA +- Замена AbstractDAO: JPA Repositories +- Разрешение зависимостей: Maven BOM [Bill Of Materials] Dependency +- Делегирование (в конце статьи) +- Getting started with Spring Data JPA +- Query methods +- Spring Data – новый взгляд на persistence (JeeConf) +- Евгений Борисов — Spring Data? Да, та! +- Ресурсы: + - Github repositories + - Spring Data JPA Tutorial + - [Spring Data JPA with QueryDSL](https://dontpanic.42.nl/2011/06/spring-data-jpa-with-querydsl.html) + - [SpEL support in Spring Data JPA @Query](https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions) + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> Какой паттерн проектирования применён в классе DataJpaUserRepository (декоратор/адаптер/прокси/другой)?: + +Вопрос интересный:) Ближе всего к адаптеру, но по логике композиция с делегированием. Мы просто используем для нашей реализации возможности `data-jpa: CrudUserRepository`. +Делегат интерфейсов не меняет, а прокси похож на делегата, но служит для неявной подмены (часто прямо в рантайм). См. [ПАТТЕРНЫ +ПРОЕКТИРОВАНИЯ](https://refactoring.guru/ru/design-patterns) + +> В spring-petclinic `DataJpa` реализована без дополнительных классов. В таком виде как у них, spring data смотрится, конечно, намного лаконичней других реализаций, но у нас получилось вдвое больше кода, чем с тем же jpa или jdbc. Плюс только пожалуй в том, что query находятся прямо в репозитории, а не где-то там в другом пакете. Так что получается, spring data лучше подходит для простейших crud без всяких "фишек"? или в чем его достоинство для больших и сложных проектов? + +Достоинства DATA-JPA по сравнению, например, с JPA: есть типизация, готовые реализации типовых методов CRUD, а также paging, data-common. Мы можем переключить реализацию JPA, например, на mongoDb (`PagingAndSortingRepository`, от которого наследуется `JpaRepository`, находится в `spring-data-common`). +Соответственно, его методы будут поддерживаться всеми реализациями `spring-data-common` (JPA - одна из них) и пр. Подробнее о них есть в видео Spring Data – новый взгляд на persistence. +Дополнительное проксирование в DATA-JPA - моя "фишка" для устранения минусов этого фреймворка: невозможность дебага, привязка к интерфейсу JpaRepository, перенос логики Repository в слой сервисов. +Для большого приложения выигрыш этого стоит. Для небольших (тестовых) приложений (например выпускного) дополнительных классов делать не нужно. + +> Почему мы для InMemory не сделали отдельного профиля? Почему их не удалить вообще? + +Реализация InMemory является примером, как в test делать подмену контекста. Для них сделали отдельный `inmemory.xml`, и запускаемый проект ничего не должен о них знать. У нас учебный проект, в котором 4 реализации репозиториев, в реальном такого не будет. + +> А как делать транзакционность для реализации jdbc? + +Будем делать на следующем уроке. + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. [Spring кэш](https://drive.google.com/file/d/1xpuL2YscL1ounS_qFRb1qzLciEOKn9I4/view?usp=sharing) +#### Apply 5_8_spring_cache.patch +- [Wiki Кэш](https://ru.wikipedia.org/wiki/Кэш) + - [Spring Cache Abstraction](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache) + - [ECache](https://www.ehcache.org) + - [Configuring the Cache Storage](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache-store-configuration) +- [Spring 3.1 новый механизм кеширования](https://russianblogs.com/article/75981527090/) +- [Spring 4+ with Ehcache 3 – how to](https://imhoratiu.wordpress.com/2017/01/26/spring-4-with-ehcache-3-how-to/) +- [Evict Ehcache elements programmatically, using Spring](https://stackoverflow.com/questions/29557959/evict-ehcache-elements-programmatically-using-spring) +- Дополнительно: [Оптимизация запросов с использованием Spring cache и Bean scope](https://www.youtube.com/watch?v=P7nCQepVk_Y&list=PL6rimDLSyfe947bGsCviqIIYGofeFYQTM) +-------------------- + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW05 + +- 1: Имплементировать `DataJpaMealRepository` и протестировать через `MealServiceTest`. +#### MealServlet не работает (почему?), чиним в _Optional п.5_ +- 2: Разделить реализации Repository по профилям Spring: `jdbc`, `jpa`, `datajpa` (общее в профилях можно объединять, например, ``). + - 2.1: Профили выбора DB (`postgres/hsqldb`) и реализации репозитория (`jdbc/datajpa/jpa`) независимы друг от друга, и при запуске приложения (тестов) нужно задать тот, и другой. + - 2.2: Удобно для интеграции с IDEA выставить в `spring-db.xml` справа вверху в `Change Profiles...` профили, например, `datajpa, postgres`. **Это влияет ТОЛЬКО на отображение в IDEA и НИКАК на работу приложения и тестов** + - 2.3: Общие части для всех в `spring-db.xml` можно оставить как есть без профилей вверху файла **(до первого `Java Persistence/OneToMany + +--------------------- +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Типичные ошибки и подсказки по реализации +- 1: Для того, чтобы не запускались родительские классы тестов, нужно сделать их `abstract`. +- 2: В реализациях `JdbcMealRepository` **код не должен дублироваться**. Если вы возвращаете тип `Object`, посмотрите в сторону дженериков. +- 3: В `MealServlet/SpringMain` в момент `setActiveProfiles` контекст спринга еще не должен быть инициализирован, иначе выставление профиля уже ни на что не повлияет. +Уметь пользоваться гугл для разработчика, это умение №1. Если застряли- попробуйте например слова: `spring context set profile` +- 4: Если у метода нет реализации, то стандартно бросается `UnsupportedOperationException`. Для уменьшения количества кода при реализации _Optional_ (п. 7, только `DataJpa`) попробуйте сделать `default` метод в интерфейсе. +- 5: В Data-Jpa метод для ссылки на entity (аналог `em.getReference`) - `T getReferenceById(ID id)` +- 6: Проверьте, что в `DataJpaMealRepository` все обращения к DB для одной операции выполняются в **одной транзакции**. +(`` для логирования информации по транзакциям) +- 7: Для 7.1 `достать по id пользователя вместе с его едой` - в `User` добавить `List meals`. Учесть, что у юзера может отсутствовать еда. [Ordering a join fetched collection in JPA using JPQL/HQL](http://stackoverflow.com/questions/5903774/ordering-a-join-fetched-collection-in-jpa-using-jpql-hql) +- 8: Проверьте, что все тесты запускаются из Maven (имена классов тестов удовлетворяют соглашению) и итоги тестов класса выводятся корректно (не копятся). По умолчанию [maven-surefire-plugin](http://maven.apache.org/surefire/maven-surefire-plugin/examples/inclusion-exclusion.html) включает в тесты классы, заканчивающиеся на Test. +- 9: В тестах можно вынести общие `@ActiveProfiles` наверх, в разных классах они суммируются +- 10: `Topjava + +## Материалы занятия + +### ![correction](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Правки в проекте + +#### Apply 6_0_update_version.patch + +- После перехода на JDK 21 обновил версии логирования и плагина тестирования + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW5 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. [HW5: Spring Profiles. Spring Data JPA](https://drive.google.com/file/d/1dlhXeQr0fi0XymEFyBG-TXv5hpPgXtlT) + +
+ Краткое содержание + +Перед просмотром разбора домашнего задания еще раз ознакомьтесь с материалом по ссылке: + +- readOnly и Propagation.SUPPORTS (без транзакции не будет оптимизации по флагу `readOnly` при выполнении JDBC и в управлении ресурсами Spring's JPA, в том числе выключение `flush`) + +В нашем приложении над репозиториями **CrudMealRepository** и **CrudUserRepository** указана аннотация +**@Transactional(readOnly = true)**, что предполагает выполнение всех операций в репозитории внутри транзакции на +чтение. В таком случае, для операций, которые вносят изменения в базу данных, нам необходимо переопределить транзакцию и указать над модифицирующим методом аннотации: +> @Transactional +> @Modifying + +В реализации **SimpleJpaRepository**, которая используется Spring Data JPA, транзакционность описывается точно +так же, как и в нашем приложении (все операции выполняются в транзакции на чтение, а для модифицирующих методов +транзакции переопределяются). + +#### Для реализации репозитория для работы с Meal через Spring Data JPA: + +1. Создаем интерфейс `CrudMealRepository`, который должен быть унаследован от `JpaRepository` +2. Этот интерфейс помечаем аннотацией `@Transactional(readonly = true)` +3. Основная функциональность репозитория будет реализована прямо в интерфейсе (такой подход называется *интерфейсное + программирование*) - Spring Data JPA автоматически создаст прокси для нашего интерфейса с реализацией необходимой + функциональности. +4. По аналогии с `DataJpaUserRepository` создаем класс `DataJpaMealRepository`, который имплементируют `MealRepository`. В + этот класс через конструктор спрингом будут внедрены `crudMealRepository` и `crudUserRepository`. Все методы + реализуются по аналогии с `DataJpaUserRepository`. Для сохранения еды в методе `save(Meal mea, int userId)` сначала + проверяем, что еда принадлежит аутентифицированному пользователю и после этого присваиваем еде ссылку на `User`, + которому она принадлежит. Чтобы не загружать этого user из базы, используем метод `getById(int id)`, который вернет + ссылку на прокси-объект user с указанным `id`. + +#### Разделение конфигурации на профили + +Spring позволяет настроить несколько профилей, которые позволяют переключать его функциональность. +У нас уже настроено два профиля для переключения между базами данных: + +- `postgres` - для работы PostgreSQl +- `hsqldb` - для работы с базой данных в памяти + +Для реализаций способа работы с базой данных настроим еще три профиля: + +- `jdbc` +- `jpa` +- `dataJpa` + +Общие для нескольких профилей свойства можно выносить в общий блок, перечислив в декларации профилей их наименования +через запятую. Следует обратить особое внимание на то, что тег `` в котором объявляются профили и их +конфигурация, может располагаться только в самом конце файла конфигурации после всех остальных настроек. Intellij Idea +предоставляет интерфейс для переключения между профилями Spring, настроенными в конфигурации (такое переключение влияет +только на отображение файла конфигурации, но не оказывает никакого влияния на запуск и работу приложения). + +#### Тесты для всех реализаций репозитория + +Все настройки логирования и определения времени выполнения тестов вынесем в общий абстрактный базовый класс +`AbstractServiceTest`, от которого будут унаследованы остальные тестовые классы сервисов. Также над этим суперклассом +указывается аннотация `@ActiveProfiles(resolver = ActiveDbProfileResolver.class)`, которая будет автоматически +определять необходимый профиль базы данных при запуске тестов в зависимости от наличия драйвера БД в classpath. +Его наследниками будут классы `AbstractMealServiceTest` и `AbstractUserServiceTest`, в которые мы переносим +соответствующие тесты. +Далее по цепочке наследования создадим тестовые классы для различных реализаций репозитория. Аннотация `@ActiveProfile` +наследуется и может быть дополнена в классах — наследниках, поэтому в созданных тестовых классах `(Jpa | DataJpa | +Jdbc) .. ServiceTest` с помощью этих аннотаций укажем конкретные профили Spring, соответствующие тестируемой +функциональности. +> При изменении профиля maven и изменении иерархии классов, удалении классов — обязательно нужно запускать `mvn clean` + +
+ +#### Apply 6_01_HW5_data_jpa.patch + +Транзакция начинается, когда встречается первая `@Transactional`. С default propagation `REQUIRED` +остальные `@Transactional` просто участвуют в первой. Поэтому ставим аннотацию сверху `DataJpaMealRepository.save()`, +чтобы все обращения к базе внутри метода были в одной транзакции. Аналогично, если из сервиса собирается несколько +запросов к репозиториям, `@Transactional` ставится над методом сервиса. + +#### Apply 6_02_HW5_profile_test.patch + +**Для IDEA в `spring-db.xml` не забудьте выставить Spring Profiles. Например `datajpa, postgres`** + +> - Заменил `description.getMethodName()` на `getDisplayName()` в выводе результатов тестов. После `printResult()` буфер сбрасывается в 0, чтобы не накапливать изменения. + +#### Apply 6_03_extract_rules.patch + +> Вынес измерение времени и сводку в отдельный класс `TimingRules` + +[JUnit Rules External Resources](https://carlosbecker.com/posts/junit-rules/#external-resources) + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. [HW5: Optional](https://drive.google.com/file/d/1S6gOOzLV9ndSbPiSuyEmqhEy3xZoUpXZ) + +
+ Краткое содержание + +#### Определим профиль Spring в SpringMain: + +При создании `XmlApplicationContext`, когда мы в конструктор передаем настройки в xml - Spring сразу считывает и +применяет конфигурации и дальнейшее изменение профилей не оказывает на контекст никакого влияния, поэтому в данном +классе сначала создаем `GenericXmlApplicationContext` без параметров, после чего указываем для него необходимые профили с +помощью + + ```java + appCtx.getEnviroment().setActiveProfiles(Profiles.POSTGRES,Profiles.DATAJPA) + ``` + +Затем передаем контексту наши конфигурации и обновляем его. В данном случае Spring считает конфигурации, и настроит +контекст в соответствии с указанными профилями. + +#### Для корректного запуска приложения определим профили Spring в MealServlet: + +Профиль Spring можно задать аналогичным способом. Также, вместо `GenericXmlApplicationContext` можно создать +`ClassPathXmlApplicationContext`. Самый короткий способ задать профили — создать `ClassPathXmlApplicationContext`, передав +ему в конструкторе конфигурации и установив флаг конструктора `refresh` в `false`. В данном случае спринг не будет +парсить контекст сразу при инициализации и мы можем задать для него требуемые профили и вручную обновить контекст, после +чего спринг поднимет и настроит контекст соответствующим образом: + + ```java + springContext=new ClassPathXmlApplicationContext(new String[]{"spring/spring-app.xml","spring/spring-db.xml"}, false); + ``` + +Так как HSQLDB (в данной версии) не работает с Java Time API, в отличие от PostgreSQL, необходимо разделить реализации репозитория для +работы в двух различных профилях БД. Для данных профилей будет различаться только способ получения даты и времени +употребления Meal. В соответствии с паттерном Template Method - создадим классы-наследники, где для +соответствующих профилей будет реализовываться абстрактный `protected` метод суперкласса, который будет возвращать +`LocalDateTime` - для PostgreSQL и `TimeStamp` для HSQLDB. Базовый класс `JdbcMealRepository` будет дженериком (типизированным классом), +объект которого возвращается этим `protected` методом. Классы-наследники являются внутренними +статическими классами, они помечены аннотациями `@Repository` и являтются бинами спринга. В `@Profile` над ними мы указываем +соответствующий профиль спринга в зависимости от БД - Spring создаст бин только для тех классов, которые будут относиться к активным профилям. +> последние версии драйвера HSQLDB поддерживает работу с Java Date Time Api, поэтому теперь можно просто обновить версию драйвера в `pom.xml` + +#### Получение Meal вместе с User + +1. В `MealService` внедряем `UserRepository` и в одной транзакции сначала + загружаем из БД `Meal`, затем загружаем соответствующего `User` и вручную устанавливаем для еды ее владельца. Так как + `@Transactional` объявлена на уровне сервиса — несмотря на то, что мы работаем с двумя разными репозиториями, операции + по получению из базы Meal и User выполнятся в одной транзакции благодаря тому, что аннотация имеет атрибут + `propagation`, который по умолчанию устанавливается как `required` - то есть, если внутри одной транзакции будет + исполняться другой транзакционный метод, то для его выполнения не будет открываться новая транзакция — он будет + выполняться во внешней транзакции. + +2. Способ: получение Meal с User в `MealRepository` через `FETCH JOIN`. Так как нам требуется реализовать получение еды с ее + владельцем только для `DataJpaMealRepository`, в интерфейсе `MealRepository` объявим `default UserMeal getWithUser(...)`, + который для всех реализаций этого интерфейса при вызове этого метода будет выбрасывать исключение + `UnsupportedOperationException()`. В `DataJpaMealRepository` переопределим этот дефолтный метод: этот метод пометим + аннотацией `@Query` и в скобках укажем HQL запрос получения Meal из базы. При этом, так как запрос содержит `JOIN + FETCH`, таблицы meals и users будут объединяться на уровне базы (обычный JOIN) и Spring Data за один запрос загрузит + из базы Meal вместе с ее User. + +#### Получение User вместе с Meals + +Для того чтобы получить из базы User вместе со всеми его Meals, для начала добавим `List meals` в класс `User`. +Эта коллекция будет помечена аннотацией `@OneToMany`, которая говорит об отношении один-ко-многим. Мы видим, что +у Meal есть ссылка на User, а у User есть ссылка на коллекцию его meal, и эти ссылки помечены соответствующими +аннотациями — такое отношение называется BiDirectional. Пользователя с его едой получаем способом, аналогичным +предыдущему шагу - с помощью HQL запроса с применением fetch join. JPA 2.0 позволяет использовать в таких случаях +UniDirectional отношение со стороны +@OneToMany [Unidirectional OneToMany](https://en.wikibooks.org/wiki/Java_Persistence/OneToMany#Unidirectional_OneToMany.2C_No_Inverse_ManyToOne.2C_No_Join_Table_.28JPA_2.x_ONLY.29). +Обратите внимание на то, что нужно **объявлять поле только в том случае, когда ваше приложение их использует!** + + +#### Проблема **N+1** + +Проблема **N + 1** возникает, когда выполняется N дополнительных SQL-запросов для получения тех же данных, которые можно +получить при выполнении одного SQL-запроса. Чем больше значение N, тем больше запросов будет выполнено и тем больше +влияние на производительность. Например, в нашем случае при получении списка пользователей сначала будут загружены все +пользователи, и затем для каждого пользователя будет выполнен запрос в базу для получения его ролей. Таким образом, если +бы у нас было 1000 пользователей, то будет выполнен 1 запрос для получения всех пользователей и дополнительно 1000 +запросов для получения ролей каждого пользователя, итого 1000 + 1 запрос. +> Некоторые способы решения проблемы N+1: +> 1. Получать сущность из базы данных через запрос с использованием `JOIN FETCH` - в таком случае таблицы будут объединяться на уровне БД и вся информация будет получена сразу. +> 2. Пометить коллекцию сущности, для получения которой выполняются дополнительные запросы, аннотацией `@Fetch(FetchMode.SUBSELECT)`. В этом случае + > сначала будет загружена коллекция пользователей и затем будет выполнен один дополнительный запрос, который получит роли всех этих пользователей. +> 3. Пометить коллекцию аннотацией `BatchSize(size = 200)` - в данном случае сначала будет выполнен запрос на получение всех пользователей, и затем дополнительными запросами будут загружаться + > роли этих пользователей — по 200 пользователей за запрос. +> 4. JPA 2.1 предоставляет для решения этой проблемы функциональность `@EntityGraph` - на уровне сущности мы можем объявить граф полей, которые будут загружаться + > из базы данных совместно с сущностью. При определении последующих запросов мы можем указать наименование нужного нам Entity Graph, и JPA сформирует запрос в соответствии с этим графом и загрузит только описанные в нем поля. + `@EntityGraph` можно определять не только на уровне сущности, это можно сделать непосредственно в интерфейсе через параметр аннотации `attributePath`, например: `@EntityGraph(attributePaths = {"meals", "roles"})` +> +
+ +#### Apply 6_04_HW5_optional_fix_jdbc_profiles.patch + +- Spring Profiles. Spring 4 Conditional. +- дополнительно: + - зайдите в исходники `@Profile` и посмотрите (подебажьте) его реализацию через `@Conditional(ProfileCondition.class)`. + - [реализация через Java Config и Profiles на уровне методов](http://stackoverflow.com/a/43645463/548473) + +#### Apply 6_05_update_hsqldb.patch + +В реальном проекте часто проблему можно решить простым обновлением версии: new HSQLDB version supports Java 8 time API + +#### Apply 6_06_HW5_optional_fetch_join.patch + +> - Добавил проверки и тесты на `NotFound` для `MealService.getWithUser` и `UserService.getWithMeals` +> - Убрал `CascadeType.REMOVE`, в уроке далее будет про Cascade. + +- JPA JoinColumn vs mappedBy +- Unidirectional OneToMany + +#### Apply 6_07_HW5_graph_batch_size.patch +- **N+1 selects issue** + +- Using Named Entity Graphs + - [Entity Graph в Spring Data JPA](https://sysout.ru/entity-graph-v-spring-data-jpa/) + - [`EntityGraphType.FETCH` vs `LOAD`](http://stackoverflow.com/questions/31978011/what-is-the-diffenece-between-fetch-and-load-for-entity-graph-of-jpa) +- Стратегии загрузки коллекций в JPA +- Стратегии загрузки коллекций в Hibernate + +> Когда мы достаем всех юзеров с ролями без `@BatchSize`, делается запрос юзеров (1), и на каждого юзера идет в базу запрос ролей (+N). C `@BatchSize(size = 200)` делается запрос на юзеров (1), и затем роли достаются пачками для 200 юзеров (+ N/200). + +## Занятие 6: + +### Добавил тесты на валидацию + +> - К сожалению, в JUnit нет `ExpectedException.expectRootCause`. `AbstractServiceTest.validateRootCause()` сделал через [JUnit 4.13 assertThrows](https://stackoverflow.com/a/2935935/548473). + +> ![](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Откуда у нас берется `ConstraintViolationException` в тестах на валидацию? Для каких наших исключений он является рутом? + +Прежде всего - пользуйтесь дебагом! Исключение легко увидеть в методе `getRootCause()`. Если подебажить выполение +Hibernate валидации, то можно найти, где обрабатываются аннотации валидации и место +в `org.hibernate.cfg.beanvalidation.BeanValidationEventListener.validate()`, где +бросается `ConstraintViolationException`. +Cамое простое - поставить брекпойнт в конструкторах `ConstraintViolationException` или в `ValidationException` и +запустить тест `createWithException` в дебаге. + +#### Apply 6_08_add_test_validation.patch + +**Тесты валидации для Jdbc не работают, нужно будет починить в HW6 (в реализация Jdbc валидация отсутствует, сделал `@Ignore`)** + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Кэш Hibernate + +> Кэш мигрировал на 3.x + +
+ Краткое содержание + +#### Уровни кэширования Hibernate + +Hibernate cache — это 3 уровня кеширования: + +- Кеш первого уровня (First-level cache) - включен по умолчанию всегда, его нельзя отключить, кэширует сущности на + уровне сессии; +- Кеш второго уровня (Second-level cache) - используется на уровне фабрики сессий, по умолчанию всегда отключен и не + имеет реализации в Hibernate, для его использования нужно самостоятельно подключать сторонние реализации; +- Кеш запросов (Query cache) - по умолчанию отключен, включается определением дополнительных параметров в + конфигурационном файле. В нем кэшируются идентификаторы объектов, которые соответствуют совокупности параметров + совершенного ранее запроса; + +Для кэширования определяются 4 стратегии, которые определяют его поведение в определенных ситуациях: + +- Read-only +- Read-write +- Nonstrict-read-write +- Transactional + +#### Подключение кэш 2 уровня Hibernate + +Существует множество сторонних реализаций кэша, которые можно подключить к Hibernate. Мы будем использовать одну из +самых распространенных - EhCache. Для того чтобы подключить к Hibernate EhCache, в файл `pom.xml` нужно добавить +дополнительную зависимость: + +```` + + org.hibernate + hibernate-jcache + ${hibernate.version} + +```` +Теперь кэш подключен и его осталось лишь сконфигурировать в файле spring-db.xml для профилей +"datajpa, jpa" : +```` + + + + + + + + + + +```` +Сам кэш более тонко настраивается в отдельном файле `ehcache.xml`. В нем мы указываем какие +таблицы будут кэшироваться, количество элементов, время и множество других параметров. + +Чтобы указать Hibernate какие сущности будут кэшироваться, их нужно пометить аннотацией `@Cache` и в скобках +указать необходимую стратегию кэширования. Такая аннотация предоставляется как JPA, так и Hibernate, аннотация +Hibernate позволяет определять дополнительные параметры кэширования. +Так как мы пометили User `@Cache`, то сущности будут заноситься в кэш второго уровня, но не будет кэшироваться коллекция +ролей. Чтобы роли тоже кэшировались нужно так же пометить это свойство пользователя аннотацией `@Cache`. +Теперь, при запуске тестов мы столкнемся с частой проблемой — так как перед каждым тестом мы повторно заполняем базу +тестовыми данными, и делаем мы это в обход Hibernate - содержимое кэша 2 уровня и базы данных будут различаться. +Поэтому перед каждым тестом дополнительно нужно кэш инвалидировать — специально для этого мы создадим +утильный класс `JpaUtil`, где определим метод, который будет получать текущую Session Factory и инвалидировать кэш Hibernate. +Объект этого класса внедрили в тесты, и, так как в `spring-db.xml` мы определили, что этот бин будет создаваться только для профилей *jpa, datajpa*, +тесты для JDBC реализации перестанут работать. `JpaUtil` отсутствует в профиле `jdbc` и не может быть разрезолвен при поднятии Spring контекста. +
+ +#### Apply 6_09_hibernate_cache.patch + +**Теперь уже все `JdbcUserServiceTest` тесты поломались (в профиле `jdbc` отсутствует `JpaUtil`, сделал `@Ignore`). Требуется починить в HW6** + +- Уровни кэширования Hibernate +- Hibernate Cache. Практика +- Hibernate - Caching +- Починка тестов: инвалидация кэша Hibernate +- [Hibernate User Guide: Caching](http://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#caching) +- [Hibernate 5, Ehcache 3.x](https://www.boraji.com/index.php/hibernate-5-jcache-ehcache-3-configuration-example) +- Ресурсы: + - **Hibernate performance tuning (Mikalai Alimenkou /Igor Dmitriev)** + - JPA2 @Cacheable vs Hibernate @Cache + - How does Hibernate Query Cache work + - Pitfalls of the Hibernate Second-Level / Query Caches + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Cascade. Auto generate DDL. + +
+Краткое содержание +При создании таблиц `user_roles` и `meals` для внешнего ключа мы указывали свойство `ON DELETE CASCADE`. Это означает, что при удалении +пользователя база данных автоматически будет удалять все записи, которые на него ссылались по внешнему ключу. Существует также +свойство `ON UPDATE CASCADE`, определив которое, при обновлении первичного ключа пользователя этот ключ обновлялся бы во всех зависимых +таблицах. При каскадных операциях на уровне базы данных могут возникнуть проблемы с консистентностью кэша второго уровня, так как все +операции производятся в обход Hibernate. +Hibernate тоже позволяет указывать `CascadeType` для управляемых сущностей (что не имеет абсолютно никакой связи с таким же свойством в базе данных). + +> Для Hibernate допускается указывать `CascadeType` только для сущности @OneToMany - со стороны родителя! +> Для этой цели со стороны дочерней сущности можно указать аннотацию @OnDelete над ссылкой на родительскую сущность. +> Для коллекций элементов сущности все действия всегда распространяются каскадно! + +Действие, указанное в `CascadeType` при манипулировании сущностью — будут распространяться на все ее дочерние объекты. + +> ALL - все возможные каскадные операции, выполняемые над исходной сущностью, применяются к дочерним сущностям. +> MERGE - если исходная сущность объединяется, слияние каскадно передается на дочерние сущности. +> PERSIST - если исходный объект сохраняется, сохранятся каскадно и дочерние объекты. +> REFRESH - если исходная сущность обновляется, каскадно обновятся и дочерние объекты. +> DELETE - при удалении исходной сущности удаляются и дочерние объекты. + +Также для аннотации `@OneToMany` существует параметр `orphanRemoval`. Если установить этот параметр в true, то при удалении исходной сущности +все объекты, которые ранее на нее ссылались так же удалятся, если установить это значение в false, то в дочерних сущностях ссылка на +исходную будет просто обнуляться. + +#### Генерация схемы базы данных по Java Entity +JPA 2.1 предоставляет возможность генерировать базу данных по сущностям. Для этого в spring-db.xml +укажем следующие параметры: +``` + + + + + + + + + + + +``` +> Автоматическую генерацию не рекомендуется использовать для реального приложения, так как генерируемые +> команды часто некорректны. Чтобы они были более правильными — нужно указывать дополнительные условия и ограничения +> в аннотациях при описании entity. +
+ +#### Apply 6_10_cascade_ddl.patch + +#### Cascading + +> Есть SQL ON .. CASCADE, которая выполняется в базе данных, и есть аннотация в Hibernate, исполняемая в приложении + +- Do not use `CascadeType` for @ManyToOne +- CascadeType meaning +- No cascade option on an ElementCollection, the target objects are always persisted, merged, removed with their parent. +- Create ON DELETE CASCADE: `@OnDelete` +- [Сascade for `@ElementCollection`](https://stackoverflow.com/a/62848296/548473) +- Hibernate second level cache and ON DELETE CASCADE in database schema +- [`orphanRemoval=true` vs `CascadeType.REMOVE`](http://stackoverflow.com/a/19645397/548473) +- [JPA `cascade/orphanRemoval` doesn't work with `NamedQuery`](http://stackoverflow.com/questions/7825484/jpa-delete-where-does-not-delete-children-and-throws-an-exception) + +#### Auto schema generation +- JPA DATABASE SCHEMA GENERATION +- hbm2ddl.auto and autoincrement +- Hibernate/JPA DB Schema Generation Best Practices + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Spring Web + +
+ Краткое содержание +Для работы с web с помощью Spring подключим к проекту следующие зависимости: + +``` + + + org.apache.tomcat + tomcat-servlet-api + ${tomcat.version} + provided + + + + + javax.servlet + jstl + 1.2 + + + + + org.springframework + spring-web + ${spring.version} + +``` +При старте web-приложения в контейнере сервлетов требуется инициализировать контекст спринга. +Запустить Spring можно с помощью `ContextLoaderListener`, который будет отслеживать работу веб-приложения и при инициализации сервлета +поднимать Spring context в методе `contextInitialized` и отключать контекст спринга при остановке +приложения в методе `contextDestroyed`. +Для этого нужно определить этот ContextListener в `web.xml`: + +``` + + org.springframework.web.context.ContextLoaderListener + +``` +Чтобы Listener смог поднять контекст спринга — ему нужно указать в `web.xml` путь к конфигурационным файлам и +задать необходимые профили: + +``` + + spring.profiles.default + postgres,datajpa + + + + contextConfigLocation + + classpath:spring/spring-app.xml + classpath:spring/spring-db.xml + + +``` + +Для каждого сервлета, при инициализации, после создания запускается метод `init(ServletConfig config)`, где мы можем получить текущий контекст Spring. +Для web приложений определяется свой собственный `WebApplicationContext`, который может работать с сервлетами. +В `UserServlet` мы можем получить контекст с помощью метода `WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext())`. + Из полученного контекста мы можем получать бины Spring, например, объект `UserService`, и работать через него с пользователями. +
+ +#### Apply 6_11_spring_web.patch + +> - Для сборки проекта в окне Maven отключите тесты (`Toggele 'Skip Tests' Mode`) +> - В `web.xml` задаются профили запуска по умолчанию: `postgres,datajpa`. **Если запускаетесь под HSQLDB, надо поменять на `hsqldb,datajpa`**. + +- ServletContextListener. +- Servlet Lifecycle + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. JSP, JSTL, internationalization + +
+ Краткое содержание +Ко всему, что находится в папке webapp, можно получить доступ из браузера. +Для отображения пользователей создадим jsp страницу `userList`. Для работы со специализированными функциями и выражениями на +странице импортируем некоторые библиотеки jstl(java standard tag library). + +#### Локализация + - для локализации **стандартными средствами java** можно использовать *Bundle* - это набор файлов properties, +где определены ключ и значение. В зависимости от локали автоматически будет выбран нужный файл properties, из которого по + ключу страница будет получать текст на нужном языке и подставлять в места, где указаны соответствующие ключи. +```html + +``` + - Для локализации нашего приложения создадим в папке `resources/messages` два файла - `app.properties` и `app_ru.properties`, в которых +мы и пропишем ключи и соответствующие им значения на русском и английском языках. +При этом нужно иметь в виду, что локализация в jpa/jstl не работает с UTF8, поэтому для отображения текста на кириллице приходится записывать +его в виде набора кодов unicode (intellij idea предоставляет нам удобный функционал для работы с этими кодами). + +> **Это было до Java 9. Теперь можно не парится и писать напрямую в UTF-8** + + - На каждой странице будут дублироваться верхняя часть(header) и нижняя часть(footer), поэтому сделаем их в виде фрагментов, которые будут +включаться в каждую страницу с помощью тега + ```< jsp:include page="fragments/bodyHeader.jsp"/ > ``` + - Для того чтобы на странице JSP понимало, с каким объектом оно работает (а в IDEA работали автодополнения), мы можем явно указать с каким типом +объекта мы будем работать. Для этого мы используем тег: +```html + +``` +После этого на странице мы сможем работать с объектом в java-вставках *< % >* и с помощью expression language *${ }*. +Без этого тэга приложение работать тоже будет, не будет IDEA интергации. Не забывайте про getter-ы, JSP обращается к объектам через них! + +> Локаль приложения определяется на основе локали операционной системы и свойств браузера. Чтобы проверить работу +> локализации можно из браузера в заголовке запроса указать Content-Language:"en-US". Этот заголовок будет +> считан в сервлете, и приложение определит требуемую локаль. + +
+ +**Убедитесь, что [в настройках IDEA](https://github.com/JavaOPs/topjava/wiki/IDEA#Поставить-кодировку-utf-8) кодировка везде UTF-8 до применения патча** +#### Apply 6_12_jsp_jstl_i18n.patch + +> - Поменял `users/meals` в ключах локализации на `user/meal`. Понадобится при локализации ошибок (сделаем позже) +> - [Since Java 9 default encoding in properties files is UTF-8](https://docs.oracle.com/javase/9/intl/internationalization-enhancements-jdk-9.htm). Галочка `Transparent` и ASCII кода уже не нужны. + + +**Для работы с несколькими языками установите плагин `Resource Bundle Editor`** + +- Including Content in a JSP Page + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Динамическое изменение профиля при запуске. + +
+ Краткое содержание + +#### Динамическое изменение профиля при запуске +Изначально мы определили профиль спринга в web.xml: +```xml + + spring.profiles.default + postgres,datajpa + +``` +При этом приложение будет деплоиться на сервер с указанными в конфигурации профилями. +Чтобы определить профиль Spring при запуске без перекомпиляции проекта можно использовать системные опции, которые мы можем +задать при запуске проекта, указав их в параметрах запуска (в командной строке, а для Intellij Idea: Edit Run/Debug Configurations - VM options). +В этом случае конфигурации по умолчанию будут переопределены и будут использованы заданные при запуске системные переменные. + +
+ + -Dspring.profiles.active="postgres,datajpa" + +- Set profiles in Spring 3.1 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. Конфигурирование Tomcat через maven plugin. Jndi-lookup. + +
+ Краткое содержание +Многие настройки сервера(web-контейнера) можно вынести в отдельный файл-конфигурацию. +Настройки TomCat определим в отдельном файле `context.xml` + +#### Настройка пула TomCat для соединения с базой данных +```xml + +``` +Для того чтобы TomCat при запуске создавал пул коннектов, требуется добавить maven плагин в +секцию **buid** + +```xml + + + + org.codehaus.cargo + cargo-maven3-plugin + 1.9.5 + + + tomcat9x + + UTF-8 + + + tomcat,datajpa + + + + + + org.postgresql + postgresql + + + + + + + + + src/main/resources/tomcat/context.xml + conf/Catalina/localhost/ + ${project.build.finalName}.xml + + + + + + ru.javawebinar + topjava + war + + ${project.build.finalName} + + + + + +``` +В `spring-db.xml` создаем новый профиль "tomcat", для него будет создаваться бин `dataSource` с помощью соединения, +которое будет получено из пула коннектов TomCat по JNDI. При этом в профиле мы можем указать расположение файла свойств, +в котором будут описаны дополнительные параметры пула коннектов контейнера сервлетов - у нас это файл `tomcat.properties`. + +
+ +С плагином мы можем сконфигурировать Tomcat прямо в `pom.xml` и запустить его с задеплоенным туда нашим приложением WAR +из командной строки без IDEA и без инсталляции Tomcat. По умолчанию он скачивает его из центрального maven-репозитория ( +можно также указать свой в `${container.home}`). При запуске Tomcat из IDEA +запускается Tomcat, путь к которому мы прописали в конфигурации запуска (со своими настройками). + +#### Apply 6_13_tomcat_pool_jndi_cargo.patch + +> - для запуска в Tomcat 9 поменял `tomcat7-maven-plugin` на `cargo-maven3-plugin`. +> - в `pom.xml` вместо `context.xml.default` можно делать [индивидуальный контекст приложения](https://stackoverflow.com/a/60797999/548473) +> - Конфигурация сделана под postgres. Для HSQLDB нужно скорректировать `driverClassName` + `validationQuery="SELECT 1 FROM INFORMATION_SCHEMA.SYSTEM_USERS` в `context.xml` и `dependencies`. + +> ![](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Томкат сам управляет пулом коннектов? На каждый запрос в браузере будет даваться свой коннект? + +Да, в Томкате есть реализация пула коннектов `tomcat-jdbc` (мы его подключаем со `scope=provided`). Если запускаемся с профилем `tomcat`, приложение на каждую транзакцию (или операцию не в транзакции) берет коннект к базе из пула, сконфигурированного в подкладываемом Tomcat `context.xml`. + +> ![](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Для чего мы делаем профиль `tomcat`? Возможно два варианта запуска приложения: либо cargo, либо tomcat? И если мы запускаем через tomcat то в `spring-db.xml` через `jee:jndi-lookup` подтягивается конфигурация tomcata из `\src\main\resources\tomcat\context.xml`? + +1. Есть `cargo-maven3-plugin` который автоматически запускает Tomcat и деплоит туда наше приложение. Т.е. это тоже деплой + в Tomcat, но через Maven. +2. В xml конфигурации Tomcat можно настраивать ресурсы (кроме пула коннектов к БД могут быть, например, JMS или + настройки Mail). Это никак не связано с `cargo` плагином. В Spring этот сконфигурированный ресурс контейнера + сервлетов подлючается через `jee:jndi-lookup`. Тк у нас несколько вариантов конфигурирования `DataSource`, мы этот + вариант сделали в `spring-db.xml` в профиле `tomcat`. +3. Плагин cargo позволяет задавать xml конфигурацию запускаемого Tomcat (у нас `src/main/resources/tomcat/context.xml`). + И в параметрах запуска мы задаем активные профили Spring `tomcat,datajpa` через `spring.profiles.active`. Таким + образом мы в плагине конфигурируем Tomcat, деплоим в него приложение и задаем приложению активные профили Spring + для `DataSource` из конфигурации Tomcat. + +Еще раз: плагин `cargo` и JNDI - это две не связанные между собой вещи, просто мы добавили их в проект в одном патче. +Плагин запускается **после сборки проекта**. Запуск из командной строки: + + mvn clean package -DskipTests=true org.codehaus.cargo:cargo-maven3-plugin:1.9.5:run + +Приложение деплоится в application context topjava: [http://localhost:8080/topjava](http://localhost:8080/topjava) + +- Cargo Maven3 plugin +- Кастомизация context.xml в cargo-maven2-plugin +- Tomcat JNDI Resources +- BasicDataSource Configuration + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 9. Spring Web MVC + +
+ Краткое содержание + +Работа Spring MVC основана на паттерне Front Controller (Единая точка входа). +Все запросы поступают в единый собственный сервлет Spring, в котором происходит его перенаправление на нужный сервлет приложения. +Для работы со Spring MVC нужно заменить зависимость `spring-web` на `spring-webmvc`: +```xml + + org.springframework + spring-webmvc + +``` +После этого в `web.xml` необходимо сконфигурировать единую точку входа - Spring `DispatcherServlet`, в который будут поступать все запросы +к приложению: +```xml + + mvc-dispatcher + org.springframework.web.servlet.DispatcherServlet + + + contextConfigLocation + classpath:spring/spring-mvc.xml + + 1 + + + + + mvc-dispatcher + / + +``` +> Различные контексты не имеют доступа к бинам друг друга (исключение - дочерние контексты могут получать доступ к бинам родителя, но не наоборот), +> каждый контекст поднимает для себя свои собственные экземпляры, поэтому нужно следить за конфигурацией бинов и поднимать их в соответствующем контексте. + +Конфигурацию webmvc контекста и диспетчер-сервлета определим в файле `spring-mvc.xml`. + +#### Сценарий обработки запроса + 1. Запрос поступает в dispatcher-servlet, в нем определен набор Handler Mappings - классы, которые обрабатывают запросы в зависимости от их типа. +- Соответствующий запросу Handler делегирует обработку запроса нужному контроллеру +- Контроллер необходимым образом обрабатывает запрос и возвращает View +- View отображает результат выполнения запроса + +В нашем приложении будет два вида контроллеров: одни — работают с User Interface и отображают результат работы приложения в браузере, +другие — работают по REST-интерфейсу. Контроллеры помечаются аннотацией `@Controller`. +Паттерн и тип HTTP метода, по которым мы можем получить доступ к методу контроллера конфигурируются +с помощью аннотации `@RequestMapping(value = "/users", method = RequestMethod.GET)`. + +> **В последних версиях Spring можно сделать проще: `@GetMapping("/users")`** + +При этом Spring внедрит в метод объект `Model`, в который мы можем добавлять атрибуты и передавать их из слоя контроллера в слой представления. + +Чтобы Spring MVC контекст мог осуществлять роутинг запросов по этим аннотациям, в конфигурации `spring-mvc.xml` нужно +вручную включить поддержку аннотаций: +```xml + +``` + +Методы контроллера, помеченные аннотацией `@RequestMapping` (а также `@GetMapping, @PostMapping, @PutMapping, ..`) после обработки запроса должны возвращать имя представления, в которое +будет передана Model. Эта View отобразится как результат выполнения запроса. Чтобы в этих методах возвращать только название нужной View, в конфигурации нужно +определить `ViewResolver`, который автоматически к этому названию добавит путь к view в приложении и суффикс — формат view: +```xml + +``` + +Для того чтобы приложение имело доступ к статическим ресурсам (например, стили) - нужно добавить дополнительную конфигурацию +в `spring-mvc.xml`: +```xml + +``` +> Spring MVC имеет конфигурацию по умолчанию. Если в `spring-mvc.xml` мы не укажем никаких Handlers, то их стандартный набор +> будет создан автоматически и приложение будет работать. Как только мы добавляем собственные Handlers в конфигурацию — настройки +> по умолчанию переопределяются и будут созданы только те бины, которые мы определили, стандартные Handlers созданы не будут. + +
+ +#### Apply 6_14_spring_webmvc.patch + +**Контроллеры переехали в _spring-mvc.xml_ и не резолвятся в тестах `InMemory`, сделал им `@Ignore`. Требуется починить в HW6** + +Обработка запросов переезжает в `RootController`, сервлеты уже не нужны +> - Починил [путь к корню](http://stackoverflow.com/questions/10327390/how-should-i-get-root-folder-path-in-jsp-page) +> - В Spring 4.3 ввели новые аннотации `@Get/Post/...Mapping` (сокращенный вариант `@RequestMapping`) + +- Spring Web MVC +- [Spring MVC + Spring Data JPA + Hibernate - CRUD Example](https://www.codejava.net/frameworks/spring/spring-mvc-spring-data-jpa-hibernate-crud-example) +- [ContextLoaderListener vs DispatcherServlet](https://howtodoinjava.com/spring-mvc/contextloaderlistener-vs-dispatcherservlet/) +- Паттерн Front Controller +- Иерархия контекстов в Spring Web MVC +- Сценарий обработки запроса. HandlerMappings +- View Resolution: прячем jsp под WEB-INF. +- HandlerMapping: SimpleUrlHandlerMapping, BeanNameUrlHandlerMapping +- Маппинг ресурсов. +- Ресурсы: + - Spring MVC hello world + - Special bean types in the WebApplicationContext + +> Настройки `Project Structure->Modules->Spring`: + +![image](https://user-images.githubusercontent.com/11200258/112018359-8aa1eb80-8b3f-11eb-942f-a8eb7938de41.png) + +> ![](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) В `web.xml` мы инициализируем `DispatcherServlet`, передавая ему параметром `spring-mvc.xml`. Получается, что `DispatcherServlet` парсит `spring-mvc.xml` и находит в нем context? + +Да, можно подебажить родителя (`FrameworkServlet.initWebApplicationContext()`). После инициализации +сервлет `DispatcherServlet` раскидывает все запросы по контроллерам (бинам контекста Спринга). +См. паттерн Front Controller. + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 10. Spring Internationalization + +
+ Краткое содержание + +Spring нормально работает с кириллицей, замена русских символов на их коды уже не требуется (как и с JDK 9). +Spring также автоматически может изменять локаль приложения, для этого в конфигурации `spring-mvc.xml` ему нужно +определить `ReloadableResourceBundleMessageSource`, который будет отвечать за локализацию и указать для него путь к Bundles с локализованными данными. +В страницах JSP мы также должны указать, что теперь будем работать не через JSTL, а через Spring локализацию. +Для этого в страницах удаляем тег `fmt:setBundle`. Теперь Spring автоматически будет подставлять сообщения в зависимости от локали. + Но сейчас Spring работает на основании JDK `ResourceBundle` и он игнорирует свойство *p:cacheSeconds="5"*, так как ресурсы +интернационализации будут кэшироваться Java. Чтобы ресурсы не кэшировались нужно использовать бин `ReloadableResourceBundleMessageSource` с путем к локализации, отличным от classpath приложения. + +```xml + + + + +``` +Теперь ресурсы интернационализации не будут кэшироваться и их можно будет менять во время работы приложения "на ходу". +
+ +#### Apply 6_15_spring_i18n.patch + +**Внимание: проверьте, что переменная окружения `TOPJAVA_ROOT` настроена!** +> - В локализации поменял `fmt:message` на `spring:message` +> - Выбор языка зависит от языка операционной системы и заголовка `Accept-Language`. Добавил в `spring-mvc.xml` `messageSource` параметр [`fallbackToSystemLocale`](http://stackoverflow.com/questions/4281504/spring-local-sensitive-data). +Он управляет выбором, куда переключаться при выборе `en` и отсутствии `app_en.properties`: локаль операционной системы или `app.properties` (`fallbackToSystemLocale=false`). Переключение локалей будем реализовывать в конце проекта. + +#### Для тестирования локали [можно поменять `Accept-Language`](https://stackoverflow.com/questions/7769061/how-to-add-custom-accept-languages-to-chrome-for-pseudolocalization-testing). Для Хрома в `chrome://settings/languages` перетащить нужную локаль наверх или поставить [плагин Locale Switcher](https://chrome.google.com/webstore/detail/locale-switcher/kngfjpghaokedippaapkfihdlmmlafcc) + +- Reloadable MessageSources +- nginx: Serving Static Content + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> Кэш hibernate надстраивается над ehcache или живет самостоятельно? + +- Как работает кэш в Hibernate: +Hibernate подерживает следующие опенсорсные имлементации кэша: EHCache (Easy Hibernate Cache), OSCache (Open Symphony Cache), Swarm Cache, JBoss Tree Cache. + +> Где конфигурируется интернализация для jstl (т.е. файл, где задаются app, app_ru.properties)? Достаточно указать в страницах bundle и путь в ресурсы? + +`` означает, что ресурсы будут искаться в `classpath:messages/app(_xx)/properties`: +Tag setBundle: fully-qualified resource name, which has the same form as a fully-qualified class name. +После сборки проекта maven их можно найти в `target/classes` или `target/topjava/WEB-INF/classes`. + +> Отлично, что она все пишет на том языке, который пришел в заголовке запроса. А если я хочу выбрать? + +Выбор языка зависит от языка операционной системы и заголовка `Accept-Language`. Параметр `fallbackToSystemLocale`, который управляет выбором, когда с `Accept-Language: en,en-US;` не находится локализация `app_en.properties`. Для переключения локали используется JSTL Format Tag fmt:setLocale. Мы будем реализовывать переключение локалей в Spring i18n в конце проекта. + +> Мы создаем бин, где получаем dataSource по имени ``. +Но там не указан класс, как в других dataSource. Получается, по имени jdbc/topjava нам уже отдается готовый объект dataSource, и мы как бы помещаем его в бин? + +Здесь используется namespace `jee:jndi-lookup`, который прячет под собой классы реализации. JNDI объект DataSource конфигурируется в `src/main/resources/tomcat/context.xml` + +> В плагине прописан профиль `tomcat,datajpa`, а в web.xml `postgres,datajpa`. +Какой же реально отрабатывает? + +См. видео урока "Динамическое изменение профиля при запуске". В плагине мы задаем параметры JVM запуска Tomcat + +> A `@NamedQuery` или `@Query` подвержены кэшу запросов? Т.е. если мы поставим _USE_QUERY_CACHE_value_="true", будет ли Hibernate их кэшировать? + +Чтобы запрос кэшировался, кроме true в конфигурации [нужно еще явно выставить запросу _setCacheable_](http://vladmihalcea.com/2015/06/08/how-does-hibernate-query-cache-work/). +По поводу кэширования `@NamedQuery` нашел [`@QueryHint`](https://docs.jboss.org/jbossas/docs/Clustering_Guide/5/html/ch04s02s03.html) + +> Почему messages мы кладем в config и используем system environment? Разве так делают в реальном проекте? Не будешь же вписывать на сервере эти переменные каждый раз, если проект куда-то будет переезжать. Можно по-другому, кроме systemEnvironment['TOPJAVA_ROOT'], задать путь от корня проекта? + +1. messages нам нужны в runtime (при работе приложения). Проект к собранному и задеплоенному в Tomcat war отношения никакого уже не имеет и на этом сервере он обычно не находится. Если ресурсы нужны только при сборке и тестировании, то путь к корню для одномодульного maven проекта можно задать как `${project.basedir}`, но для многомодульного проекта (а все реальные проекты многомодульные) это путь к корню своего модуля. +2. В "реальном приложении" делается совершенно по-разному: + - нести с собой в classpath, но ресурсы нельзя будет динамически (без передеплоя) обновлять + - класть в war (не в classpath) и обновлять в развернутом TOMCAT_HOME/webapps/[appname]/... + - класть в зафиксированное определенное место (например, в home: `~` или в путь от корня `/app/config`). Можно задавать фиксированный пусть в пропертях профиля maven и фильтровать ресурсы (maven resources), чтобы они попали в проперти проекта. + - делать через переменную окружения, как у нас + - задавать в параметрах запуска JVM как системную переменную через -D.. + - располагать в преференсах (для unix это home, для windows - registry): использование Preferences API + - держать настройки в DB + + Часто в одном приложении используют несколько способов для разных видов конфигураций. + +> Не происходит ли дублирования при кэшировании пользователей чрез Hibernate и `@Cacheable`? + +`@Cacheable` кэширует результат запроса `getAll()`, т.е. список юзеров. Hibernate кэширует юзеров по отдельности, т.е., грубо говоря, делает мапу id->User. Можно назвать это дублированием. Нужно ли будет такое в реальном приложении? Все смотрится из логики запросов и их частоты, вполне вероятно, что нет. Как-то мы писали приложение для Дойчебанка (аналог skype на GWT, т.е. на экране небольшое окошко) - там было 5(!!!) уровней кэширования, первый вообще в базе. + +> У меня стоит Томкат 8-й версии, в помнике у нас 9-й прописан, но всё работает. Почему? + +В `pom.xml` мы подключаем `tomcat-servlet-api` со `scope=provided`, что означает, что он используется только для компиляции и не идет в war. Т.к. мы не используем никаких фич Tomcat 9.x, то наш код совместим с Tomcat 8.x. При запуске через `cargo-maven2-plugin` Tomcat 9 загружается из maven-репозитория. + +> Откуда `@Transactional` вытягивает класс для работы с транзакцией, в составе какого бина он идет? + +1. Если в контексте Spring есть ``, то подключается `BeanPostProcessors`, который проксирует классы (и методы), помеченные `@Transactional`. +2. По умолчанию для TransactionManager используется бин с `id=transactionManager` + +--------------------------- + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW06 +- 1.1 Починить тесты `InMemoryAdminRestControllerSpringTest/InMemoryAdminRestControllerTest` (добавлять `spring-mvc.xml` в контекст не стоит, т.к. в новой версии Spring для этого требуется `WebApplicationContext`. Можно просто поправить `inmemory.xml`). +- 1.2 Починить тесты Jdbc (исключить валидацию в тестах Jdbc) + - org.junit.Assume + - [How to get active Profiles in Spring Application](https://stackoverflow.com/questions/9267799/548473) +- 1.3 Перенести функциональность `MealServlet` в `JspMealController` контроллер (по аналогии с `RootController`). +`MealRestController` у нас останется, с ним будем работать позже. + - 1.3.1 разнести запросы на update/delete/.. по разным методам (попробуйте вообще без параметра `action=`). Можно по аналогии с `RootController#setUser` принимать `HttpServletRequest request` (аннотации на параметры и адаптеры для `LocalDate/Time` мы введем позже). + - 1.3.2 в одном контроллере нельзя использовать другой. Чтобы не дублировать код, можно сделать наследование контроллеров от абстрактного класса. + - 1.3.3 добавить локализацию и `jsp:include` в `mealForm.jsp / meals.jsp` + +### Optional +- 2.1 Добавить транзакционность (`DataSourceTransactionManager`) в Jdbc-реализации +- 2.2 Добавить еще одну роль к юзеру Admin (будет 2 роли: `USER, ADMIN`). +- 2.3 В `JdbcUserRepository` добавить реализацию ролей юзера (добавлять можно одним запросом с JOIN и `ResultSetExtractor/ RowCallbackHandler`, либо двумя запросами (отдельно `users` и отдельно `roles`). [Объяснение SQL JOIN](http://www.skillz.ru/dev/php/article-Obyasnenie_SQL_obedinenii_JOIN_INNER_OUTER.html) + - 2.3.1 В реализации `getAll` НЕ делать запрос ролей для каждого юзера (N+1 select) + - 2.3.2 При save посмотрите на batchUpdate() +- 2.4 Добавить проверку ролей в `UserTestData.USER_MATCHER.assertMatch` и починить ВСЕ тесты (тесты должны проходить для юзера с несколькими ролями) +- 2.5 Добавить валидацию для `Jdbc..Repository` через Bean Validation API (для JPA это делается автоматически при сохранении в базу, для Jdbc мы должны это делать вручную). Оптимизировать код. + - Валидацию `@NotNull` для `Meal.user` пока можно закомментировать. На 10-м уроке решим проблему через [Jackson JSON Views](http://www.baeldung.com/jackson-json-view-annotation) + - [Валидация данных при помощи Bean Validation API](https://alexkosarev.name/2018/07/30/bean-validation-api/) + +### Optional 2 (повышенной сложности) +- 3 Отключить Spring кэш в `UserService` в тестах через `NoOpCacheManager` и для кэша Hibernate 2-го уровня `hibernate.cache.use_second_level_cache=false`. + - [JPA 2.0 disable session cache for unit tests](https://stackoverflow.com/a/58963737/548473) + - [Example of PropertyOverrideConfigurer](https://www.concretepage.com/spring/example_propertyoverrideconfigurer_spring) + - [Spring util schema](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#xsd-schemas-util) + +--------------------- +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Подсказки по HW06 +- 1: Неверная кодировка UTF-8 с Spring обычно решается фильтром `CharacterEncodingFilter`: +``` + + encodingFilter + org.springframework.web.filter.CharacterEncodingFilter + + encoding + UTF-8 + + + forceEncoding + true + + + + encodingFilter + /* + +``` +- 2: **Если не поднимается контекст Spring, смотрим причину вверху самого нижнего исключения.** Все ошибки на отсутствия бина в контексте или его нескольких реализациях относятся к пониманию основ: Spring application context. Если нет понимания этих основ, двигаться дальше нельзя, нужно вернуться к видео Спринг, где объясняется, что это такое. Также пересмотрите видео [Тестирование UserService через AssertJ](https://drive.google.com/file/d/1SPMkWMYPvpk9i0TA7ioa-9Sn1EGBtClD). Начиная с 11.30 как раз разбираются подобные ошибки. +- 3: Если неправильно формируется url относительно контекста приложения (например, `/topjava/meals/meals`), посмотрите на + - Relative paths in JSP + - Spring redirect: prefix +- 4: При проблемах с запуском Томкат проверьте запущенные процессы `java`, нет ли в `TOMCAT_HOME\webapps` приложения каталога `topjava`, логи tomcat (нет ли проблем с доступом к каталогам или контекстом Spring) +- 5: Если создаете неизменяемые List или Map, пользуйтесь `List.of()/ Map.of()` +- 6: В MealController общую часть `@RequestMapping(value = "/meals")` лучше вынести на уровень класса +- 7: Проверьте `@Transactional(readOnly = true)` сверху `Jdbc..Repository` +- 8: Проверьте, что `config\messages\app_ru.properties` у вас в кодировке UTF-8 (в любом редакторе/вьюере или при отключенном [Transparent native-to-ascii conversion](https://github.com/JavaOPs/topjava/wiki/IDEA#%D0%9F%D0%BE%D1%81%D1%82%D0%B0%D0%B2%D0%B8%D1%82%D1%8C-%D0%BA%D0%BE%D0%B4%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D1%83-utf-8) в IDEA). +- 9: Учтите, что роли у юзеров можно менять/добавлять/удалять +- 10: Убедитесь, что все методы UserService корректно работают с юзерами, у которых несколько ролей (**запусти наши тесты для Admin с 2-мя ролями**) diff --git a/lessons/lesson07.md b/lessons/lesson07.md new file mode 100644 index 00000000000..32067939740 --- /dev/null +++ b/lessons/lesson07.md @@ -0,0 +1,687 @@ +# Стажировка Topjava + +## Материалы занятия + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW6 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW6 + +#### Apply 7_01_HW6_fix_tests.patch + +
+ Краткое содержание + +#### Починить InMemory и JDBC тесты + +InMemory-тесты перестали работать, т.к ранее мы перенесли сканирование каталога `web` из `spring-app.xml` в конфигурацию `spring-mvc.xml`, которой нет в тестах. В результате контроллеры перестали +попадать в спринг-контекст тестов. Для восстановления добавим сканирование каталога `web` в конфигурацию `inmemory.xml`. Теперь в классах, которые работают с InMemory-реализацией, для создания +контекста можно оставить импорт только конфигурации +`spring/inmemory.xml`. + +JDBC-тесты перестали работать, т.к в конфигурации `spring-db.xml` мы объявили бин `JpaUtil` только для профилей jpa и dataJpa, для других профилей (jdbc) этот бин создаваться не будет. +JDBC-тесты мы запускаем с профилем jdbc, но в абстрактном классе AbstractUserServiceTest (общем для всех тестов сервисного слоя User) для всех профилей мы указали необходимость создания переменной +типа `JpaUtil`. Соответственно, для профиля jdbc в контексте спринга будет отсутствовать этот бин, и спринг не сможет запустить приложение из-за неразрешенной зависимости. + +Чтобы спринг смог поднять контекст в профиле JDBC, нужно указать над переменной `jpaUtil` +аннотацию `@Autowired(required = false)` - мы указываем спрингу, что эта зависимость не является обязательной и можно ее проигнорировать. + +> В новой версии заменил аннотацию на ленивую инициализацию `@Lazy` + +И в `@Before` методе тестов используем этот бин только для JPA реализаций. +Для этого создадим утильный метод `isJpaBased()`, который будет проверять, относится ли текущая реализация к jpa. Чтобы проверить, с какими профилями запущен Spring, нам придется внедрить +в `AbstractServiceTest` +бин класса `Environment`. Это класс спринга, который позволит получить доступ к информации о том, с какими параметрами он был запущен, с помощью +```env.acceptsProfiles(org.springframework.core.env.Profiles.of(Profiles.JPA, Profiles.DATAJPA))``` +С помощью этого же утильного метода теперь мы можем проверить, что для `MealServiceTest` тесты на валидацию `validateRootCause()` будут выполняться только для jpa/dataJpa профилей (если этот тест +запустить для профиля jdbc, то он упадет, т.к. пока в JDBC у нас нет валидации). + +#### Локализация, jsp:include для meal*.jsp + +1. В файлы интернационализации `app.properties` добавляем дополнительные пары ключ-значение для русского и английского языка. В JSP страницах вместо текста, по аналогии со страницами для User, + указываем ключи, вместо которых спринг должен подставить локализованные сообщения. +2. Для каждой JSP страницы для включения фрагментов указываем теги: + +`` - в нем определены title страницы, ссылка на статические ресурсы и базовая ссылка на корень приложения. + +`` - верхняя часть страниц, в ней определены ссылки для навигации по приложению. +И в самом низу страниц: +`` + +Так как мы локализуем приложение с помощью Spring, на страницах нужно удалить тег: +`` - с ним работает только jstl. + +3. Для того, чтобы на страницах получить доступ к корню приложения, используется + `"${pageRequest.request.contextPath}"` - эту ссылку на root удобнее вынести в `headTag` в виде [`` элемента](https://stackoverflow.com/a/40228804/548473), чтобы она вместе с этим + фрагментом добавлялась к каждой странице, и не требовалось бы ее везде дублировать. + +4. Чтобы видеть, к каким URL были привязаны контроллеры во время работы приложения, в `logback.xml` настроим уровень логирования для Spring web: + `` + +#### Перенести функциональность из `MealServlet` в контроллеры + +Чтобы не дублировать одну и ту же функциональность для REST- и JSP-контроллеров, создадим абстрактный +`AbstractMealController` (от него будут наследоваться остальные Meal-контроллеры), куда перенесем все методы из +`MealRestController`. JSP-контроллер будет работать с jsp-страницами. Каждый метод этого контроллера будет делегировать основную функциональность в родительский абстрактный контроллер. + +> **Внимание!**. Не делайте без нужды абстрактных контроллеров в своих выпускных проектах! + +Так как каждый метод этого контроллера должен отвечать за единственное действие, разнесем функциональность по разным методам, а доступ к самим методам разделим с помощью +аннотации `@RequestMapping (@GetMapping / @PostMapping)`, в их параметрах укажем путь к endpoint, по которому можно обратиться к методу. + +При этом для всего контроллера также зададим `@RequestMapping("/meals")` (`value=` - параметр по умолчанию, можно не указывать). Это префикс запроса для всех методов контроллера. + +> Один из признаков "хорошего" контроллера, где не смешивается разная функциональность, - этот общий url. Для каждой функциональности в выпускных создавайте свой собственный контроллер! + +Для доступа к определенному методу контроллера нужно будет указать уникальный для нашего приложения "путь + http-метод", который складывается из маппинга к контроллеру, маппинга к нужному методу и +http-метода, например: +`GET {корень приложения} + "/meals" + "/delete"` +`GET {корень приложения} + "/meals"` +`POST {корень приложения} + "/meals"` +Для `mealList.jsp` теперь не нужно с запросом дополнительно передавать тип действия, которое мы хотим совершить с едой, мы можем просто обратиться к нужному методу по его уникальному пути (endpoint, +url). + +Если на этом шаге запустить приложение, то мы столкнемся с проблемой: при выполнении манипуляций и переходе по ссылкам путь портится. + +- путь к ресурсу по этой ссылке строится не от корня приложения (application context - topjava), а от текущего контекста сервлета (servlet context), например: + `localhost:8080/topjava/meals'+'/meals` + Также перестали работать стили, так как путь к статическим ресурсам тоже определяется неверно (посмотрите вкладку *Network* браузера). + Чтобы это исправить, добавим базовый URL в `headTag`: + `base href = "${pageContext.request.contextPath}/"`. **Теперь это станет url, от которой будут строиться все относительные ссылки на страницах**. + +Также некоторые методы контроллера в результате работы должны не просто вернуть название view, который Spring MVC должен отобразить, а совершить *redirect*. Для этого при возврате имени view +дополнительно укажем ключевое слово `redirect:`, например, `redirect: /meals`. + +Последняя проблема — некорректное отображение текста в кодировке UTF-8. Spring предоставляет для ее решения стандартный фильтр, который будет перехватывать все запросы и ответы сервера и устанавливать +им нужную кодировку: в `web.xml` подключим `encodingFilter`. + +
+ +> Инжекцию в `AbstractUserServiceTest.jpaUtil` сделал [`@Lazy`: не иннициализировать бин до первого использования](https://www.logicbig.com/tutorials/spring-framework/spring-core/lazy-at-injection-point.html). + +#### Apply 7_02_HW6_meals.patch + +> сделал фильтрацию еды через `get`: операция идемпотентная, можно делать в браузере обновление по F5 + +### Внимание: чиним пути в следующем патче + +При переходе на AJAX `JspMealController` удалим за ненадобностью, возвращение всей еды `meals()` останется в `RootController`. + +#### Apply 7_03_HW6_fix_relative_url_utf8.patch + +- [Relative paths in JSP](http://stackoverflow.com/questions/4764405/548473) +- [Spring redirect: prefix](http://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/mvc.html#mvc-redirecting-redirect-prefix) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW6 Optional + +
+ Краткое содержание + +#### Добавление еще одной роли для Admin + +1. В файле популирования базы данных `populateDB.sql` добавим для admin дополнительную роль `ROLE_USER`. + +2. В тестовых данных для него также добавим аналогичную роль. + +> После этого тесты, которые связаны с методом `getAll()`, перестали работать, потому что для получения +> списка всех пользователей с их ролями в именованном запросе мы использовали **LEFT JOIN FETCH**. +> Происходит объединение таблиц, в результирующей таблице вместо одной записи для админа появляются дублирующие записи для одного и того же пользователя. +> - простой способ решения - исключить из запроса **LEFT JOIN FETCH**. Роли все равно будут загружены, так как они FetchType.EAGER. +> - также можно добавить в запрос ключевое слово **DISTINCT(u)** - теперь в результирующей таблице будут содержаться только уникальные записи. + +#### Добавление транзакционности в JDBC реализацию репозитория + +Чтобы аннотация `@Transactional` стала работать во всех профилях Spring - в файле `spring-db.xml` вынесем из профиля jpa, dataJpa в общую конфигурацию для всех профилей тег: +`````` + +Для профиля jdbc настроим DatasourceTransactionManager, который будет управлять транзакциями: +`` + + + +`` +После этого в JDBC-репозитории мы можем расставить аннотации `@Transactional` аналогично jpa репозиториям, и действия станут выполняться транзакционно ( +напомню: `` для логирования информации по транзакциям) + +#### Чтобы JDBC репозиторий смог работать с множественными ролями пользователя: + +У пользователя добавим сеттер для его ролей. Для JDBC-репозитория создадим вспомогательные методы для записи ролей в базу и их считывания из базы и установления пользователю. Запись ролей в базу будем +производить методом +`JdbcTemplate#batchUpdate`, в таком случае не будет обращения в базу для записи каждой конкретной роли, команды для записи ролей будут накоплены в один пакет и выполнятся за одно обращение к БД. Для +удобства работы с batch Spring предоставляет нам интерфейс `BatchPreparedStatementSetter`, с помощью которого мы определяем как будут устанавливаться параметры для запроса и количество запросов в +одном пакете. Также создадим метод `deleteRoles`, в котором будем удалять роли пользователя из базы (для обновления ролей в базе мы делаем просто: сначала удалим старые из базы и запишем туда новые). + +> PS: в JPA с `@ElementCollection` и с параметром *cascade* в `@OneToMany` слияние (merge) изменений в связанных коллекциях происходит автоматически. + +Если мы будем получать всех пользователей вместе с их ролями из базы с помощью JOIN, мы столкнемся с проблемой Декартова произведения: для каждого уникального пользователя количество записей в +результирующей таблице будет повторяться столько раз, сколько у него было ролей. Чтобы этого избежать, отдельным запросом получим из базы все роли, и сгруппируем их в `Map` по `userId`, где ключом +будет являться `userId`, а значением — набор ролей пользователя. После чего пройдемся по всем пользователям, загруженным из базы, и установим каждому его роли. +
+ +#### Apply 7_04_HW6_optional_add_role.patch + +> - Для доставания ролей у нас дублируется `fetch = EAGER` и `LEFT JOIN FETCH u.roles` (можно делать что-то одно). Запросы выполняются по-разному: проверьте. + +- Отключил `JdbcUserServiceTest` - роли не работают. Будем чинить в `7_06_HW6_jdbc_transaction_roles.patch` +- `DataJpaUserServiceTest.getWithMeals` не работает для admin (у админа 2 роли, и еда при JOIN дублируется). Чиним в следующем патче. + +#### Apply 7_05_HW6_fix_hint_graph.patch + +- `@EntityGraph` в `DataJpaUserServiceTest.getWithMeals()` в последнем Hibernate работает только с `attributePaths = {"meals"}` и `type = EntityGraph.EntityGraphType.LOAD` +- В `JpaUserRepositoryImpl.getByEmail` и `CrudUserRepository.getByEmail` DISTINCT попадает в запрос, хотя он там не нужен. Это просто указание Hibernate не дублировать данные. Для оптимизации можно + указать Hibernate делать запрос без distinct: [15.16.2. Using DISTINCT with entity queries](https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql-distinct) +- Бага [HINT_PASS_DISTINCT_THROUGH does not work if 'hibernate.use_sql_comments=true'](https://hibernate.atlassian.net/browse/HHH-13280). При `hibernate.use_sql_comments=false` все работает - в SELECT + нет DISTINCT. + +Еще один вариант решения - в `User` сделать `Set`. Интересно, что в ее реализации `PersistentSet`порядок соблюдается и `@OrderBy` работает. + +#### Apply 7_06_HW6_jdbc_transaction_roles.patch + +> - в `JdbcUserRepositoryImpl.getAll()` собираю роли из `ResultSet` напрямую в `map` +> - в `insertRoles` поменял метод `batchUpdate` и сделал проверку на empty +> - в `setRoles` достаю роли через `queryForList` + +Еще интересные JDBC реализации: + +- в `getAll()/ get()/ getByEmail()` делать запросы с `LEFT JOIN` и сделать реализацию `ResultSetExtractor` +- подключить зависимость `spring-data-jdbc-core`. Там есть готовый `OneToManyResultSetExtractor`. Можно посмотреть, как он реализован. +- реализация, зависимая от БД: доставать агрегированные роли и делать им `split(",")`. В этой реализации есть ограничение - одно поле из зависимой таблицы. + +``` +SELECT u.*, string_agg(r.role, ',') AS roles +FROM users u + JOIN user_roles r ON u.id=r.user_id +GROUP BY u.id +``` + +### Валидация для `JdbcUserRepository` через Bean Validation API + +#### Apply 7_07_HW6_optional_jdbc_validation.patch + +- [Валидация данных при помощи Bean Validation API](https://alexkosarev.name/2018/07/30/bean-validation-api/). + +На данный момент у нас реализована валидация сущностей только для jpa- и dataJpa-репозиториев. При работе через JDBC-репозиторий может произойти попытка записи в БД некорректных данных, что приведет +к `SQLException` из-за нарушения ограничений, наложенных на столбцы базы данных. Для того, чтобы перехватить невалидные данные еще до обращения в базу, воспользуемся API *javax.validation* (ее +реализация `hibernate-validator` используется для проверки данных в Hibernate и будет использоваться в Spring Validation, которую подключим позже). В `ValidationUtil` создадим один потокобезопасный +валидатор, который можно переиспользовать (см. *javadoc*). +С его помощью в методах сохранения и обновления сущности в jdbc-репозиториях мы можем производить валидацию этой сущности: `ValidationUtil.validate(object);` +Чтобы проверка не падала, `@NotNull Meal.user` пришлось пока закомментировать. Починим в 10-м занятии через `@JsonView`. + +### Отключение кэша в тестах: + +Вместо наших приседаний с `JpaUtil` и проверкой профилей мы можем отключить Spring-кэш в тестах, подменив `spring-cache.xml` на тестовый (положив его в ресурсы тестов). +Отключить кэширование можно через пустую реализацию `NoOpCacheManager` или, как сейчас, не включая `cache:annotation-driven`, который подключает обработку `@Cache`-аннотаций. +Кэш Hibernate второго уровня отключаем через переопределение свойства `entityManagerFactory.jpaPropertyMap: hibernate.cache.use_second_level_cache=false` (кроме стандартного использования файла +пропертей, можно задать их прямо в конфигурации, через автодополнение в xml можно смотреть все варианты). Подкладываем новый `spring-cache.xml` в ресурсы тестов, он перекроет настройки кэша в +приложении. Остается удалить наши уже ненужные `JpaUtil` и `AbstractServiceTest.isJpaBased()` + +#### Apply 7_08_HW06_optional2_disable_tests_cache.patch + +- [Example of PropertyOverrideConfigurer](https://www.concretepage.com/spring/example_propertyoverrideconfigurer_spring) +- [Spring util schema](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#xsd-schemas-util) + +## Занятие 7: + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Тестирование Spring MVC + +
+ Краткое содержание + +#### Тестирование Spring MVC + +Для более удобного сравнения объектов в тестах мы будем использовать библиотеку *Harmcrest* с Matcher'ами, которая позволяет делать сложные проверки. С *Junit* по умолчанию подтягивается *Harmcrest +core*, но нам потребуется расширенная версия: +в `pom.xml` из зависимости Junit исключим дочернюю `hamcrest-core` и добавим `hamcrest-all`. + +Для тестирования web создадим вспомогательный класс `AbstractControllerTest`, от которого будут наследоваться все тесты контроллеров. Его особенностью будет наличие `MockMvc` - эмуляции Spring MVC для +тестирования web-компонентов. Инициализируем ее в методе, отмеченном `@PostConstruct`: + + ``` +mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilter(CHARACTER_ENCODING_FILTER).build(); + ``` + +Для того, чтобы в тестах контроллеров не популировать базу перед каждым тестом, пометим этот базовый тестовый класс аннотацией `@Transactional`. Теперь каждый тестовый метод будет выполняться в +транзакции, которая будет откатываться после окончания метода и возвращать базу данных в исходное состояние. Однако теперь в работе тестов могут возникнуть нюансы, связанные с пропагацией транзакций: +все транзакции репозиториев станут вложенными во внешнюю транзакцию теста. При этом, например, кэш первого уровня станет работать не так, как ожидается. Т.е при таком подходе нужно быть готовыми к +ошибкам: мы их увидим и поборем в тестах на обработку ошибок на последних занятиях TopJava. + +#### UserControllerTest + +Создадим тестовый класс для контроллера юзеров, он должен наследоваться от `AbstractControllerTest`. В `MockMvc` +используется [паттерн проектирования Builder](https://refactoring.guru/ru/design-patterns/builder). + + ``` + mockMvc.perform(get("/users")) // выполнить HTTP метод GET к "/users" + .andDo(print()) // распечатать содержимое ответа + .andExpect(status().isOk()) // от контроллера ожидается ответ со статусом HTTP 200(ok) + .andExpect(view().name("users")) // контроллер должен вернуть view с именем "users" + .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")) // ожидается, что клиент должен быть перенаправлен на "/WEB-INF/jsp/users.jsp" + .andExpect(model().attribute("users", hasSize(2))) // в модели должен быть атрибут "users" размером = 2 + .andExpect(model().attribute("users", hasItem( // внутри которого есть элемент ... + allOf( + hasProperty("id", is(START_SEQ)), // ... с аттрибутом id = START_SEQ + hasProperty("name", is(USER.getName())) //... и name = user + ) + ))); +} + ``` + +В параметры метода `andExpect()` передается реализация `ResultMatcher`, в которой мы определяем как должен быть обработан ответ контроллера. + +
+ +#### Apply 7_09_controller_test.patch + +> - в `MockMvc` добавился `CharacterEncodingFilter` +> - поменял реализацию `ActiveDbProfileResolver`: в профили аттрибута `@ActiveProfiles(profiles=..)` он добавляет `Profiles.getActiveDbProfile()` +> - сделал вспомогательный метод `AbstractControllerTest.perform()` + +- Hamcrest +- Unit Testing of Spring MVC Controllers + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. [Миграция на JUnit 5](https://drive.google.com/open?id=16wi0AJLelso-dPuDj6xaGL7yJPmiO71e) + +
+ Краткое содержание + +Для миграции на 5-ю версию JUnit в файле `pom.xml` поменяем зависимость `junit` +на `junit-jupiter-engine` ([No need `junit-platform-surefire-provider` dependency in `maven-surefire-plugin`](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven)). Актуальную +версию всегда можно посмотреть [в центральном maven репозитории](https://search.maven.org/search?q=junit-jupiter-engine), берем только релизы (..-Mx означают предварительные milestone версии) +Изменять конфигурацию плагина `maven-sureface-plugin` в новых версиях JUnit уже не требуется. Junit5 не содержит в себе зависимости от *Harmcrest* (которую нам приходилось вручную отключать для JUnit4 +в предыдущих шагах), поэтому исключение `hamcrest-core` просто удаляем. В итоге у нас останутся зависимости JUnit5 и расширенный Harmcrest. +Теперь мы можем применить все нововведения пятой версии в наших тестах: + +1. Для всех тестов теперь мы можем удалить `public`. +2. Аннотацию `@Before` исправим на `@BeforeEach` - теперь метод, который будет выполняться перед каждым тестом, помечается именно так. +3. В Junit5 работа с исключениями похожа на Junit4 версии 4.13: вместо ожидаемых исключений в параметрах аннотации `@Test(expected = Exception.class)` используется метод `assertThrows()`, в который + первым аргументом мы передаем ожидаемое исключение, а вторым аргументом — реализацию функционального интерфейса `Executable` (кода теста, в котором ожидается возникновение исключения). +4. Метод `assertThrows()` возвращает исключение, которое было выброшено в переданном ему коде. Теперь мы можем получить это исключение, извлечь из него сообщение с помощью + `e.getMessage()` и сравнить с ожидаемым. +5. Для теста на валидацию при проверке предусловия, только при выполнении которого будет выполняться следующий участок кода (например, в нашем случае тесты на валидацию выполнялись только в jpa + профиле), - теперь нужно пользоваться утильным методом `Assumptions` (нам уже не требуется). +6. Проверку Root Cause - причины, из-за которой было выброшено пойманное исключение, мы будем делать позднее, при тестах на ошибки. +7. Из JUnit5 исключена функциональность `@Rule`, вместо них теперь нужно использовать `Extensions`, которые могут встраиваться в любую фазу тестов. Чтобы добавить их в тесты, пометим базовый тестовый + класс аннотацией `@ExtendWith`. + +JUnit предоставляет нам набор коллбэков — интерфейсов, которые будут исполняться в определенный момент тестирования. Создадим класс `TimingExtension`, который будет засекать время выполнения тестовых +методов. +Этот класс будет имплементировать маркерные интерфейсы — коллбэки JUnit: + +- `BeforeTestExecutionCallback` - коллбэк, который будет вызывать методы этого интерфейса перед каждым тестовым методом. +- `AfterTestExecutionCallback` - методы этого интерфейса будут вызываться после каждого тестового метода; +- `BeforeAllCallback` - методы перед выполнением тестового класса; +- `AfterAllCallback` - методы после выполнения тестового класса; + +Осталось реализовать соответствующие методы, которые описываются в каждом из этих интерфейсов, они и будут вызываться JUnit в нужный момент: + +- в методе `beforeAll` (который будет вызван перед запуском тестового класса) создадим спринговый утильный секундомер `StopWatch` для текущего тестового класса; +- в методе `beforeTestExecution` (будет вызван перед тестовым методом) - запустим секундомер; +- в методе `afterTestExecution` (будет вызван после тестового метода) - остановим секундомер. +- в методе `afterAll` (который будет вызван по окончанию работы тестового класса) - выведем результат работы этого секундомера в консоль; + +8. Аннотации `@ContextConfiguration` и `@ExtendWith(SpringExtension.class)` (замена `@RunWith`) мы можем заменить одной `@SpringJUnitConfiguration` (в старых версиях IDEA ее не понимает) + +
+ +#### Apply 7_10_JUnit5.patch + +- [No need `junit-platform-surefire-provider` dependency in `maven-surefire-plugin`](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven) +- [Наконец пофиксили баг с `@SpringJUnitConfig`](https://youtrack.jetbrains.com/issue/IDEA-166549) +- Проверил без [`junit-platform-launcher` в pom для запуска JUnit 5 тестов из IDEA](https://youtrack.jetbrains.com/issue/IDEA-231927). В новой версии IDEA работает без него, проверьте у себя. +- [JUnit 5 homepage](https://junit.org/junit5) +- [Overview](https://junit.org/junit5/docs/snapshot/user-guide/#overview) +- [Миграция с JUnit4 на JUnit5: важные отличия и преимущества](https://topjava.ru/blog/migratsiya-s-junit4-na-junit5) +- [10 интересных нововведений](https://habr.com/post/337700) +- Дополнительно: + - [Extension Model](https://junit.org/junit5/docs/current/user-guide/#extensions) + - [A Guide to JUnit 5](http://www.baeldung.com/junit-5) + - [Migrating from JUnit 4](http://www.baeldung.com/junit-5-migration) + - [Before and After Test Execution Callbacks](https://junit.org/junit5/docs/snapshot/user-guide/#extensions-lifecycle-callbacks-before-after-execution) + - [Conditional Test Execution](https://junit.org/junit5/docs/snapshot/user-guide/#writing-tests-conditional-execution) + - [Third party Extensions](https://github.com/junit-team/junit5/wiki/Third-party-Extensions) + - [Реализация assertThat](https://stackoverflow.com/questions/43280250) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. [Принципы REST. REST контроллеры](https://drive.google.com/open?id=1e4ySjV15ZbswqzL29UkRSdGb4lcxXFm1) + +
+ Краткое содержание + +#### Принципы REST, REST-контроллеры + +> [REST](http://spring-projects.ru/understanding/rest/) - архитектурный стиль проектирования распределенных систем (типа клиент-сервер). + +Чаще всего в REST сервер и клиент общаются посредством обмена JSON-объектами через HTTP-методы GET/POST/PUT/DELETE/PATCH. +Особенностью REST является отсутствие состояния (контекста) взаимодействий клиента и сервера. + +В нашем приложении есть контроллеры для Admin и для User. Чтобы сделать их REST-контроллерами, заменим аннотацию `@Controller` на `@RestController` + +> Не поленитесь зайти чз Ctrl+Click в `@RestController`: к аннотации `@Controller` добавлена `@ResponseBody`. Т.е. ответ от нашего приложения будет не имя View, а данные в теле ответа. + +В `@RequestMapping`, кроме пути для методов контроллера (`value`) добавляем параметр `produces = MediaType.APPLICATION_JSON_VALUE`. Это означает, что в заголовки ответа будет добавлен +тип `ContentType="application/json"` - в ответе от контроллера будет приходить JSON-объект. + +> Чтобы было удобно использовать путь к этому контроллеру в приложении и в тестах, +> выделим путь к нему в константу REST_URL, к которой можно будет обращаться из других классов + +1. Метод `AdminRestController.getAll` пометим аннотацией `@GetMapping` - маршрутизация к методу по HTTP GET. + +2. Метод `AdminRestController.get` пометим аннотацией `@GetMapping("/{id}")`. + В скобках аннотации указано, что к основному URL контроллера будет добавляться `id` пользователя - переменная, которая передается в запросе непосредственно в URL. + Соответствующий параметр метода нужно пометить аннотацией `@PathVariable` (если имя в URL и имя аргумента метода не совпадают, в параметрах аннотации дополнительно нужно будет уточнить имя в URL. + Если они совпадают, [этого не требуется](https://habr.com/ru/post/440214/). + +3. Метод создания пользователя `create` отметим аннотацией `@PostMapping` - маршрутизация к методу по HTTP POST. В метод мы передаем объект `User` в теле запроса (аннотация `@RequestBody`) и в формате + JSON (`consumes = MediaType.APPLICATION_JSON_VALUE`). При создании нового ресурса правила хорошего тона - вернуть в заголовке ответа URL созданного ресурса. Для этого возвращем не `User`, + а `ResponseEntity`, который мы можем с помощью билдера `ServletUriComponentsBuilder` дополнить заголовком ответа `Location` и вернуть статус `CREATED(201)` + (если пойти в код `ResponseEntity.created` можно докопаться до сути, очень рекомендую смотреть в исходники кода). + +4. Метод `delete` помечаем `@DeleteMapping("/{id}")` - HTTP DELETE. Он ничего не возвращает, поэтому помечаем его аннотацией `@ResponseStatus(HttpStatus.NO_CONTENT)`. Статус ответа будет HTTP.204; + +5. Над методом обновления ставим `@PutMapping` (HTTP PUT). В аргументах метод принимает `@RequestBody User user` и `@PathVariable int id`. + +6. Метод поиска по `email` также помечаем `@GetMapping`, и, чтобы не было конфликта маршрутизации с методом `get()`, указываем в URL добавку "/by". В этот метод `email` передается как параметр + запроса, аннотация `@RequestParam`. + +> **Все это СТАНДАРТ архитектурного стиля REST. НЕ придумывайте ничего своего в своих выпускных проектах! Это очень большая ошибка - не придерживаться стандартов API.** + +7. `ProfileRestController` выполняем аналогичным способом с учетом того, что пользователь имеет доступ только к своим данным. + +Если на данном этапе попытаться запустить приложение и обратиться к какому-либо методу контроллера, сервер ответит нам ошибкой со статусом 406, так как Spring не знает, как преобразовать объект User в +JSON... + +
+ +#### Apply 7_11_rest_controller.patch + +> - Переделал URL поиска по email на `/by-email` + +- Понимание REST +- JSON (JavaScript Object Notation) +- [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/) +- [10 Best Practices for Better RESTful](https://medium.com/@mwaysolutions/10-best-practices-for-better-restful-api-cbe81b06f291) +- [Best practices for rest nested resources](https://stackoverflow.com/questions/20951419/what-are-best-practices-for-rest-nested-resources) +- + Request mapping +- [Лучшие практики разработки REST API: правила 1-7,15-17](https://tproger.ru/translations/luchshie-praktiki-razrabotki-rest-api-20-sovetov/) +- Дополнительно: + - [Подборка практик REST](https://gist.github.com/Londeren/838c8a223b92aa4017d3734d663a0ba3) + - JAX-RS vs Spring MVC + - RESTful API для сервера – делаем правильно (Часть 1) + - RESTful API для сервера – делаем правильно (Часть 2) + - И. Головач. RestAPI + - [value/name в аннотациях @PathVariable и @RequestParam](https://habr.com/ru/post/440214/) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. [Тестирование REST контроллеров. Jackson.](https://drive.google.com/open?id=1aZm2qoMh4yL_-i3HhRoyZFjRAQx-15lO) + +
+ Краткое содержание + +Для работы с JSON добавляем в `pom.xml` зависимость `jackson-databind`. +Актуальную версию библиотеки можно посмотреть в [центральном maven-репозитории](https://search.maven.org/artifact/com.fasterxml.jackson.core/jackson-databind). +Теперь спринг будет автоматически использовать эту библиотеку для сериализации/десериализации объектов в JSON (найдя ее в *classpath*). +Если сейчас запустить приложение и обратиться к методам REST-контроллера, то оно выбросит `LazyInitializationException`. Оно возникает из-за того, что у наших сущностей есть лениво загружаемые поля, +отмеченные `FetchType.LAZY` - при загрузке сущности из базы, вместо этого поля подставится Proxy, который и должен вернуть реальный экземпляр этого поля при первом же обращении. Jackson при +сериализации в JSON использует все поля сущности, и при обращении к *Lazy* полям возникает исключение, так как сессия работы с БД в этот момент уже закрыта, и нужный объект не может быть +инициализирован. Чтобы Jackson игнорировал эти поля, пометим их аннотацией `@JsonIgnore`. + +Теперь при запуске приложения REST-контроллер будет работать. Но при получении JSON объектов мы можем увидеть, что Jackson сериализовал объект через геттеры (например в ответе есть поле `new` от +метода `Persistable.isNew()`). Чтобы учитывались только поля объектов, добавим над `AbstractBaseEntity`: + +````java +@JsonAutoDetect(fieldVisibility = ANY, // jackson видит все поля + getterVisibility = NONE, // ... но не видит геттеров + isGetterVisibility = NONE, //... не видит геттеров boolean полей + setterVisibility = NONE) // ... не видит сеттеров +```` + +Теперь все сущности, унаследованные от базового класса, будут сериализоваться/десериализоваться через поля. + +
+ +#### Apply 7_12_rest_test_jackson.patch (внимание: обновил патч 23.03 в 11.00) + +- [Jackson databind github](https://github.com/FasterXML/jackson-databind) +- [Jackson Annotation Examples](https://www.baeldung.com/jackson-annotations) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. [Кастомизация Jackson Object Mapper](https://drive.google.com/open?id=1CM6y1JhKG_yeLQE_iCDONnI7Agi4pBks) + +
+ Краткое содержание + +Сейчас, чтобы не сериализовать *Lazy* поля, мы должны пройтись по каждой сущности и вручную пометить их аннотацией `@JsonIgnore`. Это неудобно, засоряет код и допускает возможные ошибки. К тому же, +при некоторых условиях, нам иногда нужно загрузить и в ответе передать эти *Lazy* поля. +Чтобы запретить сериализацию Lazy полей для всего проекта, подключим в `pom.xml` библиотеку `jackson-datatype-hibernate`. +Также изменим сериализацию/десериализацию полей объектов в JSON: не через аннотацию `@JsonAutoDetect`, а в классе `JacksonObjectMapper`, который унаследуем от `ObjectMapper` (стандартный Mapper, +который использует Jackson) и сделаем в нем другие настройки. В конструкторе: + +- регистрируем `Hibernate5Module` - модуль `jackson-datatype-hibernate`, который не делает сериализацию ленивых полей. +- модуль для корректной сериализации `LocalDateTime` в поля JSON - `JavaTimeModule` модуль библиотеки `jackson-datatype-jsr310` +- запрещаем доступ ко всем полям и методам класса и потом разрешаем доступ только к полям +- не сериализуем null-поля (`setSerializationInclusion(JsonInclude.Include.NON_NULL)`) + +Чтобы подключить наш кастомный `JacksonObjectMapper` в проект, в конфигурации `spring-mvc.xml` к настройке `` добавим `MappingJackson2HttpMessageConverter`, который будет +использовать наш маппер. + +
+ +#### Apply 7_13_jackson_object_mapper.patch + +- Сериализация hibernate lazy-loading с помощью + jackson-datatype-hibernate +- Handle Java 8 dates with Jackson +- Дополнительно: + - Jackson JSON Serializer & Deserializer + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. [Тестирование REST контроллеров через JSONassert и Матчеры](https://drive.google.com/open?id=1oa3e0_tG57E71g6PW7_tfb3B61Qldctl) + +
+ Краткое содержание + +Сейчас в тестах REST-контроллера мы проводим проверку только на статус ответа и тип возвращаемого контента. Добавим проверку содержимого ответа. + +#### 7_14_json_assert_tests + +Чтобы сравнивать содержимое ответа контроллера в виде JSON и сущность, воспользуемся библиотекой +`jsonassert`, которую подключим в `pom.xml` со scope *test*. + +Эта библиотека при сравнении в тестах в качестве ожидаемого значения ожидает от нас объект в виде JSON-строки. Чтобы вручную не преобразовывать объекты в JSON и не хардкодить их в виде строк в наши +тесты, воспользуемся Jackson. +Для преобразования объектов в JSON и обратно создадим утильный класс `JsonUtil`, в котором с помощью нашего `JacksonObjectMapper` и будет конвертировать объекты. +И мы сталкиваемся с проблемой: `JsonUtil` - утильный класс и не является бином спринга, а для его работы требуется наш кастомный маппер, который находится под управлением спринга и расположен в +контейнере зависимостей. Поэтому, чтобы была возможность получить наш маппер из других классов - сделаем его синглтоном и сделаем в нем статический метод, который будет возвращать его экземпляр. +Теперь `JsonUtil` сможет его получить. +И нам нужно указать спрингу, чтобы он не создавал второй экземпляр этого объекта, а клал в свой контекст существующий. Для этого в конфигурации `spring-mvc.xml` определим factory-метод, с помощью +которого спринг должен получить экземпляр (instance) этого класса: + +```xml + + +``` + +а в конфигурации `message-converter` вместо создания бина просто сошлемся на сконфигурированный `objectMapper`. + +Метод `ContentResultMatchers.json()` из `spring-test` использует библиотеку `jsonassert` для сравнения 2-х JSON строк: одну из ответа контроллера и вторую - JSON-сериализация `admin` без +поля `registered` (это поле инициализируется в момент создания и отличается). В методе `JsonUtil.writeIgnoreProps` мы преобразуем объект `admin` в мапу, удаляем из нее игнорируемые поля и снова +сериализуем в JSON. + +Также сделаем тесты для утильного класса `JsonUtil`. В тестах мы записываем объект в JSON-строку, затем конвертируем эту строку обратно в объект и сравниваем с исходным. И то же самое делаем со +списком объектов. + +#### 7_15_tests_refactoring + +**`RootControllerTest`** + +Сделаем рефакторинг `RootControllerTest`. Ранее мы в тесте получали модель, доставали из нее сущности и с помощью `hamcrest-all` +производили по одному параметру их сравнение с ожидаемыми значениями. Метод `ResultActions.andExpect()` позволяет передавать реализацию интерфейса `Matcher`, в котором можно делать любые сравнения. +Функциональность сравнения списка юзеров по ВСЕМ полям у нас уже есть - мы просто делегируем сравнение объектов в `UserTestData.MATCHER`. При этом нам больше не нужен `harmcrest-all`, нам достаточно +только `harmcrest-core`. + +**`MatcherFactory`** + +Теперь вместо `jsonassert` и сравнения JSON-строк в тестах контроллеров сделаем сравнения JSON-объектов через `MatcherFactory`. Преобразуем ответ контроллера из JSON в объект и сравним с эталоном +через уже имеющийся у нас матчер. +Вместо сравнения JSON-строк в метод `andExpect()` мы будем передавать реализации интерфейса `ResultMatcher` из `MATCHER.contentJson(..)`. + +`MATCHER.contentJson(..)` принимают ожидаемый объект и возвращают для него `ResultMatcher` с реализацией единственного метода `match(MvcResult result)`, в котором делегируем сравнение уже существующим +у нас матчерам. Мы берем JSON-тело ответа (`MatcherFactory.getContent`), десериализуем его в объект (`JsonUtil.readValue/readValues`) и сравниваем через имеющийся `MATCHER.assertMatch` +десериализованный из тела контроллера объект и ожидаемое значение. + +> Методы из класса `TestUtil` перенес в `MatcherFactory`, лишние удалил. + +**`AdminRestControllerTest`** + +- `getByEmail()` - сделан по аналогии с тестом `get()`. Дополнительно нужно дополнить строку URL параметрами запроса. +- `delete()` - выполняем HTTP.DELETE. Проверяем статус ответа 204. Проверяем, что пользователь удален. + +> Раньше я получал всех users из базы и проверял, что среди них нет удаленного. При этом тесты становятся чувствительными ко всем users в базе и ломаются при добавлении-удалении новых тестовых данных. + +- `update()` - выполняем HTTP.PUT. В тело запроса подаем сериализованный `JsonUtil.writeValue(updated)`. После выполнения проверяем, что объект в базе обновился. +- `create()` - выполняем HTTP.POST аналогично `update()`. Но сравнить результат мы сразу не можем, т.к. при создании объекта ему присваивается `id`. + Поэтому мы извлекаем созданного пользователя из ответа (`MATCHER.readFromJson(action)`), получаем его `id`, и уже с этим `id` эталонный объект мы можем сравнить с объектом в ответе контроллера и со + значением в базе. +- `getAll()` - аналогично get(). Список пользователей из ответа в формате JSON сравниваем с эталонным списком (`MATCHER.contentJson(admin, user)`). + +Тесты для `ProfileRestController` выполнены аналогично. + +
+ +#### Apply 7_14_json_assert_tests.patch + +> - В `JsonUtil.writeIgnoreProps` вместо цикла по мапе сделал `map.keySet().removeAll` + +- [JSONassert](https://github.com/skyscreamer/JSONassert) +- [Java Code Examples for ObjectMapper](https://www.programcreek.com/java-api-examples/index.php?api=com.fasterxml.jackson.databind.ObjectMapper) + +#### Apply 7_15_tests_refactoring.patch + +> - Сделал внутренний класс `MatcherFactory.Matcher`, который возвращается из фабрики матчеров. +> - Методы из класса `TestUtil` перенес в `MatcherFactory`, лишние удалил. +> - Раньше в тестах я для проверок получал всех users из базы и сравнивал с эталонным списком. При этом тесты становятся чувствительными ко всем users в базе и ломаются при добавлении-удалении новых тестовых данных. + +- [Java @SafeVarargs Annotation](https://www.baeldung.com/java-safevarargs) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 9. [Тестирование через SoapUi. UTF-8](https://drive.google.com/open?id=0B9Ye2auQ_NsFVXNmOUdBbUxxWVU) + +
+ Краткое содержание + +SOAP UI - это один из инструментов для тестирования API приложений, которые работают по REST и по SOAP. +Он позволяет нам по HTTP протоколу дернуть методы нашего API и увидеть ответ контроллеров. + +Если в контроллер мы добавим метод, который в теле ответа будет возвращать текст на кириллице, то увидим, что кодировка теряется. Для сохранения кодировки используем `StringHttpMessageConverter`, +который конфигурируем в `spring-mvc.xml`. При этом мы должны явно указать, что конвертор будет работать только с текстом в кодировке *UTF-8*. + +
+ +#### Apply 7_16_soapui_utf8_converter.patch + +- Инструменты тестирования REST: + - SoapUi + - [Что такое Curl? Как работает эта команда?](https://highload.today/curl/) + - Написание HTTP-запросов с помощью Curl. + Для Windows 7 можно использовать Git Bash, с Windows 10 v1803 можно прямо из консоли. Возможны проблемы с UTF-8: + - [CURL doesn't encode UTF-8](https://stackoverflow.com/a/41384903/548473) + - [кириллица в теле POST-запроса](https://barbitoff.blogspot.com/2018/11/soapui-post-rest.html) + - [Нстройка кодировки в Windows](https://support.socialkit.ru/ru/knowledge-bases/4/articles/11110-preduprezhdenie-obnaruzhenyi-problemyi-svyazannyie-s-raspoznavaniem-russkih-simvolov) + - **[IDEA: Tools->HTTP Client->...](https://www.jetbrains.com/help/idea/rest-client-tool-window.html)** + - Postman + - [Insomnia REST client](https://insomnia.rest/) + +**Импортировать проект в SoapUi из `config\Topjava-soapui-project.xml`. Response смотреть в формате JSON.** + +> Проверка UTF-8: http://localhost:8080/topjava/rest/profile/text + +[ResponseBody and UTF-8](http://web.archive.org/web/20190102203042/http://forum.spring.io/forum/spring-projects/web/74209-responsebody-and-utf-8) + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы + +> Зачем у нас и UIController'ы, и RestController'ы? То есть в общем случае backend-разработчику недостаточно предоставить REST-api и RestController? + +Часто используются и те и другие. REST обычно используют для отдельного UI например на React или Angular или для интеграции / мобильного приложения. +У нас REST контроллеры используются только для тестирования. UI мы используем для нашего приложения на JSP шаблонах. +Таких сайтов без богатой UI логики тоже немало. Например https://javaops.ru/ :) +Разница в обработке запросов: + +- из UI контроллеров возвращаются как готовые HTML странички, так и данные в формате JSON (будет для AJAX запросов в следующих занятиях) +- для UI мы используем только GET и POST запросы +- при создании-обновлении в UI мы принимаем данные из формы `application/x-www-form-urlencoded` (посмотрите вкладку `Network`, не в формате JSON) +- для REST запросы GET, POST, PUT, DELETE, PATCH и возвращают только данные (обычно JSON) + +И в способе авторизации: + +- для RESТ у нас будет базовая авторизация +- для UI - через cookies + +Также часто бывают смешанные сайты - где есть и отдельное JS приложение и шаблоны. + +> При выполнении тестов через MockMvc никаких изменений на базе не видно, почему оно не сохраняет? + +`AbstractControllerTest` аннотируется `@Transactional` - это означает, что тесты идут в транзакции, и после каждого теста JUnit делает rollback базы. + +> Что получается в результате выполнения запроса `SELECT DISTINCT(u) FROM User u LEFT JOIN FETCH u.roles ORDER BY u.name, u.email`? В чем разница в SQL без `DISTINCT`. + +Запросы SQL можно посмотреть в логах. Т.е. `DISTINCT` в `JPQL` влияет на то, как Hibernate обрабатывает дублирующиеся записи (с `DISTINCT` их исключает). Результат можно посмотреть в тестах или +приложении, поставив брекпойнт. По поводу `SQL DISTINCT` не стесняйтесь пользоваться google, например, [оператор SQL DISTINCT](http://2sql.ru/novosti/sql-distinct/) + +> В чем заключается расширение функциональности hamcrest в нашем тесте, что нам пришлось его отдельно от JUnit прописывать? + +`hamcrest-all` используется в проверках `RootControllerTest`: `org.hamcrest.Matchers.*`. JUnit 4 включает в себя `hamcrest-core`, в JUnit 5 его нужно подключать отдельно. + +> Jackson мы просто подключаем в помнике, и Spring будет с ним работать без любых других настроек? + +Да, Spring смотрит в classpath и если видит там Jackson, то подключает интеграцию с ним. + +> Где-то слышал, что любой ресурс по REST должен однозначно идентифицироваться через url без параметров. Правильно ли задавать URL для фильтрации в виде `http://localhost/topjava/rest/meals/filter/{startDate}/{startTime}/{endDate}/{endTime}` ? + +Так делают, только при отношении +агрегация, например, если давать админу право смотреть еду любого юзера, URL мог бы быть похож на `http://localhost/topjava/rest/users/{userId}/meals/{mealId}` (не рекомендуется, см ссылку ниже). +В случае критериев поиска или страничных данных они передаются как параметр. Смотри также: + +- [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/) +- 10 Best Practices for Better RESTful +- [REST resource hierarchy (если кратко: не рекомендуется)](https://stackoverflow.com/questions/15259843/how-to-structure-rest-resource-hierarchy) + +> Что означает конструкция в `JsonUtil`: `reader.readValues(json)`; + +См. Generic Methods. Когда компилятор не может вывести тип, можно его уточнить при вызове generic метода. Неважно, +static или нет. + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW07 + +- 1: Добавить тесты контроллеров: + - 1.1 `RootControllerTest.getMeals` для `meals.jsp` + - 1.2 Сделать `ResourceControllerTest` для `style.css` (проверить `status` и `ContentType`) +- 2: Реализовать `MealRestController` и протестировать его через `MealRestControllerTest` + - 2.1 следите, чтобы url в тестах совпадал с параметрами в методе контроллера. Можно добавить логирование `` для проверки маршрутизации. + - 2.2 в параметрах `getBetween` принимать `LocalDateTime` (конвертировать через @DateTimeFormat with Java 8 + Date-Time API), пока без проверки на `null` (используя `toLocalDate()/toLocalTime()`, см. Optional п.3). В тестах передавать в формате `ISO_LOCAL_DATE_TIME` ( + например `'2011-12-03T10:15:30'`). + +### Optional + +- 3: Переделать `MealRestController.getBetween` на параметры `LocalDate/LocalTime` c раздельной фильтрацией по времени/дате, работающий при `null` значениях (см. демо и `JspMealController.getBetween`) + . Заменить `@DateTimeFormat` на свои LocalDate/LocalTime конверторы или форматтеры. + - [Spring Type Conversion and Field Formatting — пишем первый конвертер или форматтер](https://habr.com/ru/post/703402/) +- 4: Протестировать `MealRestController` (SoapUi, curl, IDEA Test RESTful Web Service, Postman). Запросы `curl` занести в отдельный `md` файл (или `README.md`) +- 5: Добавить в `AdminRestController` и `ProfileRestController` методы получения пользователя вместе с едой (`getWithMeals`, `/with-meals`). + - [Jackson – Bidirectional Relationships](https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion) + +### Optional 2 + +- 6: Сделать тесты на методы контроллеров `getWithMeals()` (п.5) + +**На следующем занятии используется JavaScript/jQuery. Если у вас там пробелы, пройдите его основы** + +--------------------- + +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Типичные ошибки и подсказки по реализации + +- 1: Ошибка в тесте _Invalid read array from JSON_ обычно расшифровывается немного ниже: читайте внимательно. +- 2: Jackson и неизменяемые объекты (для сериализации MealTo) +- 3: Если у meal, приходящий в контроллер, поля `null`, проверьте `@RequestBody` перед параметром (данные приходят в формате JSON) +- 4: При проблемах с собственным форматтером убедитесь, что в конфигурации `Topjava + +## Материалы занятия + +- **Браузер кэширует javascript и css. Если изменения не работают, обновите приложение в браузере (в хроме `Ctrl+F5`)** +- **При удалении файлов не забывайте делать clean: `mvn clean`** + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW7 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. [HW7](https://drive.google.com/file/d/1h6wg2V9yZoNX7fA7mNA7w7Kxp8IACsIJ) + +
+ Краткое содержание + +#### Тесты ResourceController +Прежде всего в настройках логирования для класса `ExceptionHandlerExceptionResolver` +установим уровень "debug". Теперь в логах мы сможем увидеть запросы, у которых проблемы с маппингом. +Чтобы протестировать доступ к ресурсам, создадим `ResourceControllerTest` с единственным тестовым методом. +Класс `MediaType` позволяет указать требуемый тип с помощью фабричного метода `valueOf`. +Начиная с [Spring 4.3 ожидаемый тип ответа нужно сравнивать с помощью `contentTypeCompatibleWith`](https://github.com/spring-projects/spring-framework/issues/19041), а не `contentType` +(в этом случае кодировка UTF-8 в типе ответа не учитывается в сравнении). + +#### Тесты для RootController на еду +Для `RootController` тесты на еду делаем точно так же, как и на `User`, с небольшим отличием. +Так как `MealTo` - это транспортный объект, который не является Entity и не находится под управлением +JPA, у него нет ограничений по методам `equals / hashCode`, и мы можем +добавить свои (сгенерировать с помощью IDEA). Теперь в тестах объекты `MealTo` мы можем сравнивать +через `equals()`. +Чтобы убедиться что два списка `MealTo` - ожидаемый, и полученный от контроллера, сравниваются поэлементно +через `equals`, мы можем установить в сравнении брекпоинт и запустить тест в режиме дебага. + +#### Реализовать MealRestController +`MealRestController` реализуем аналогично контроллерам пользователей. +В метод `MealRestController#getBetween` с параметрами запроса нужно передать +время и дату начала и конца диапазона, для которого будет найдена еда. Это можно сделать с помощью аннотации `@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)`. +Spring автоматически конвертирует параметры запроса в объекты типа `LocalDateTime`. + +В `MealRestControllerTest` нужно обратить внимание на тесты для методов `getAll` и `getBetween` контроллера, так как они возвращают список `MealTo`, а не `Meal`. +Поэтому для сравнения списков еды создадим отдельный `TO_MATCHER` с помощью статического фабричного метода `usingEqualsComparator(MealTo.class)`: +``` +public static MatcherFactory.Matcher TO_MATCHER = MatcherFactory.usingEqualsComparator(MealTo.class) +``` +Он будет сравнивать `MealTo` уже не рекурсивно, а с помощью `MealTo#equals()` — сравнения в методах `assertMatch` переделал с использованием реализаций интерфейса `BiConsumer`: +*assertion* и *iterableAssertion*. Получается очень гибко (привет, паттерн "стратегия"): для создания матчера мы можем использовать любые собственные реализации сравнений. + +Для того чтобы для тестов создать объекты `MealTo`, используем утилитный метод `MealsUtil#createTo`, изменив у него модификатор доступа на *public*. + +Для некоторых методов с переменным количеством аргументов IDEA сообщает о небезопасности типов. Чтобы подавить эти +предупреждения, над методами у нас стоят аннотации `@SafeVarargs` (для использования этой аннотации метод должен быть `final`). + +Чтобы Jackson мог сериализовать/десериализовать объекты `MealTo`, нам нужно сделать для этого класса сеттеры, или создать конструктор, помеченный специальной аннотацией `@ConstructorProperties`, +в параметры которой передаем поля объекта json, соответствующие аргументам конструктора. + +
+ + + +#### Apply 8_01_HW07_controller_test.patch + +- [Persistent classes implementing equals and hashcode](https://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html_single/#persistent-classes-equalshashcode): переопределять `equals()/hashCode()` + необходимо, если + - использовать Entity в `Set` (рекомендовано для many-ассоциаций) либо как ключи в `HashMap` + - использовать _reattachment of detached instances_ (т.е. манипулировать одним Entity в нескольких транзакциях/сессиях). +- Оптимально использовать уникальные неизменяемые бизнес-поля, но обычно таких нет, и чаще всего используется суррогатный PK с ограничением, что он может быть `null` у новых объектов и нельзя объекты сравнивать + через `equals` в бизнес-логике (например, в тестах). +- [Equals() and hashcode() when using JPA and Hibernate](https://stackoverflow.com/questions/1638723) + +------------------------ + +#### Apply 8_02_HW07_rest_controller.patch +> - В `MealTo` вместо изменяемых полей и конструктора без параметров сделал [`@ConstructorProperties`](https://www.logicbig.com/tutorials/misc/jackson/constructor-properties.html). `Immutable` классы + всегда предпочтительнее для данных. +- [Паттерн стратегия](https://refactoring.guru/ru/design-patterns/strategy). + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW7 Optional + +
+ Краткое содержание + +#### Собственный Spring-конвертер (форматтер) для даты и времени +Spring фраймворк с помощью встроенных конвертеров (реализующих интерфейс `org.springframework.core.convert.converter.Converter`) и форматтеров (интерфейс `org.springframework.format.Formatter`) делает автоматическое преобразование параметров запроса из одного типа в другой. +В нашем случае параметры фильтрации еды - дата и время - по REST приходят в виде строки, и мы можем добавить свой конвертер или форматтер, чтобы он автоматически приводил их к нужному нам типу. +> - Конвертер Spring преобразует объект одного типа в объект другого типа +> - Форматер преобразует объект типа String в объект нужного типа (при этом может поддерживать локаль) + +Сделаем собственные форматтеры для преобразования строки в дату и время `DateTimeFormatters`, добавим в `spring-mvc.xml` бин `conversionService` с перечнем наших форматтеров и сделаем на него ссылку: +``` + +``` +`LocalTimeFormatter` и `LocalDateFormatter` - наши кастомные форматтеры, которые будут парсить строку параметра. Для этого они должны реализовывать +интерфейс `Formatter<Целевой тип>` и переопределять его методы `#parse` и `#print`. Теперь мы можем убрать аннотации `@DateTimeFormat` из аргументов `MealRestController#getBetween`. `conversionService` будет +искать среди форматеров или конвертеров те, которые смогли бы преобразовать параметр-строку в объект соответствующего типа, объявленный в методе контроллера, и в результате будут использованы наши кастомные форматеры. +Для новой реализации метода `getBetween` теперь создадим несколько тестов - с различным набором параметров (в том числе и с пустыми параметрами). + +#### Протестировать сервисы с помощью SoapUI +Помимо SoapUI, для тестирования REST можно использовать команду *curl* через *Git Bash* (этот способ имеет свои недостатки - не поддерживается UTF8). +Для запросов требуется указывать Content-Type, иначе контроллер не сможет обработать запрос. +Также популярными средствами тестирования REST являются *Postman* и в IDEA: *Tools->HTTP Client*. +> Для тестирования REST у вас должен быть запущен Tomcat с вашим приложением! + +
+ + +#### Apply 8_03_HW07_formatters.patch + +> - Перенес форматтеры в подпакет `web`, т.к. они используются Spring MVC +> - Заменил `@RequestParam(required = false)` на `@RequestParam @Nullable` + +#### Apply 8_04_HW07_soapui_curl.patch + +> Добавил примеры запросов curl в `config/curl.md` + +- Написание HTTP-запросов с помощью Curl (для Windows можно использовать Git Bash) +- В IDEA появился отличный инструмент тестирования запросов. Для конвертации + в [Tools->HTTP Client->Test RESTful Web Service](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html) скопируйте curl без флага `-s` + +### Внимание! curl команды, требуемые в ТЗ к выпускному проекту, сделайте в `readme.md`, НЕ НАДО делать в выпускном проекте отдельный `curl.md`. + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. [HW7 Optional: getWithMeals + тесты](https://drive.google.com/file/d/13cjenXzWDr52HTTzleomOd-yjPAEAbOA) + +
+ Краткое содержание + +В нашем приложении у `Meal` есть ссылка на `User`, а в `User` есть ссылка на коллекцию `Meal`. +Таким образом, мы имеем дело с *BiDirectional* циклической зависимостью. При сериализации через Jackson у нас возникнут проблемы, так как он перейдет в +бесконечный цикл при переходе по ссылкам сущностей друг на друга. +Возможно следующее разрешение циклических зависимостей: + +- над полем `Meal.user` добавить аннотацию `@JsonBackReference`, теперь для еды это поле не будет сериализоваться в json; +- над коллекцией `User.meals` добавить аннотацию `@JsonManagedReference`, поле будет сериализоваться. + +Теперь для получения пользователя с едой в методах контроллера можно просто вызвать соответствующий метод сервиса. + +Для новой функциональности создадим дополнительные тесты. В тестовых данных для пользователей заполним поля *meals*. +Чтобы сразу проверять пользователя вместе с его едой, создадим дополнительный `UserTestData.WITH_MEALS_MATCHER`, который будет сравнивать сущности с помощью переданных ему интерфейсов сравнения. +Коллекции пользователей с едой мы не реализуем, поэтому `iterableAssertion` также делать не нужно, бросаем `UnsupportedOperationException`. + +Так как метод получения пользователя с едой у нас реализован только в профиле datajpa, в тестах перед выполнением метода нужно проверить, что текущий профиль Spring - `dataJpa`, тесты будут пропускаться для других профилей. +Такую функциональность мы ранее уже реализовывали - внедряем в тестовый класс `Environment` и проверяем активный профиль с помощью `Assumptions#assumeTrue`. + +
+ +#### Apply 8_05_HW07_with_meals.patch +#### Apply 8_06_HW07_test_with_meals.patch +> Изменения в AssertJ: `ignoringAllOverriddenEquals` для рекурсивных сравнений не нужен. См. [overridden equals used before 3.17.0](https://assertj.github.io/doc/#assertj-core-recursive-comparison-ignoring-equals) + +## Занятие 8: + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. WebJars. jQuery and JavaScript frameworks + +
+ Краткое содержание + +**WebJars** — библиотеки на стороне клиента (JavaScript библиотека и/или CSS модуль), упакованные в JAR. + +Добавим в наш проект в `pom.xml` дополнительные зависимости - библиотеки JavaScript и css: +- *jQuery* - самая распространенная утилитная JavaScript-библиотека; +- *Bootstrap* - фреймворк CSS-стилей; +- *Datatables* - плагин для отрисовки таблиц; +- *datetimepicker* - плагин для работы с датой и временем; +- *noty* - для работы с уведомлениями; + +
+ +#### Apply 8_07_webjars.patch + +> - Обновил jQuery до 3.x, Bootstrap до 4.x + > - Новое в jQuery 3 +> - УБРАЛ из проекта Dandelion обертку к Datatables + > - не встречал нигде, кроме Spring Pet Clinic; + > - поддержка работы с Datatables через Dandelion оказалось гораздо более трудоемкой, чем работа с плагином напрямую. +> - Исключил ненужные зависимости + +- Подключение веб-ресурсов. WebJars. +- Introducing WebJars +- Document Object Model (DOM) +- What is the DOM? +- jQuery +- Is jQuery a javascript library or framework +- DataTables +- Working with jQuery DataTables + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. [Bootstrap](https://drive.google.com/file/d/1RHtzw8OQt6guCu6xe3apT7F9EfiX96tr) + +
+ Краткое содержание + +Front-end нашего приложения будет строиться на основе фреймворка Bootstrap. +> В новой версии Bootstrap 5 из зависимостей исключена библиотека jQuery, и весь необходимый функционал Bootstrap делается на простом JavaScript. Однако JQuery нам нужна для *Datatables* и плагинов, поэтому не стал переходить на 5-ю версию. + +По ссылке [Bootstrap Examples](https://getbootstrap.com/docs/4.6/examples/) приведены примеры сайтов на Bootstrap. Из перечня уже готовых шаблонов можно выбрать +подходящий шаблон, скопировать из его исходного кода стили, форматирование и использовать в своем проекте. +- В `spring-mvc.xml` мы должны явно указать маппинг на *WebJars*-ресурсы, с которыми будет работать приложение: +````xml + +```` +- В `headTag.jsp`, который у нас сейчас добавляется через `jsp:include` в начало каждой JSP страницы, подтягиваем из *WebJars* нужные нам *css*-ресурсы и иконку для нашего приложения. +- Для отрисовывания стандартных иконок подключается ресурс ``. + В класс иконок `.fa` добавим `cursor: pointer` - это курсор-рука, который обычно используется для кнопок. +- В стили добавим sticky-footer - это footer, который будет включаться в конце JSP-страниц и приклеиваться к нижней части экрана. +- JSP-страницу со списком пользователей оформим с использованием элементов Bootstrap и добавим иконки на кнопки. +- на странице `index.jsp` форму выбора пользователя поместим в класс Bootstrap *jumbotron* - крупный выносной элемент с большим текстом и большими отступами +- таблицей пользователей в `users.jsp` поместим в аналогичный элемент *jumbotron* +
+ +#### Apply 8_08_bootstrap4.patch + +> - [WIKI Bootstrap](https://ru.wikipedia.org/wiki/Bootstrap_(фреймворк)) +> - Добавил Font Awesome + > - [Map glyphicon icons to font-awesome](https://gist.github.com/blowsie/15f8fe303383e361958bd53ecb7294f9) +> - В `headTag.jsp` в ссылку на `style.css` добавил `?v=2`. Стили изменились, изменяя версию в параметре мы заставляем браузер не брать их из кэша, а загружать заново. + +- [Bootstrap](https://getbootstrap.com/) + - [Navbar](https://getbootstrap.com/docs/4.1/components/navbar/) + - [Spacing](https://getbootstrap.com/docs/4.1/utilities/spacing/) + - [Forms](https://getbootstrap.com/docs/4.1/components/forms/) + - [Sticky footer](https://getbootstrap.com/docs/4.1/examples/sticky-footer/) +- [Документация Bootstrap на русском](https://bootstrap-4.ru/) +- Дополнительно + - Twitter Bootstrap Tutorial + - Видеоуроки Bootstrap 4 + - [Bootstrap верстка современного сайта за 45 минут](https://www.youtube.com/watch?v=46q2eB7xvXA) + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы + +> А где реально этот путь "classpath:/META-INF/resources/webjars"? + +Внутри подключаемых webjars ресурсы лежат по пути `/META-INF/resources/webjars/...` Не поленитесь посмотреть на них через `Ctrl+Shift+N`. Все подключаемые jar попадают в classpath, и ресурсы доступны +по этому пути. + +> У меня webjars-зависимость лежит внутри ".m2\repository\org\webjars\". С чем это может быть связано? + +Maven скачивает все зависимости в local repository, который по умолчанию находится в `~/.m2`. Каталог по умолчанию можно переопределить в `APACHE-MAVEN-HOME\conf\settings.xml`, +элемент `localRepository`. + +> WEBJARS лежат вообще в другом месте WEB-INF\lib\. Биндим mapping="/webjars/*" на реальное положение jar в war-e, откуда Spring знает, где искать наш jQuery? + +В war в `WEB-INF/lib/*` лежат все jar, которые попадают к classpath. Spring при обращении по url `/webjars/` ищет по пути +биндинга `` +по всему classpath (то же самое, как распаковать все jar в один каталог) в `META-INF/resources/webjars/`. В этом месте во всех jar, которые мы подключили из webjars, лежат наши ресурсы. + +> Оптимально ли делать доступ к статическим ресурсам (css, js, html) через webjars ? + +На продакшене под нагрузкой статические ресурсы лучше всего держать не в war, а снаружи. Доступ к ним делается либо +через конфигурирование Tomcat. +Но чаще всего для доступа к статике ставят прокси, например Nginx + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. AJAX. Datatables. jQuery + +
+ Краткое содержание + +**AJAX** (асинхронный JavaScript и XML) — подход к построению интерактивных пользовательских интерфейсов веб-приложений, заключающийся в "фоновом" обмене данными браузера с веб-сервером. + +#### AdminUIController +У нас будут отдельные от REST UI-контроллеры, так как в них будут отличаться обработка исключений, некоторая логика и авторизация. +В `AdminUIController` метод `#create` будет использоваться как для создания, так и для обновления пользователя в зависимости от значения `id`. + +#### Список пользователей +Оформляем таблицу пользователей с помощью js/css библиотеки `Datatables`. Таблица должна иметь id (в нашем случае "datatable"), чтобы к ней можно было обращаться. +Также на страницу добавляем форму, с помощью которой будем редактировать и добавлять пользователей. +Форма имеет скрытое поле `id`, которое будет использоваться в наших js-скриптах. + +#### topjava.users.js +> Код по сравнению с видео изменился! Про изменения я говорю в конце видео и перечислил их после *Краткого содержания* + +Для работы AJAX объявляем переменные: +- *ajaxUrl* - адрес нужного endpoint контроллера +- *datatableApi* - объект таблицы `datatable` + +Страница html имеет определенный жизненный цикл, в процессе которого с ней совершаются какие-то действия. +Одно из таких действий - загрузка, после которого мы можем производить какие-то манипуляции на странице. +С помощью jQuery мы определяем коллбэк-метод, который будет вызываться после загрузки страницы: +``` +$(function () { + ... +``` +Строчка +``` +datatableApi = $("#datatable").DataTable( +``` +преобразует HTML-элемент c *id=datatable* в javaScript-объект с помощью метода `DataTable` библиотеки *Datatables*. +Параметр этого метода - объект-конфигурация, который задает опции отображения таблицы и в "columns" задает соответствие колонок таблицы полям приходящего с сервера JSON-объекта пользователей. +Внизу конфигурации добавляется сортировка таблицы по первому столбцу. +После этого вызывается метод `makeEditable()` (он находится в `topjava.common.js`). + +#### topjava.common.js + +- В `makeEditable` к событию *click* всех объектов HTML c классом *delete* привязываем вызов метода `deleteRow`. Параметром берем аттрибут `id` текущего элемента `$(this)`. + +- Метод `add` вызывается из `users.jsp` по нажатию на кнопку "Добавить": `onclick="add()"`. В нем + - обнуляются все поля `input` формы `detailsForm`: `$("#detailsForm").find(":input").val("")` + - вызывается входящий в Bootstrap метод `modal()`, который преобразует HTML-элемент `id=editRow` в модальное окно. [Botstrap4 Modal](https://getbootstrap.com/docs/4.6/components/modal) + +- В методе `deleteRow` делаем AJAX-запросы к серверу и по после их успешного выполнения вызываем обновление таблицы. + +- В `updateTable` по AJAX запрашиваем с сервера массив пользователей, в случае успеха очищаем таблицу и заполняем ее данными, полученными с сервера. + +- В `save` средствами jQuery сериализуем форму `id=detailsForm` в JSON-объект и методом POST отдаем эти данные. После успешного выполнения запроса закрываем модальное окно и обновляем таблицу. + +Intellij IDEA предоставляет нам возможность дебага кода JavaScript. См. видео для примера. + +#### Загрузка HTML +По умолчанию при стандартной загрузке страницы с js-скриптами браузер будет: +- Парсить нужную HTML-страницу; +- Как только браузер сталкивается с тегом `` и сохранить.** + +- XSS для новичков +- XSS глазами злоумышленника + +Раньше я [реализовывал XSS защиту через `@SafeHtml`](https://stackoverflow.com/a/40644276/548473), пока его не [удалили из hibernate validator](https://hibernate.org/validator/documentation/migration-guide/). +Пришлось сделать собственную аннотацию `@NoHtml` на основе [Sanitizing User Input](https://thoughtfulsoftware.wordpress.com/2013/05/26/sanitizing-user-input-part-ii-validation-with-spring-rest/) + и [jsoup - Sanitize HTML](https://www.tutorialspoint.com/jsoup/jsoup_sanitize_html.htm) +Все классы, относящиеся к валидации перенес в пакет `ru.javawebinar.topjava.util.validation` +- `password` проверять не надо, т.к. он не выводится в html, а [email надо](https://stackoverflow.com/questions/17480809) +- Сделать общий интерфейс валидации `View.Web` и `@Validated(View.Web.class)` вместо `@Valid` для проверки содержимого только на входе UI/REST. +При сохранении в базу проверка на безопасный html контент (XSS) повторно не делается. +- [Validation groups in Spring MVC](https://blog.codeleak.pl/2014/08/validation-groups-in-spring-mvc.html) + +#### Apply 11_12_XSS.patch + +### Swagger2 + +Swagger это фреймворк для автоматического создания REST-API документации по аннотациям контроллеров Spring MVC. +Подключим зависимость `springfox-swagger2` и `springfox-swagger-ui` в `pom.xml`. +Сразу же в проект подключается Swagger UI интерфейс, который позволяет отправлять запросы к эндпоинтам REST-API и просматривать документацию. + +Настройка swagger производится в конфигурации `spring-mvc.xml` подключением бина `Swagger2DocumentationConfiguration`. +Чтобы смотреть REST-API документацию мог любой пользователь, в `spring-security.xml` доступ к эндпоинтам Swagger UI открываем всем: +```xml + + + +``` +`AuthorizedUser authUser` не является реальным параметром методов контроллера, который передается клиентом. +Это авторизированный пользователь, который резолвится Spring Security через `@AuthenticationPrincipal`. +Убираем его из документации запросов через `@ApiIgnore`: Swagger будет игнорировать такие параметры при генерировании документации. +UI контроллеры также исключаем из REST-API, пометив их `@ApiIgnore` на уровне класса. + +Создадим на `login.jsp` кнопку "Swagger REST Api Documentation". + +**Внимание: Swagger подключается в проект ОЧЕНЬ просто, а пользу от него для ревью трудно переоценить. Вместо примеров `curl` в выпускных проектах +предлагаю вам подключить Swagger и в `readme.md` дать ссылку на сгенерированную REST API документацию.** + +- [Setting Up Swagger 2 with a Spring REST API](https://www.baeldung.com/swagger-2-documentation-for-spring-rest-api) +- [Swagger 2 Configuration With Spring (XML)](https://medium.com/@andreymamontov/swagger-2-configuration-with-spring-xml-3cd643a12425) +- [Hiding Endpoints From Swagger Documentation](https://www.baeldung.com/spring-swagger-hiding-endpoints) +> В версиях выше 2.10 и 3.0 появились проблемы с маппингом. Вариант документации c OpenAPI 3.0 смотрите в [Spring Boot курсе](https://javaops.ru/view/bootjava) + +#### Apply 11_13_swagger2.patch + +--------------------- +### HW Optional 2 + +Обратите внимание в Swagger UI на `Example Value` при +- `POST /rest/admin/users (createWithLocation)` - здесь не должно быть поля `meals`. Пользователь создается без еды, еда управляется своими запросами. +- `POST /rest/profile/meals (createWithLocation)` - здесь не должно быть поля `user`. +Нужно поправит `Example Value`. При этом учтите, что в API у нас есть метод +`GET /rest/admin/users/{id}/with-meals (getWithMeals)` - вернуть пользователя с едой. + +[Hide a Request Field in Swagger API](https://www.baeldung.com/spring-swagger-hide-field) + +----------------------- + +Платформа Heroku стала платной, вместо нее мы предлагаем зарегистрировать и задеплоиться на собственный выделенный хостинг. +Тарифы хостингов сейчас самые демократичные: собственный Ubuntu сервер с доступом по ssh: от 130 руб./месяц. + +### Ограничение модификации пользователей +Наше [демо-приложение](http://javaops-demo.ru/topjava) доступно любому и нам нужно защитить стандартные учетные записи User и Admin от попыток их +модификации. Сделаем новый профиль `VDS` и в `UserService` введем флаг `modificationRestriction` - нужна ли нам такая проверка. +Через `Environment` проверяем активный профиль и для профиля `VDS` устанавливаем флаг в *true*. +В методах, изменяющих пользователя, проверяем этот флаг и `id` изменяемой сущности, и, попытке несанкционированных изменений, бросаем `UpdateRestrictionException`. +Отнаследовал это исключение от `ApplicationException` - универсального исключения нашего приложения, в котором можно задавать тип и код локализации ошибки. +В `GlobalExceptionHandler` и `ExceptionInfoHandler` создаем обработчики `ApplicationException`. +Для тестирования исключения при попытке изменение пользователя и админа для профиля `VDS` делаем `VdsRestControllerTest`: +задаем профиль запуска `@ActiveProfiles(VDS)`, делаем модификацию и проверяем исключение. + +#### Apply 11_14_vds_restrict_modification.patch + + - В `UserService` добавил защиту от изменения `Admin/User` для профиля `VDS` (в `UserService` заинжектил `Environment` и сделал проверку на наличие профиля `VDS`) + - **В выпускном проекте, если только не выставляете его в облако для показа, это НЕ требуется**. + - Чтобы тесты были рабочими, ввел профиль `VDS`, работающий так же, как и `POSTGRES`. + - Добавил универсальный `ApplicationException` для работы с ошибками с поддержкой i18n в приложении (от него отнаследовал `UpdateRestrictionException`) + +> Для тестирования с профилем vds добавьте в VM options: `-Dspring.profiles.active=datajpa,vds` + +### Деплой приложения на Выделенный сервер + +Наше [демо-приложение](http://javaops-demo.ru/topjava) теперь хостится на FirstVDS (SSD диск вместо NVMe) +Первое открытое занятие [курса Startup](https://javaops.ru/view/startup) обучает регистрации хостинга, установке ПО и деплою приложения + +### [Открытое занятие курса Startup](https://github.com/JavaOPs/startup) + +#### Apply 11_15_vds.patch +Файл пропертей с паролями не принято держать в репозитории (даже если репозиторий _private_). +Поэтому вам необязательно комитить его, просто скопируйте на сервер по инструкции в разделе урока "_Деплой war на сервер_" + +### HW Optional 2 Solution: + +#### Apply [11_16_HW_fix_swagger.patch](https://drive.google.com/file/d/1A76XXvZdZCKxeKnVjZ2VkrWAHEQ1iof2) +- Скрываем необязательные поле `id` при POST и PUT запросах через `@ApiModelProperty(hidden = true)` в примерах запроса Swagger, при этом передавать поле в запросе можно: [be conservative when you reply, but accept liberally](https://stackoverflow.com/a/28025008/548473) +- `Meal.user` отсутствует в REST API, можно игнорировать: `@JsonIgnore` +- `User.meals` можно было сделать `JsonProperty.Access.READ_ONLY`, но при этом не пройдут тесты `getWithMeals` (maels не будет сериализоваться из ответа сервера для сравнения). Скрыл также через `@ApiModelProperty(hidden = true)` +- Также можно было скрыть нулевое поле `User.meals` при выводе через `@JsonInclude(JsonInclude.Include.NON_EMPTY)`. Но при этом поле исчезнет при запросе `getWithMeals` пользователя с пустым списком еды (например для Guest). Все зависит от бизнес-требований приложения (например насколько API публично и должно быть красивым). Можете попробовать самостоятельно скрыть это поле из вывода для запросов без еды через `View` (или отдельный TO). +---------------------- + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) Собеседование. Разработка ПО +- [Темы/ресурсы тестового собеседования](http://javaops.ru/interview/test.html) +- [Составление резюме, подготовка к интервью, поиск работы](https://github.com/JavaOPs/topjava/blob/master/cv.md) +- [Слайды](https://docs.google.com/presentation/d/18o__IGRqYadi4jx2wX2rX6AChHh-KrxktD8xI7bS33k), [Книги](http://javaops.ru/view/books) +- [Jenkins/Hudson: что такое и для чего он нужен](https://habrahabr.ru/post/334730/) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) [Вебинар: Составление резюме и поиск работы в IT](https://www.facebook.com/watch/live/?v=2789025168007756) +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) Разбор типовых собеседований (необработанный вебинар) +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) Вебинар выпускников + +----------------------- + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее Задание: +### **Опционально: [зарегистрировать хостинг и задеплоить в него свое приложение](https://github.com/JavaOPs/startup)** + +### **Пройдите основы Spring Boot по курсу [BootJava](https://javaops.ru/view/bootjava)** +- **Занятие по миграция на BootJava будет в начале следующей недели** + +### **[Выполнить выпускной проект](https://github.com/JavaWebinar/topjava/blob/doc/doc/graduation.md)** + - Сроки сдачи и форма заполнения указаны в выпускном. + - Если проверки или Диплома нет, заполнять не нужно. + - **Возможно [доплатить за ревью выпускного отдельно из JavaOPs профиля](https://javaops.ru/auth/payonline?payId=OG), как за тестовое собеседование: 3950р** + +### **Сделать / обновить резюме (отдать на ревью в канал #lesson11 группы slack)** +- **Вставь ссылку на свой сертификат [из личного профиля](http://javaops.ru/auth/profile#finished), немного досрочно:)** + - [Загрузка сайта на GitHub. Бесплатный хостинг и домен.](https://www.youtube.com/watch?v=adaZZd73VGU) + - [CSS theme for hosting your personal site, blog, or portfolio](https://mademistakes.com/work/minimal-mistakes-jekyll-theme/) + +#### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Замечания по резюме: + - **если нет опыта в IT, обязательно вставь [участие в стажировке Topjava](https://github.com/JavaOPs/topjava/blob/master/cv.md#Позиционирование-проекта-topjava). Весь не-IT опыт можно кратко.** + - варианты размещения: Pdf в любом облаке, [Google Doc](https://docs.google.com/), LinkedIn, HH, [еще варианты и рекомендации](https://github.com/JavaOPs/topjava/blob/master/cv.md#составление-резюме) +Хорошо, если будет в html или pdf формате (например в https://pages.github.com/). [Например так](https://gkislin.github.io/), [на github](https://github.com/gkislin/gkislin.github.io/blob/master/index.html). Возраст и день рождения писать не обязательно + - [все упоминания Junior убрать!!](https://vk.com/javawebinar?w=wall-58538268_1589) + - линки делай кликабельными (если формат поддерживает) + - всю выгодную для себя информацию (и важную для HR) распологайте вверху. Название секций в резюме и их порядок относительно стандартный и важный + - **Резюме на hh или других ресурсах ДОЛЖНО БЫТЬ ОТКРЫТО ДЛЯ ПРОСМОТРА и иметь телефон для связи** + - Заполните контакты `skype/telegram/whatsapp`, HR ими пользуется! Почта как контакт очень медленная, телефон может быть не всегда удобен. Вообще `skype/telegram` для программиста - **Must have**. + - **Добавьте в резюме ссылки на свои проекты в `GitHub` и на задеплоенные на ваш сервер. Не забудьте про выпускной!**. + - Диплом РФ от Виакадемии о [профессиональной переподготовке](https://ru.wikipedia.org/wiki/Профессиональная_переподготовка) приравнивается ко второму высшему образованию. В резюме, полагаю, можно указать в высшем образовании + - Заполнить в [своем профиле Java Online Projects](http://javaops.ru/auth/profileER) ссылку на резюме и информацию по поиску работы (если конечно актуально): резюме, флаги рассматриваю работу, готов к релокации и информация для HR. + - **Рассылку обновления базы соискателей по HR буду делать в конце января, можно не спешить** + +### **После ревью резюме - опубликовать на ресурсах IT вакансий** + - [Основные сайты поиска работы](https://github.com/JavaOPs/topjava/blob/master/cv.md#основные-сайты-поиска-работы) + +### **Получить первое открытое занятие МНОГОПОТОЧНОСТЬ и пройти эту важную тему в [проекте Masterjava](http://javaops.ru/view/masterjava)** + - Обучение на MasterJava идет в индивидуальном режиме без проверки ДЗ: старт в любой момент, время прохождение ничем не ограничено + - Проект, патчи, группа Slack, занятия и видео, разбор ДЗ аналогичны проекту Topjava. + - **До 15.01 на курс MasterJava для выпускников специальная цена.** + - **Действует акция: при покупке MasterJava курс [Работа с документами в Java (DocJava)](https://javaops.ru/view/docjava) в подарок!** + +### **Моя реализация выпускного проекта [TopJava-2](http://javaops.ru/view/topjava2)** + - Так же, как и разбор домашних заданий, финальный разбор моей реализации выпускного проекта очень важен. + - Есть выгодные пакеты: TopJava-2 + полное ревью выпускного проекта + курс MasterJava + - **До 15.01 курс TopJava2 и все пакеты по минимальной цене!** + - Оплатить TopJava-2 можно сейчас по специальной цене, а активировать доступ уже после ревью вашего выпускного + +### **[Полное ревью выпускного проекта](https://javaops.ru/auth/payonline?payId=OG)** +- Ревью выпускного проекта входит в стоимость проверок. Если у вас их не было, вы много потеряли. +- Но еще можно приобрести полное ревью выпускного проекта: + - Получить 1-е ревью от наших кураторов стажировки: стиль, архитектура, API, структура, кодирование + - Исправить замечания (срок - до нескольких недель) + - Получить финальное ревью проекта от Григория Кислина + - Получить [сертификат с отличием](https://javaops.ru/certificate/topjava?email=admin@javaops.ru) и попасть [на доску почета](https://javaops.ru/bestTopjava):) + +#### Возможные доработки приложения: +- Разделить `Meal.dateTime` на `date` и `time` и выполнять запрос целиком в SQL +- Для редактирования паролей сделать отдельный интерфейс с запросом старого пароля и кнопку сброса пароля для администратора. +- Добавление и удаление ролей для пользователей в админке. +- Перевести приложение на Spring Boot (а шаблоны с JSP на Thymeleaf) +- Сделать авторизацию REST по JWT (см. открытый урок [CloudJava](https://javaops.ru/view/cloudjava)) +- Перевести UI на Angular / Vaadin elements /GWT /GXT /Vaadin / ZK/ [Ваш любимый фреймворк].. +- Сделать авторизацию в приложение по OAuth 2.0 (Spring Security OAuth, +VK auth, github oauth, ...) +- Сделать отображение еды постранично, с поиском и сортировкой на стороне сервера. +- Сделать desktop/mobile приложение, работающее по REST с нашим приложением. +- Показ ошибок в модальном окне редактирования таблицы так же, как и в JSP профиля +- Limit login attempts example + +#### Доработки участников прошлых выпусков: +- [Авторизация в приложение по OAuth2 через GitHub, ветка oauth](https://github.com/rblik/topjava/tree/oauth) +- [Авторизация в приложение по OAuth2 через GitHub/Facebook/Google](https://github.com/jacksn/topjava) +- Angular 2 UI + - [tutorial по доработке](https://github.com/Gwulior/article/blob/master/article.md) + - [ветка angular2 в гитхабе](https://github.com/12ozCode/topjava08-to-angular2/tree/angular2) +- Отдельный фронтэнд на Angular 2, который работает по REST с авторизацией по JWT + - [ветка development фронтэнда](https://github.com/evgeniycheban/topjava-frontend/tree/development) + - [ветка development бэкэнда](https://github.com/evgeniycheban/topjava/tree/development) + - в JWT токенен приложение topjava передает email, name и роль admin как boolean true/false, +на клиенте он декодируется и из него получается auth-user, с которым уже работает фронтэнд + +### Жду твою доработку из списка! + +### Ресурсы по Проекту +- Уроки Bootstrap 4 +- Spring at tutorialspoint +- Articles in Spring +- Learn Spring on Baeldung +- Spring Framework + Reference Documentation +- Hibernate Documentation +- Java Course (книга 2) +- Справочник «Паттерны проектирования» +- Catalog of Patterns of Enterprise Application Architecture diff --git a/lessons/lesson12.md b/lessons/lesson12.md new file mode 100644 index 00000000000..d782c6a1db1 --- /dev/null +++ b/lessons/lesson12.md @@ -0,0 +1,45 @@ +# Стажировка Topjava + +## [Патчи занятия](https://drive.google.com/drive/u/1/folders/1sizknxR29Yu7XXjaVIBdS88ffXiVuqiB) + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Финальные правки: + +Один из вариантов сокрытия полей в примерах Swagger - сделать специальный TO класс. Но можно сделать проще через специальные аннотации: [Hide a Request Field in Swagger API](https://www.baeldung.com/spring-swagger-hide-field) +- Скрываем необязательные поле `id` при POST и PUT запросах через `@ApiModelProperty(hidden = true)` в примерах запроса Swagger. При этом передавать поле в запросе можно. +- `Meal.user` отсутствует в REST API, можно игнорировать: `@JsonIgnore` +- `User.meals` можно было сделать `JsonProperty.Access.READ_ONLY`, но при этом не пройдут тесты `getWithMeals` (maels не будет сериализоваться из ответа сервера для сравнения). Скрыл также через `@ApiModelProperty(hidden = true)` +- Также можно было скрыть нулевое поле `User.meals` при выводе через `@JsonInclude(JsonInclude.Include.NON_EMPTY)`. Но при этом поле исчезнет при запросе `getWithMeals` пользователя с пустым списком еды (например для Guest). Все зависит от бизнес-требований приложения (например насколько API публично и должно быть красивым). Можете попробовать самостоятельно скрыть это поле из вывода для запросов без еды через `View` (или отдельный TO). + +#### Apply [11_16_HW_fix_swagger.patch](https://drive.google.com/file/d/1A76XXvZdZCKxeKnVjZ2VkrWAHEQ1iof2) + +## Миграция на Spring Boot +За основу взят **[финальный код проекта BootJava с миграцией на Spring Boot 3.2, 8-й урок](https://javaops.ru/view/bootjava/lesson08)** +Вычекайте в отдельную папку (как отдельный проект) ветку `spring_boot` нашего проекта (так удобнее, не придется постоянно переключаться между ветками): +``` +git clone --branch spring_boot --single-branch https://github.com/JavaWebinar/topjava.git topjava_boot +``` +Если будете его менять, [настройте `git remote`](https://javaops.ru/view/bootjava/lesson01#project) +> Если захотите сами накатить патчи, сделайте ветку `spring_boot` от первого `init` и в корне **создайте каталог `src\test`** + +---- + +#### Apply 12_1_init_boot_java +Оставил как в TopJava название приложения `Calories Management` и имя базы `topjava` + +#### Apply 12_2_add_calories_meals + +Добавил из TopJava: +- Еду, калории +- Таблицы назвал в единственном числе: `user_role, meal` (кроме `users`, _user_ зарезервированное слово) +- Общие вещи (пусть небольшие) вынес в сервис : `MealService` +- Проверку принадлежности еды делаю в `MealRepository.getBelonged` +- Вместо своих конверторов использую `@DateTimeFormat` +- Обратите внимание на `UserRepository.getWithMeals` - он не работает с `@EntityGraph`. Зато работает с обычным `JOIN FETCH` и `DISTINCT` больше не нужен: + - [forget about DISTINCT](https://vladmihalcea.com/spring-6-migration/#Auto-deduplication) + - [it is no longer necessary to use distinct in JPQL and HQL](https://docs.jboss.org/hibernate/orm/6.0/migration-guide/migration-guide.html#query-sqm-distinct) +- Мигрировал все тесты контроллеров. В выпускном проекте столько тестов необязательно! Достаточно нескольких, на основные юзкейсы. +- Кэширование в выпускном желательно. 7 раз подумайте, что будете кэшировать! **Максимально просто, самые частые запросы, которые редко изменяются**. +- **Добавьте в свой выпускной OpenApi/Swagger - это будет большим плюсом и избавит от необходимости писать документацию**. + +### За основу выпускного предлагаю взять этот код миграции, сделав свой выпускной МАКСИМАЛЬНО в этом стиле. +### Успехов с выпускным проектом и в карьере! diff --git a/pom.xml b/pom.xml index 0b1c2896da5..94fcec73e99 100644 --- a/pom.xml +++ b/pom.xml @@ -4,41 +4,229 @@ ru.javawebinar topjava - jar + war 1.0-SNAPSHOT Calories Management - http://topjava.herokuapp.com/ + https://javaops-demo.ru/topjava - 1.8 + 17 UTF-8 UTF-8 + 5.3.32 + 2.7.18 + 9.0.86 + + + 5.6.15.Final + 6.2.5.Final + 3.0.1-b12 + + + 3.10.8 + + + 1.2.13 + 1.7.36 + + + 42.7.1 + + 4.13.2 + 3.25.3 topjava - install + package + + org.apache.maven.plugins + maven-war-plugin + 3.4.0 + org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.12.1 ${java.version} ${java.version} + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + -Dfile.encoding=UTF-8 + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + runtime + + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + + org.springframework + spring-context-support + + + org.springframework.data + spring-data-jpa + ${spring-data-jpa.version} + + + + + org.hibernate + hibernate-core + ${hibernate.version} + + + org.hibernate.validator + hibernate-validator + ${hibernate-validator.version} + + + + + org.glassfish + javax.el + ${javax-el.version} + provided + + + + + javax.cache + cache-api + 1.1.0 + + + org.ehcache + ehcache + runtime + ${ehcache.version} + + + org.glassfish.jaxb + jaxb-runtime + + + + + + + org.glassfish.jaxb + jaxb-runtime + 2.4.0-b180830.0438 + + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + + javax.servlet + jstl + 1.2 + + + + + junit + junit + ${junit.version} + test + + + org.springframework + spring-test + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + hsqldb + + + org.hsqldb + hsqldb + 2.3.4 + + + + + + postgres + + + org.postgresql + postgresql + ${postgresql.version} + + + org.apache.tomcat + tomcat-jdbc + ${tomcat.version} + provided + + + org.slf4j + jul-to-slf4j + ${slf4j.version} + runtime + + + + true + + + + + org.springframework + spring-framework-bom + ${spring.version} + pom + import + + diff --git a/src/main/java/ru/javawebinar/topjava/Main.java b/src/main/java/ru/javawebinar/topjava/Main.java deleted file mode 100644 index c2f9cc618f7..00000000000 --- a/src/main/java/ru/javawebinar/topjava/Main.java +++ /dev/null @@ -1,11 +0,0 @@ -package ru.javawebinar.topjava; - -/** - * @see Demo application - * @see Initial project - */ -public class Main { - public static void main(String[] args) { - System.out.format("Hello TopJava Enterprise!"); - } -} diff --git a/src/main/java/ru/javawebinar/topjava/Profiles.java b/src/main/java/ru/javawebinar/topjava/Profiles.java new file mode 100644 index 00000000000..e4111a2afd2 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/Profiles.java @@ -0,0 +1,27 @@ +package ru.javawebinar.topjava; + +import org.springframework.util.ClassUtils; + +public class Profiles { + public static final String + JDBC = "jdbc", + JPA = "jpa", + DATAJPA = "datajpa"; + + public static final String REPOSITORY_IMPLEMENTATION = DATAJPA; + + public static final String + POSTGRES_DB = "postgres", + HSQL_DB = "hsqldb"; + + // Get DB profile depending of DB driver in classpath + public static String getActiveDbProfile() { + if (ClassUtils.isPresent("org.postgresql.Driver", null)) { + return POSTGRES_DB; + } else if (ClassUtils.isPresent("org.hsqldb.jdbcDriver", null)) { + return HSQL_DB; + } else { + throw new IllegalStateException("Could not find DB driver"); + } + } +} diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java new file mode 100644 index 00000000000..1d96e59271f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -0,0 +1,66 @@ +package ru.javawebinar.topjava.model; + +import org.springframework.data.domain.Persistable; +import org.springframework.util.Assert; + +import javax.persistence.*; + +import static ru.javawebinar.topjava.util.Util.getEffectiveClass; + +@MappedSuperclass +// http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access +@Access(AccessType.FIELD) +public abstract class AbstractBaseEntity implements Persistable { + public static final int START_SEQ = 100000; + + @Id + @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1, initialValue = START_SEQ) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq") +// See https://hibernate.atlassian.net/browse/HHH-3718 and https://hibernate.atlassian.net/browse/HHH-12034 +// Proxy initialization when accessing its identifier managed now by JPA_PROXY_COMPLIANCE setting + protected Integer id; + + protected AbstractBaseEntity() { + } + + protected AbstractBaseEntity(Integer id) { + this.id = id; + } + + public void setId(Integer id) { + this.id = id; + } + + @Override + public Integer getId() { + return id; + } + + public int id() { + Assert.notNull(id, "Entity must have id"); + return id; + } + + @Override + public boolean isNew() { + return this.id == null; + } + + @Override + public String toString() { + return getClass().getSimpleName() + ":" + id; + } + + // https://jpa-buddy.com/blog/hopefully-the-final-article-about-equals-and-hashcode-for-jpa-entities-with-db-generated-ids/ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getEffectiveClass(this) != getEffectiveClass(o)) return false; + return getId() != null && getId().equals(((AbstractBaseEntity) o).getId()); + } + + @Override + public final int hashCode() { + return getEffectiveClass(this).hashCode(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java new file mode 100644 index 00000000000..0b32aac584f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java @@ -0,0 +1,37 @@ +package ru.javawebinar.topjava.model; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + + +@MappedSuperclass +public abstract class AbstractNamedEntity extends AbstractBaseEntity { + + @NotBlank + @Size(min = 2, max = 128) + @Column(name = "name", nullable = false) + protected String name; + + protected AbstractNamedEntity() { + } + + protected AbstractNamedEntity(Integer id, String name) { + super(id); + this.name = name; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public String toString() { + return super.toString() + '(' + name + ')'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java new file mode 100644 index 00000000000..fd7c5daee7e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -0,0 +1,111 @@ +package ru.javawebinar.topjava.model; + +import org.hibernate.validator.constraints.Range; + +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +@NamedQueries({ + @NamedQuery(name = Meal.ALL_SORTED, query = "SELECT m FROM Meal m WHERE m.user.id=:userId ORDER BY m.dateTime DESC"), + @NamedQuery(name = Meal.DELETE, query = "DELETE FROM Meal m WHERE m.id=:id AND m.user.id=:userId"), + @NamedQuery(name = Meal.GET_BETWEEN, query = """ + SELECT m FROM Meal m + WHERE m.user.id=:userId AND m.dateTime >= :startDateTime AND m.dateTime < :endDateTime ORDER BY m.dateTime DESC + """), +// @NamedQuery(name = Meal.UPDATE, query = "UPDATE Meal m SET m.dateTime = :datetime, m.calories= :calories," + +// "m.description=:desc where m.id=:id and m.user.id=:userId") +}) +@Entity +@Table(name = "meal", uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "date_time"}, name = "meal_unique_user_datetime_idx")}) +public class Meal extends AbstractBaseEntity { + public static final String ALL_SORTED = "Meal.getAll"; + public static final String DELETE = "Meal.delete"; + public static final String GET_BETWEEN = "Meal.getBetween"; + + @Column(name = "date_time", nullable = false) + @NotNull + private LocalDateTime dateTime; + + @Column(name = "description", nullable = false) + @NotBlank + @Size(min = 2, max = 120) + private String description; + + @Column(name = "calories", nullable = false) + @Range(min = 10, max = 5000) + private int calories; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @NotNull + private User user; + + public Meal() { + } + + public Meal(LocalDateTime dateTime, String description, int calories) { + this(null, dateTime, description, calories); + } + + public Meal(Integer id, LocalDateTime dateTime, String description, int calories) { + super(id); + this.dateTime = dateTime; + this.description = description; + this.calories = calories; + } + + public LocalDateTime getDateTime() { + return dateTime; + } + + public String getDescription() { + return description; + } + + public int getCalories() { + return calories; + } + + public LocalDate getDate() { + return dateTime.toLocalDate(); + } + + public LocalTime getTime() { + return dateTime.toLocalTime(); + } + + public void setDateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setCalories(int calories) { + this.calories = calories; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + @Override + public String toString() { + return "Meal{" + + "id=" + id + + ", dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + '}'; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/model/Role.java b/src/main/java/ru/javawebinar/topjava/model/Role.java new file mode 100644 index 00000000000..acb7a276f6d --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/Role.java @@ -0,0 +1,6 @@ +package ru.javawebinar.topjava.model; + +public enum Role { + USER, + ADMIN +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java new file mode 100644 index 00000000000..3a9d4523c7b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -0,0 +1,145 @@ +package ru.javawebinar.topjava.model; + +import org.hibernate.validator.constraints.Range; +import org.springframework.util.CollectionUtils; + +import javax.persistence.*; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.*; + +import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; + +@NamedQueries({ + @NamedQuery(name = User.DELETE, query = "DELETE FROM User u WHERE u.id=:id"), + @NamedQuery(name = User.BY_EMAIL, query = "SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email=?1"), + @NamedQuery(name = User.ALL_SORTED, query = "SELECT u FROM User u LEFT JOIN FETCH u.roles ORDER BY u.name, u.email"), +}) +@Entity +@Table(name = "users") +public class User extends AbstractNamedEntity { + + public static final String DELETE = "User.delete"; + public static final String BY_EMAIL = "User.getByEmail"; + public static final String ALL_SORTED = "User.getAllSorted"; + + @Column(name = "email", nullable = false, unique = true) + @Email + @NotBlank + @Size(max = 128) + private String email; + + @Column(name = "password", nullable = false) + @NotBlank + @Size(min = 5, max = 128) + private String password; + + @Column(name = "enabled", nullable = false, columnDefinition = "bool default true") + private boolean enabled = true; + + @Column(name = "registered", nullable = false, columnDefinition = "timestamp default now()", updatable = false) + @NotNull + private Date registered = new Date(); + + @Enumerated(EnumType.STRING) + @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), + uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "uk_user_role")}) + @Column(name = "role") + @ElementCollection(fetch = FetchType.EAGER) + private Set roles; + + @Column(name = "calories_per_day", nullable = false, columnDefinition = "int default 2000") + @Range(min = 10, max = 10000) + private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; + + @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) + @OrderBy("dateTime") + private List meals; + + public User() { + } + + public User(User u) { + this(u.id, u.name, u.email, u.password, u.caloriesPerDay, u.enabled, u.registered, u.roles); + } + + public User(Integer id, String name, String email, String password, Role... roles) { + this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, new Date(), List.of(roles)); + } + + public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Date registered, Collection roles) { + super(id, name); + this.email = email; + this.password = password; + this.caloriesPerDay = caloriesPerDay; + this.enabled = enabled; + this.registered = registered; + setRoles(roles); + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setPassword(String password) { + this.password = password; + } + + public Date getRegistered() { + return registered; + } + + public void setRegistered(Date registered) { + this.registered = registered; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getCaloriesPerDay() { + return caloriesPerDay; + } + + public void setCaloriesPerDay(int caloriesPerDay) { + this.caloriesPerDay = caloriesPerDay; + } + + public boolean isEnabled() { + return enabled; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Collection roles) { + this.roles = CollectionUtils.isEmpty(roles) ? EnumSet.noneOf(Role.class) : EnumSet.copyOf(roles); + } + + public String getPassword() { + return password; + } + + public List getMeals() { + return meals; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", email=" + email + + ", name=" + name + + ", enabled=" + enabled + + ", roles=" + roles + + ", caloriesPerDay=" + caloriesPerDay + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java new file mode 100644 index 00000000000..9461d5f9f69 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -0,0 +1,23 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.Meal; + +import java.time.LocalDateTime; +import java.util.List; + +public interface MealRepository { + // null if updated meal does not belong to userId + Meal save(Meal meal, int userId); + + // false if meal does not belong to userId + boolean delete(int id, int userId); + + // null if meal does not belong to userId + Meal get(int id, int userId); + + // ORDERED dateTime desc + List getAll(int userId); + + // ORDERED dateTime desc + List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId); +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java new file mode 100644 index 00000000000..1b094a6fd53 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java @@ -0,0 +1,23 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +public interface UserRepository { + // null if not found, when updated + User save(User user); + + // false if not found + boolean delete(int id); + + // null if not found + User get(int id); + + // null if not found + User getByEmail(String email); + + List getAll(); + + User getByIdWithMeals(int id); +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java new file mode 100644 index 00000000000..7c7f93209ab --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java @@ -0,0 +1,26 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.Meal; + +import java.time.LocalDateTime; +import java.util.List; + +@Transactional(readOnly = true) +public interface CrudMealRepository extends JpaRepository { + @Transactional + @Modifying + @Query("DELETE FROM Meal m WHERE m.id=:id AND m.user.id=:userId") + int delete(@Param("id") int id, @Param("userId") int userId); + + Meal getByIdAndUserId(@Param("id") int id, @Param("userId") int userId); + + @Query("SELECT m FROM Meal m WHERE m.user.id=:userId AND m.dateTime >= :startDateTime AND m.dateTime < :endDateTime ORDER BY m.dateTime DESC") + List getBetweenHalfOpen(@Param("startDateTime") LocalDateTime startDateTime, @Param("endDateTime") LocalDateTime endDateTime, @Param("userId") int userId); + + List getByUserIdOrderByDateTimeDesc(@Param("userId") int userId); +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java new file mode 100644 index 00000000000..f4af460a3fb --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java @@ -0,0 +1,22 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.User; + +@Transactional(readOnly = true) +public interface CrudUserRepository extends JpaRepository { + @Transactional + @Modifying +// @Query(name = User.DELETE) + @Query("DELETE FROM User u WHERE u.id=:id") + int delete(@Param("id") int id); + + User getByEmail(String email); + + @Query("SELECT u FROM User u LEFT JOIN FETCH u.meals WHERE u.id=:id") + User getByIdWithMeals(@Param("id")int id); +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java new file mode 100644 index 00000000000..b46cd1c6dc8 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java @@ -0,0 +1,49 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public class DataJpaMealRepository implements MealRepository { + + private final CrudMealRepository crudRepository; + private final CrudUserRepository crudUserRepository; + + public DataJpaMealRepository(CrudMealRepository crudRepository, CrudUserRepository crudUserRepository) { + this.crudRepository = crudRepository; + this.crudUserRepository = crudUserRepository; + } + + @Override + public Meal save(Meal meal, int userId) { + if (meal.isNew() || get(meal.id(), userId) != null) { + meal.setUser(crudUserRepository.getReferenceById(userId)); + return crudRepository.save(meal); + } + return null; + } + + @Override + public boolean delete(int id, int userId) { + return crudRepository.delete(id, userId) != 0; + } + + @Override + public Meal get(int id, int userId) { + return crudRepository.getByIdAndUserId(id, userId); + } + + @Override + public List getAll(int userId) { + return crudRepository.getByUserIdOrderByDateTimeDesc(userId); + } + + @Override + public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return crudRepository.getBetweenHalfOpen(startDateTime, endDateTime, userId); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java new file mode 100644 index 00000000000..2710c1f44f4 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java @@ -0,0 +1,48 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.List; + +@Repository +public class DataJpaUserRepository implements UserRepository { + private static final Sort SORT_NAME_EMAIL = Sort.by(Sort.Direction.ASC, "name", "email"); + + private final CrudUserRepository crudRepository; + + public DataJpaUserRepository(CrudUserRepository crudRepository) { + this.crudRepository = crudRepository; + } + + @Override + public User save(User user) { + return crudRepository.save(user); + } + + @Override + public boolean delete(int id) { + return crudRepository.delete(id) != 0; + } + + @Override + public User get(int id) { + return crudRepository.findById(id).orElse(null); + } + + @Override + public User getByEmail(String email) { + return crudRepository.getByEmail(email); + } + + @Override + public List getAll() { + return crudRepository.findAll(SORT_NAME_EMAIL); + } + + public User getByIdWithMeals(int id) { + return crudRepository.getByIdWithMeals(id); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java new file mode 100644 index 00000000000..9c2a240bbbe --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -0,0 +1,86 @@ +package ru.javawebinar.topjava.repository.jdbc; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public class JdbcMealRepository implements MealRepository { + + private static final RowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(Meal.class); + + private final JdbcTemplate jdbcTemplate; + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private final SimpleJdbcInsert insertMeal; + + @Autowired + public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + this.insertMeal = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("meal") + .usingGeneratedKeyColumns("id"); + + this.jdbcTemplate = jdbcTemplate; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } + + @Override + public Meal save(Meal meal, int userId) { + MapSqlParameterSource map = new MapSqlParameterSource() + .addValue("id", meal.getId()) + .addValue("description", meal.getDescription()) + .addValue("calories", meal.getCalories()) + .addValue("date_time", meal.getDateTime()) + .addValue("user_id", userId); + + if (meal.isNew()) { + Number newId = insertMeal.executeAndReturnKey(map); + meal.setId(newId.intValue()); + } else { + if (namedParameterJdbcTemplate.update("" + + "UPDATE meal " + + " SET description=:description, calories=:calories, date_time=:date_time " + + " WHERE id=:id AND user_id=:user_id", map) == 0) { + return null; + } + } + return meal; + } + + @Override + public boolean delete(int id, int userId) { + return jdbcTemplate.update("DELETE FROM meal WHERE id=? AND user_id=?", id, userId) != 0; + } + + @Override + public Meal get(int id, int userId) { + List meals = jdbcTemplate.query( + "SELECT * FROM meal WHERE id = ? AND user_id = ?", ROW_MAPPER, id, userId); + return DataAccessUtils.singleResult(meals); + } + + @Override + public List getAll(int userId) { + return jdbcTemplate.query( + "SELECT * FROM meal WHERE user_id=? ORDER BY date_time DESC", ROW_MAPPER, userId); + } + + @Override + public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return jdbcTemplate.query( + "SELECT * FROM meal WHERE user_id=? AND date_time >= ? AND date_time < ? ORDER BY date_time DESC", + ROW_MAPPER, userId, startDateTime, endDateTime); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java new file mode 100644 index 00000000000..7ba4ee2d64d --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java @@ -0,0 +1,80 @@ +package ru.javawebinar.topjava.repository.jdbc; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.List; + +@Repository +public class JdbcUserRepository implements UserRepository { + + private static final BeanPropertyRowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(User.class); + + private final JdbcTemplate jdbcTemplate; + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private final SimpleJdbcInsert insertUser; + + @Autowired + public JdbcUserRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + this.insertUser = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("users") + .usingGeneratedKeyColumns("id"); + + this.jdbcTemplate = jdbcTemplate; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } + + @Override + public User save(User user) { + BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(user); + + if (user.isNew()) { + Number newKey = insertUser.executeAndReturnKey(parameterSource); + user.setId(newKey.intValue()); + } else if (namedParameterJdbcTemplate.update(""" + UPDATE users SET name=:name, email=:email, password=:password, + registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id + """, parameterSource) == 0) { + return null; + } + return user; + } + + @Override + public boolean delete(int id) { + return jdbcTemplate.update("DELETE FROM users WHERE id=?", id) != 0; + } + + @Override + public User get(int id) { + List users = jdbcTemplate.query("SELECT * FROM users WHERE id=?", ROW_MAPPER, id); + return DataAccessUtils.singleResult(users); + } + + @Override + public User getByEmail(String email) { +// return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email=?", ROW_MAPPER, email); + List users = jdbcTemplate.query("SELECT * FROM users WHERE email=?", ROW_MAPPER, email); + return DataAccessUtils.singleResult(users); + } + + @Override + public List getAll() { + return jdbcTemplate.query("SELECT * FROM users ORDER BY name, email", ROW_MAPPER); + } + + @Override + public User getByIdWithMeals(int id) { + return null; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java new file mode 100644 index 00000000000..30226da774a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java @@ -0,0 +1,62 @@ +package ru.javawebinar.topjava.repository.jpa; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.MealRepository; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.time.LocalDateTime; +import java.util.List; + +@Repository +@Transactional(readOnly = true) +public class JpaMealRepository implements MealRepository { + + @PersistenceContext + private EntityManager em; + + @Override + @Transactional + public Meal save(Meal meal, int userId) { + meal.setUser(em.getReference(User.class, userId)); + if (meal.isNew()) { + em.persist(meal); + return meal; + } + return get(meal.getId(), userId) == null ? null : em.merge(meal); + } + + @Override + @Transactional + public boolean delete(int id, int userId) { + return em.createNamedQuery(Meal.DELETE) + .setParameter("id", id) + .setParameter("userId", userId) + .executeUpdate() != 0; + } + + @Override + public Meal get(int id, int userId) { + Meal meal = em.find(Meal.class, id); + return meal != null && meal.getUser().getId() == userId ? meal : null; + } + + @Override + public List getAll(int userId) { + return em.createNamedQuery(Meal.ALL_SORTED, Meal.class) + .setParameter("userId", userId) + .getResultList(); + } + + @Override + public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return em.createNamedQuery(Meal.GET_BETWEEN, Meal.class) + .setParameter("userId", userId) + .setParameter("startDateTime", startDateTime) + .setParameter("endDateTime", endDateTime) + .getResultList(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java new file mode 100644 index 00000000000..f2948a71201 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java @@ -0,0 +1,78 @@ +package ru.javawebinar.topjava.repository.jpa; + +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.util.List; + +@Repository +@Transactional(readOnly = true) +public class JpaUserRepository implements UserRepository { + +/* + @Autowired + private SessionFactory sessionFactory; + + private Session openSession() { + return sessionFactory.getCurrentSession(); + } +*/ + + @PersistenceContext + private EntityManager em; + + @Override + @Transactional + public User save(User user) { + if (user.isNew()) { + em.persist(user); + return user; + } else { + return em.merge(user); + } + } + + @Override + public User get(int id) { + return em.find(User.class, id); + } + + @Override + @Transactional + public boolean delete(int id) { + +/* User ref = em.getReference(User.class, id); + em.remove(ref); + + Query query = em.createQuery("DELETE FROM User u WHERE u.id=:id"); + return query.setParameter("id", id).executeUpdate() != 0; +*/ + return em.createNamedQuery(User.DELETE) + .setParameter("id", id) + .executeUpdate() != 0; + } + + @Override + public User getByEmail(String email) { + List users = em.createNamedQuery(User.BY_EMAIL, User.class) + .setParameter(1, email) + .getResultList(); + return DataAccessUtils.singleResult(users); + } + + @Override + public List getAll() { + return em.createNamedQuery(User.ALL_SORTED, User.class) + .getResultList(); + } + + @Override + public User getByIdWithMeals(int id) { + return null; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java new file mode 100644 index 00000000000..df874378eb8 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -0,0 +1,50 @@ +package ru.javawebinar.topjava.service; + +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; + +import java.time.LocalDate; +import java.util.List; + +import static ru.javawebinar.topjava.util.DateTimeUtil.atStartOfDayOrMin; +import static ru.javawebinar.topjava.util.DateTimeUtil.atStartOfNextDayOrMax; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; + +@Service +public class MealService { + + private final MealRepository repository; + + public MealService(MealRepository repository) { + this.repository = repository; + } + + public Meal get(int id, int userId) { + return checkNotFoundWithId(repository.get(id, userId), id); + } + + public void delete(int id, int userId) { + checkNotFoundWithId(repository.delete(id, userId), id); + } + + public List getBetweenInclusive(@Nullable LocalDate startDate, @Nullable LocalDate endDate, int userId) { + return repository.getBetweenHalfOpen(atStartOfDayOrMin(startDate), atStartOfNextDayOrMax(endDate), userId); + } + + public List getAll(int userId) { + return repository.getAll(userId); + } + + public void update(Meal meal, int userId) { + Assert.notNull(meal, "meal must not be null"); + checkNotFoundWithId(repository.save(meal, userId), meal.id()); + } + + public Meal create(Meal meal, int userId) { + Assert.notNull(meal, "meal must not be null"); + return repository.save(meal, userId); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java new file mode 100644 index 00000000000..5fac6664f11 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -0,0 +1,58 @@ +package ru.javawebinar.topjava.service; + +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; + +@Service +public class UserService { + + private final UserRepository repository; + + public UserService(UserRepository repository) { + this.repository = repository; + } + + @CacheEvict(value = "users", allEntries = true) + public User create(User user) { + Assert.notNull(user, "user must not be null"); + return repository.save(user); + } + + @CacheEvict(value = "users", allEntries = true) + public void delete(int id) { + checkNotFoundWithId(repository.delete(id), id); + } + + public User get(int id) { + return checkNotFoundWithId(repository.get(id), id); + } + + public User getByEmail(String email) { + Assert.notNull(email, "email must not be null"); + return checkNotFound(repository.getByEmail(email), "email=" + email); + } + + @Cacheable("users") + public List getAll() { + return repository.getAll(); + } + + @CacheEvict(value = "users", allEntries = true) + public void update(User user) { + Assert.notNull(user, "user must not be null"); + checkNotFoundWithId(repository.save(user), user.id()); + } + + public User getUserWithMeals(int id) { + return repository.getByIdWithMeals(id); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/to/MealTo.java b/src/main/java/ru/javawebinar/topjava/to/MealTo.java new file mode 100644 index 00000000000..d14feae792b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/to/MealTo.java @@ -0,0 +1,54 @@ +package ru.javawebinar.topjava.to; + +import java.time.LocalDateTime; + +public class MealTo { + private final Integer id; + + private final LocalDateTime dateTime; + + private final String description; + + private final int calories; + + private final boolean excess; + + public MealTo(Integer id, LocalDateTime dateTime, String description, int calories, boolean excess) { + this.id = id; + this.dateTime = dateTime; + this.description = description; + this.calories = calories; + this.excess = excess; + } + + public Integer getId() { + return id; + } + + public LocalDateTime getDateTime() { + return dateTime; + } + + public String getDescription() { + return description; + } + + public int getCalories() { + return calories; + } + + public boolean isExcess() { + return excess; + } + + @Override + public String toString() { + return "MealTo{" + + "id=" + id + + ", dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + ", excess=" + excess + + '}'; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java new file mode 100644 index 00000000000..7c1ac9fecdb --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -0,0 +1,42 @@ +package ru.javawebinar.topjava.util; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +public class DateTimeUtil { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + // DB doesn't support LocalDate.MIN/MAX + private static final LocalDateTime MIN_DATE = LocalDateTime.of(1, 1, 1, 0, 0); + private static final LocalDateTime MAX_DATE = LocalDateTime.of(3000, 1, 1, 0, 0); + + private DateTimeUtil() { + } + + public static LocalDateTime atStartOfDayOrMin(LocalDate localDate) { + return localDate != null ? localDate.atStartOfDay() : MIN_DATE; + } + + public static LocalDateTime atStartOfNextDayOrMax(LocalDate localDate) { + return localDate != null ? localDate.plusDays(1).atStartOfDay() : MAX_DATE; + } + + public static String toString(LocalDateTime ldt) { + return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER); + } + + public static @Nullable + LocalDate parseLocalDate(@Nullable String str) { + return StringUtils.hasLength(str) ? LocalDate.parse(str) : null; + } + + public static @Nullable + LocalTime parseLocalTime(@Nullable String str) { + return StringUtils.hasLength(str) ? LocalTime.parse(str) : null; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java new file mode 100644 index 00000000000..658671b9df1 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -0,0 +1,44 @@ +package ru.javawebinar.topjava.util; + +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealTo; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class MealsUtil { + public static final int DEFAULT_CALORIES_PER_DAY = 2000; + + private MealsUtil() { + } + + public static List getTos(Collection meals, int caloriesPerDay) { + return filterByPredicate(meals, caloriesPerDay, meal -> true); + } + + public static List getFilteredTos(Collection meals, int caloriesPerDay, LocalTime startTime, LocalTime endTime) { + return filterByPredicate(meals, caloriesPerDay, meal -> Util.isBetweenHalfOpen(meal.getTime(), startTime, endTime)); + } + + private static List filterByPredicate(Collection meals, int caloriesPerDay, Predicate filter) { + Map caloriesSumByDate = meals.stream() + .collect( + Collectors.groupingBy(Meal::getDate, Collectors.summingInt(Meal::getCalories)) +// Collectors.toMap(Meal::getDate, Meal::getCalories, Integer::sum) + ); + + return meals.stream() + .filter(filter) + .map(meal -> createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) + .toList(); + } + + private static MealTo createTo(Meal meal, boolean excess) { + return new MealTo(meal.getId(), meal.getDateTime(), meal.getDescription(), meal.getCalories(), excess); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/Util.java b/src/main/java/ru/javawebinar/topjava/util/Util.java new file mode 100644 index 00000000000..3a94ab2d0b8 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/Util.java @@ -0,0 +1,19 @@ +package ru.javawebinar.topjava.util; + +import org.hibernate.proxy.HibernateProxy; +import org.springframework.lang.Nullable; + +public class Util { + + private Util() { + } + + public static > boolean isBetweenHalfOpen(T value, @Nullable T start, @Nullable T end) { + return (start == null || value.compareTo(start) >= 0) && (end == null || value.compareTo(end) < 0); + } + + public static Class getEffectiveClass(Object o) { + return o instanceof HibernateProxy ? + ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java new file mode 100644 index 00000000000..5212eea7506 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -0,0 +1,46 @@ +package ru.javawebinar.topjava.util; + + +import ru.javawebinar.topjava.model.AbstractBaseEntity; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +public class ValidationUtil { + + private ValidationUtil() { + } + + public static T checkNotFoundWithId(T object, int id) { + checkNotFoundWithId(object != null, id); + return object; + } + + public static void checkNotFoundWithId(boolean found, int id) { + checkNotFound(found, "id=" + id); + } + + public static T checkNotFound(T object, String msg) { + checkNotFound(object != null, msg); + return object; + } + + public static void checkNotFound(boolean found, String msg) { + if (!found) { + throw new NotFoundException("Not found entity with " + msg); + } + } + + public static void checkNew(AbstractBaseEntity entity) { + if (!entity.isNew()) { + throw new IllegalArgumentException(entity + " must be new (id=null)"); + } + } + + public static void assureIdConsistent(AbstractBaseEntity entity, int id) { +// conservative when you reply, but accept liberally (http://stackoverflow.com/a/32728226/548473) + if (entity.isNew()) { + entity.setId(id); + } else if (entity.id() != id) { + throw new IllegalArgumentException(entity + " must be with id=" + id); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java new file mode 100644 index 00000000000..f1e9b0e4637 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.javawebinar.topjava.util.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java new file mode 100644 index 00000000000..f35e950858d --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -0,0 +1,94 @@ +package ru.javawebinar.topjava.web; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.util.StringUtils; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.web.meal.MealRestController; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; +import java.util.Objects; + +import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate; +import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime; + +public class MealServlet extends HttpServlet { + + private ConfigurableApplicationContext springContext; + private MealRestController mealController; + + @Override + public void init() { + springContext = new ClassPathXmlApplicationContext(new String[]{"spring/spring-db.xml", "spring/spring-app.xml"}, false); + springContext.getEnvironment().setActiveProfiles("postgres", "datajpa"); + springContext.refresh(); + mealController = springContext.getBean(MealRestController.class); + } + + @Override + public void destroy() { + springContext.close(); + super.destroy(); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + request.setCharacterEncoding("UTF-8"); + Meal meal = new Meal( + LocalDateTime.parse(request.getParameter("dateTime")), + request.getParameter("description"), + Integer.parseInt(request.getParameter("calories"))); + + if (StringUtils.hasLength(request.getParameter("id"))) { + mealController.update(meal, getId(request)); + } else { + mealController.create(meal); + } + response.sendRedirect("meals"); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String action = request.getParameter("action"); + + switch (action == null ? "all" : action) { + case "delete" -> { + int id = getId(request); + mealController.delete(id); + response.sendRedirect("meals"); + } + case "create", "update" -> { + final Meal meal = "create".equals(action) ? + new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000) : + mealController.get(getId(request)); + request.setAttribute("meal", meal); + request.getRequestDispatcher("/mealForm.jsp").forward(request, response); + } + case "filter" -> { + LocalDate startDate = parseLocalDate(request.getParameter("startDate")); + LocalDate endDate = parseLocalDate(request.getParameter("endDate")); + LocalTime startTime = parseLocalTime(request.getParameter("startTime")); + LocalTime endTime = parseLocalTime(request.getParameter("endTime")); + request.setAttribute("meals", mealController.getBetween(startDate, startTime, endDate, endTime)); + request.getRequestDispatcher("/meals.jsp").forward(request, response); + } + default -> { + request.setAttribute("meals", mealController.getAll()); + request.getRequestDispatcher("/meals.jsp").forward(request, response); + } + } + } + + private int getId(HttpServletRequest request) { + String paramId = Objects.requireNonNull(request.getParameter("id")); + return Integer.parseInt(paramId); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java new file mode 100644 index 00000000000..4bad5863e3c --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java @@ -0,0 +1,25 @@ +package ru.javawebinar.topjava.web; + +import ru.javawebinar.topjava.model.AbstractBaseEntity; + +import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; + +public class SecurityUtil { + + private static int id = AbstractBaseEntity.START_SEQ; + + private SecurityUtil() { + } + + public static int authUserId() { + return id; + } + + public static void setAuthUserId(int id) { + SecurityUtil.id = id; + } + + public static int authUserCaloriesPerDay() { + return DEFAULT_CALORIES_PER_DAY; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java new file mode 100644 index 00000000000..226023400c7 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -0,0 +1,28 @@ +package ru.javawebinar.topjava.web; + +import org.slf4j.Logger; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.slf4j.LoggerFactory.getLogger; + +public class UserServlet extends HttpServlet { + private static final Logger log = getLogger(UserServlet.class); + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + int userId = Integer.parseInt(request.getParameter("userId")); + SecurityUtil.setAuthUserId(userId); + response.sendRedirect("meals"); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + log.debug("forward to users"); + request.getRequestDispatcher("/users.jsp").forward(request, response); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java new file mode 100644 index 00000000000..bbfe35e3f0f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -0,0 +1,76 @@ +package ru.javawebinar.topjava.web.meal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Controller; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.service.MealService; +import ru.javawebinar.topjava.to.MealTo; +import ru.javawebinar.topjava.util.MealsUtil; +import ru.javawebinar.topjava.web.SecurityUtil; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; + +@Controller +public class MealRestController { + private static final Logger log = LoggerFactory.getLogger(MealRestController.class); + + private final MealService service; + + public MealRestController(MealService service) { + this.service = service; + } + + public Meal get(int id) { + int userId = SecurityUtil.authUserId(); + log.info("get meal {} for user {}", id, userId); + return service.get(id, userId); + } + + public void delete(int id) { + int userId = SecurityUtil.authUserId(); + log.info("delete meal {} for user {}", id, userId); + service.delete(id, userId); + } + + public List getAll() { + int userId = SecurityUtil.authUserId(); + log.info("getAll for user {}", userId); + return MealsUtil.getTos(service.getAll(userId), SecurityUtil.authUserCaloriesPerDay()); + } + + public Meal create(Meal meal) { + int userId = SecurityUtil.authUserId(); + checkNew(meal); + log.info("create {} for user {}", meal, userId); + return service.create(meal, userId); + } + + public void update(Meal meal, int id) { + int userId = SecurityUtil.authUserId(); + assureIdConsistent(meal, id); + log.info("update {} for user {}", meal, userId); + service.update(meal, userId); + } + + /** + *
    Filter separately + *
  1. by date
  2. + *
  3. by time for every date
  4. + *
+ */ + public List getBetween(@Nullable LocalDate startDate, @Nullable LocalTime startTime, + @Nullable LocalDate endDate, @Nullable LocalTime endTime) { + int userId = SecurityUtil.authUserId(); + log.info("getBetween dates({} - {}) time({} - {}) for user {}", startDate, endDate, startTime, endTime, userId); + + List mealsDateFiltered = service.getBetweenInclusive(startDate, endDate, userId); + return MealsUtil.getFilteredTos(mealsDateFiltered, SecurityUtil.authUserCaloriesPerDay(), startTime, endTime); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java new file mode 100644 index 00000000000..0000f1c1e02 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -0,0 +1,51 @@ +package ru.javawebinar.topjava.web.user; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.UserService; + +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; + +public abstract class AbstractUserController { + protected final Logger log = LoggerFactory.getLogger(getClass()); + + @Autowired + private UserService service; + + public List getAll() { + log.info("getAll"); + return service.getAll(); + } + + public User get(int id) { + log.info("get {}", id); + return service.get(id); + } + + public User create(User user) { + log.info("create {}", user); + checkNew(user); + return service.create(user); + } + + public void delete(int id) { + log.info("delete {}", id); + service.delete(id); + } + + public void update(User user, int id) { + log.info("update {} with id={}", user, id); + assureIdConsistent(user, id); + service.update(user); + } + + public User getByMail(String email) { + log.info("getByEmail {}", email); + return service.getByEmail(email); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java new file mode 100644 index 00000000000..b37a8ed6c8a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -0,0 +1,40 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.stereotype.Controller; +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +@Controller +public class AdminRestController extends AbstractUserController { + + @Override + public List getAll() { + return super.getAll(); + } + + @Override + public User get(int id) { + return super.get(id); + } + + @Override + public User create(User user) { + return super.create(user); + } + + @Override + public void delete(int id) { + super.delete(id); + } + + @Override + public void update(User user, int id) { + super.update(user, id); + } + + @Override + public User getByMail(String email) { + return super.getByMail(email); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java new file mode 100644 index 00000000000..7d3702c31c4 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -0,0 +1,22 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.stereotype.Controller; +import ru.javawebinar.topjava.model.User; + +import static ru.javawebinar.topjava.web.SecurityUtil.authUserId; + +@Controller +public class ProfileRestController extends AbstractUserController { + + public User get() { + return super.get(authUserId()); + } + + public void delete() { + super.delete(authUserId()); + } + + public void update(User user) { + super.update(user, authUserId()); + } +} \ No newline at end of file diff --git a/src/main/resources/cache/ehcache.xml b/src/main/resources/cache/ehcache.xml new file mode 100644 index 00000000000..05589f71f06 --- /dev/null +++ b/src/main/resources/cache/ehcache.xml @@ -0,0 +1,25 @@ + + + + + + + + + + 5 + + 5000 + + + + + + + 1 + + + + diff --git a/src/main/resources/db/hsqldb.properties b/src/main/resources/db/hsqldb.properties new file mode 100644 index 00000000000..17c03ef4ebd --- /dev/null +++ b/src/main/resources/db/hsqldb.properties @@ -0,0 +1,11 @@ +#database.url=jdbc:hsqldb:file:D:/temp/topjava + +database.url=jdbc:hsqldb:mem:topjava +database.username=sa +database.password= + +database.init=true +jdbc.initLocation=classpath:db/initDB_hsql.sql +jpa.showSql=true +hibernate.format_sql=true +hibernate.use_sql_comments=true \ No newline at end of file diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql new file mode 100644 index 00000000000..4bf3d8446ac --- /dev/null +++ b/src/main/resources/db/initDB.sql @@ -0,0 +1,37 @@ +DROP TABLE IF EXISTS user_role; +DROP TABLE IF EXISTS meal; +DROP TABLE IF EXISTS users; +DROP SEQUENCE IF EXISTS global_seq; + +CREATE SEQUENCE global_seq START WITH 100000; + +CREATE TABLE users +( + id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'), + name VARCHAR NOT NULL, + email VARCHAR NOT NULL, + password VARCHAR NOT NULL, + registered TIMESTAMP DEFAULT now() NOT NULL, + enabled BOOL DEFAULT TRUE NOT NULL, + calories_per_day INTEGER DEFAULT 2000 NOT NULL +); +CREATE UNIQUE INDEX users_unique_email_idx ON users (email); + +CREATE TABLE user_role +( + user_id INTEGER NOT NULL, + role VARCHAR NOT NULL, + CONSTRAINT user_roles_idx UNIQUE (user_id, role), + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TABLE meal +( + id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'), + user_id INTEGER NOT NULL, + date_time TIMESTAMP NOT NULL, + description TEXT NOT NULL, + calories INT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX meal_unique_user_datetime_idx ON meal (user_id, date_time); \ No newline at end of file diff --git a/src/main/resources/db/initDB_hsql.sql b/src/main/resources/db/initDB_hsql.sql new file mode 100644 index 00000000000..9e0e195e600 --- /dev/null +++ b/src/main/resources/db/initDB_hsql.sql @@ -0,0 +1,39 @@ +DROP TABLE user_role IF EXISTS; +DROP TABLE meal IF EXISTS; +DROP TABLE users IF EXISTS; +DROP SEQUENCE global_seq IF EXISTS; + +CREATE SEQUENCE GLOBAL_SEQ AS INTEGER START WITH 100000; + +CREATE TABLE users +( + id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + registered TIMESTAMP DEFAULT now() NOT NULL, + enabled BOOLEAN DEFAULT TRUE NOT NULL, + calories_per_day INTEGER DEFAULT 2000 NOT NULL +); +CREATE UNIQUE INDEX users_unique_email_idx + ON USERS (email); + +CREATE TABLE user_role +( + user_id INTEGER NOT NULL, + role VARCHAR(255) NOT NULL, + CONSTRAINT user_roles_idx UNIQUE (user_id, role), + FOREIGN KEY (user_id) REFERENCES USERS (id) ON DELETE CASCADE +); + +CREATE TABLE meal +( + id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY, + date_time TIMESTAMP NOT NULL, + description VARCHAR(255) NOT NULL, + calories INT NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES USERS (id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX meal_unique_user_datetime_idx + ON meal (user_id, date_time) \ No newline at end of file diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql new file mode 100644 index 00000000000..5021dc6f13b --- /dev/null +++ b/src/main/resources/db/populateDB.sql @@ -0,0 +1,24 @@ +DELETE FROM user_role; +DELETE FROM meal; +DELETE FROM users; +ALTER SEQUENCE global_seq RESTART WITH 100000; + +INSERT INTO users (name, email, password) +VALUES ('User', 'user@yandex.ru', 'password'), + ('Admin', 'admin@gmail.com', 'admin'), + ('Guest', 'guest@gmail.com', 'guest'); + +INSERT INTO user_role (role, user_id) +VALUES ('USER', 100000), + ('ADMIN', 100001); + +INSERT INTO meal (date_time, description, calories, user_id) +VALUES ('2020-01-30 10:00:00', 'Завтрак', 500, 100000), + ('2020-01-30 13:00:00', 'Обед', 1000, 100000), + ('2020-01-30 20:00:00', 'Ужин', 500, 100000), + ('2020-01-31 0:00:00', 'Еда на граничное значение', 100, 100000), + ('2020-01-31 10:00:00', 'Завтрак', 500, 100000), + ('2020-01-31 13:00:00', 'Обед', 1000, 100000), + ('2020-01-31 20:00:00', 'Ужин', 510, 100000), + ('2020-01-31 14:00:00', 'Админ ланч', 510, 100001), + ('2020-01-31 21:00:00', 'Админ ужин', 1500, 100001); diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties new file mode 100644 index 00000000000..75d7d68730a --- /dev/null +++ b/src/main/resources/db/postgres.properties @@ -0,0 +1,9 @@ +database.url=jdbc:postgresql://localhost:5432/topjava +database.username=user +database.password=password + +database.init=true +jdbc.initLocation=classpath:db/initDB.sql +jpa.showSql=true +hibernate.format_sql=true +hibernate.use_sql_comments=true \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 00000000000..c0335c928eb --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,30 @@ + + + + + + + + ${TOPJAVA_ROOT}/log/topjava.log + + + UTF-8 + %date %-5level %logger{50}.%M:%L - %msg%n + + + + + + UTF-8 + %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml new file mode 100644 index 00000000000..d6c643e972e --- /dev/null +++ b/src/main/resources/spring/spring-app.xml @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/spring-cache.xml b/src/main/resources/spring/spring-cache.xml new file mode 100644 index 00000000000..73325fee065 --- /dev/null +++ b/src/main/resources/spring/spring-cache.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml new file mode 100644 index 00000000000..5337c1d1ce2 --- /dev/null +++ b/src/main/resources/spring/spring-db.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/tld/functions.tld b/src/main/webapp/WEB-INF/tld/functions.tld new file mode 100644 index 00000000000..d138fecdbfb --- /dev/null +++ b/src/main/webapp/WEB-INF/tld/functions.tld @@ -0,0 +1,16 @@ + + + + 1.0 + functions + http://topjava.javawebinar.ru/functions + + + formatDateTime + ru.javawebinar.topjava.util.DateTimeUtil + java.lang.String toString(java.time.LocalDateTime) + + diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..0ac5a7610ed --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,28 @@ + + + TopJava + + + userServlet + ru.javawebinar.topjava.web.UserServlet + 0 + + + userServlet + /users + + + + mealServlet + ru.javawebinar.topjava.web.MealServlet + 0 + + + mealServlet + /meals + + diff --git a/src/main/webapp/css/style.css b/src/main/webapp/css/style.css new file mode 100644 index 00000000000..cfffdcb5335 --- /dev/null +++ b/src/main/webapp/css/style.css @@ -0,0 +1,24 @@ +dl { + background: none repeat scroll 0 0 #FAFAFA; + margin: 8px 0; + padding: 0; +} + +dt { + display: inline-block; + width: 170px; +} + +dd { + display: inline-block; + margin-left: 8px; + vertical-align: top; +} + +tr[data-meal-excess="false"] { + color: green; +} + +tr[data-meal-excess="true"] { + color: red; +} diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html new file mode 100644 index 00000000000..57d710a81ab --- /dev/null +++ b/src/main/webapp/index.html @@ -0,0 +1,18 @@ + + + + Java Enterprise (Topjava) + + +

Проект Java Enterprise (Topjava)

+
+
+ Meals of  + + +
+ + diff --git a/src/main/webapp/mealForm.jsp b/src/main/webapp/mealForm.jsp new file mode 100644 index 00000000000..98a6f48735d --- /dev/null +++ b/src/main/webapp/mealForm.jsp @@ -0,0 +1,34 @@ +<%@ page contentType="text/html;charset=UTF-8" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + Meal + + + +
+

Home

+
+

${param.action == 'create' ? 'Create meal' : 'Edit meal'}

+ +
+ +
+
DateTime:
+
+
+
+
Description:
+
+
+
+
Calories:
+
+
+ + +
+
+ + diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/meals.jsp new file mode 100644 index 00000000000..7d9bf3e425b --- /dev/null +++ b/src/main/webapp/meals.jsp @@ -0,0 +1,66 @@ +<%@ page contentType="text/html;charset=UTF-8" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="fn" uri="http://topjava.javawebinar.ru/functions" %> + + + Meals + + + +
+

Home

+
+

Meals

+
+ +
+
From Date (inclusive):
+
+
+
+
To Date (inclusive):
+
+
+
+
From Time (inclusive):
+
+
+
+
To Time (exclusive):
+
+
+ +
+
+ Add Meal +

+ + + + + + + + + + + + + + + + + + + + +
DateDescriptionCalories
+ <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%> + <%--<%=TimeUtil.toString(meal.getDateTime())%>--%> + <%--${fn:replace(meal.dateTime, 'T', ' ')}--%> + ${fn:formatDateTime(meal.dateTime)} + ${meal.description}${meal.calories}UpdateDelete
+
+ + \ No newline at end of file diff --git a/src/main/webapp/users.jsp b/src/main/webapp/users.jsp new file mode 100644 index 00000000000..650c8dda479 --- /dev/null +++ b/src/main/webapp/users.jsp @@ -0,0 +1,11 @@ +<%@ page contentType="text/html;charset=UTF-8" %> + + + Users + + +

Home

+
+

Users

+ + \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java new file mode 100644 index 00000000000..43f143cc766 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java @@ -0,0 +1,19 @@ +package ru.javawebinar.topjava; + +import org.springframework.lang.NonNull; +import org.springframework.test.context.support.DefaultActiveProfilesResolver; + +import java.util.Arrays; + +//http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver +public class ActiveDbProfileResolver extends DefaultActiveProfilesResolver { + @Override + public @NonNull + String[] resolve(@NonNull Class aClass) { + // https://stackoverflow.com/a/52438829/548473 + String[] activeProfiles = super.resolve(aClass); + String[] activeProfilesWithDb = Arrays.copyOf(activeProfiles, activeProfiles.length + 1); + activeProfilesWithDb[activeProfiles.length] = Profiles.getActiveDbProfile(); + return activeProfilesWithDb; + } +} diff --git a/src/test/java/ru/javawebinar/topjava/MatcherFactory.java b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java new file mode 100644 index 00000000000..c5f04086a35 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java @@ -0,0 +1,37 @@ +package ru.javawebinar.topjava; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Factory for creating test matchers. + *

+ * Comparing actual and expected objects via AssertJ + */ +public class MatcherFactory { + public static Matcher usingIgnoringFieldsComparator(String... fieldsToIgnore) { + return new Matcher<>(fieldsToIgnore); + } + + public static class Matcher { + private final String[] fieldsToIgnore; + + private Matcher(String... fieldsToIgnore) { + this.fieldsToIgnore = fieldsToIgnore; + } + + public void assertMatch(T actual, T expected) { + assertThat(actual).usingRecursiveComparison().ignoringFields(fieldsToIgnore).isEqualTo(expected); + } + + @SafeVarargs + public final void assertMatch(Iterable actual, T... expected) { + assertMatch(actual, List.of(expected)); + } + + public void assertMatch(Iterable actual, Iterable expected) { + assertThat(actual).usingRecursiveFieldByFieldElementComparatorIgnoringFields(fieldsToIgnore).isEqualTo(expected); + } + } +} diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java new file mode 100644 index 00000000000..d044e3f90a2 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -0,0 +1,38 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.model.Meal; + +import java.time.Month; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import static java.time.LocalDateTime.of; +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + +public class MealTestData { + public static final MatcherFactory.Matcher MEAL_MATCHER = MatcherFactory.usingIgnoringFieldsComparator("user"); + + public static final int NOT_FOUND = 10; + public static final int MEAL1_ID = START_SEQ + 3; + public static final int ADMIN_MEAL_ID = START_SEQ + 10; + + public static final Meal meal1 = new Meal(MEAL1_ID, of(2020, Month.JANUARY, 30, 10, 0), "Завтрак", 500); + public static final Meal meal2 = new Meal(MEAL1_ID + 1, of(2020, Month.JANUARY, 30, 13, 0), "Обед", 1000); + public static final Meal meal3 = new Meal(MEAL1_ID + 2, of(2020, Month.JANUARY, 30, 20, 0), "Ужин", 500); + public static final Meal meal4 = new Meal(MEAL1_ID + 3, of(2020, Month.JANUARY, 31, 0, 0), "Еда на граничное значение", 100); + public static final Meal meal5 = new Meal(MEAL1_ID + 4, of(2020, Month.JANUARY, 31, 10, 0), "Завтрак", 500); + public static final Meal meal6 = new Meal(MEAL1_ID + 5, of(2020, Month.JANUARY, 31, 13, 0), "Обед", 1000); + public static final Meal meal7 = new Meal(MEAL1_ID + 6, of(2020, Month.JANUARY, 31, 20, 0), "Ужин", 510); + public static final Meal adminMeal1 = new Meal(ADMIN_MEAL_ID, of(2020, Month.JANUARY, 31, 14, 0), "Админ ланч", 510); + public static final Meal adminMeal2 = new Meal(ADMIN_MEAL_ID + 1, of(2020, Month.JANUARY, 31, 21, 0), "Админ ужин", 1500); + + public static final List meals = List.of(meal7, meal6, meal5, meal4, meal3, meal2, meal1); + + public static Meal getNew() { + return new Meal(null, of(2020, Month.FEBRUARY, 1, 18, 0), "Созданный ужин", 300); + } + + public static Meal getUpdated() { + return new Meal(MEAL1_ID, meal1.getDateTime().plus(2, ChronoUnit.MINUTES), "Обновленный завтрак", 200); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java new file mode 100644 index 00000000000..9d7aa871ae9 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java @@ -0,0 +1,38 @@ +package ru.javawebinar.topjava; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.MealTo; +import ru.javawebinar.topjava.web.meal.MealRestController; +import ru.javawebinar.topjava.web.user.AdminRestController; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.Month; +import java.util.Arrays; +import java.util.List; + +public class SpringMain { + public static void main(String[] args) { + // java 7 automatic resource management (ARM) + try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext(new String[]{"spring/spring-app.xml", "spring/inmemory.xml"}, false)) { + appCtx.getEnvironment().setActiveProfiles("postgres", "datajpa"); + appCtx.refresh(); + System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); + AdminRestController adminUserController = appCtx.getBean(AdminRestController.class); + adminUserController.create(new User(null, "userName", "email@mail.ru", "password", Role.ADMIN)); + System.out.println(); + + MealRestController mealController = appCtx.getBean(MealRestController.class); + List filteredMealsWithExcess = + mealController.getBetween( + LocalDate.of(2020, Month.JANUARY, 30), LocalTime.of(7, 0), + LocalDate.of(2020, Month.JANUARY, 31), LocalTime.of(11, 0)); + filteredMealsWithExcess.forEach(System.out::println); + System.out.println(); + System.out.println(mealController.getBetween(null, null, null, null)); + } + } +} diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java new file mode 100644 index 00000000000..84e1e96b98b --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -0,0 +1,44 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + +public class UserTestData { + public static final MatcherFactory.Matcher USER_MATCHER = MatcherFactory.usingIgnoringFieldsComparator("registered", "roles", "meals"); + + public static final int USER_ID = START_SEQ; + public static final int ADMIN_ID = START_SEQ + 1; + public static final int GUEST_ID = START_SEQ + 2; + public static final int NOT_FOUND = 10; + + public static final User user = new User(USER_ID, "User", "user@yandex.ru", "password", Role.USER); + public static final User admin = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", Role.ADMIN); + public static final User guest = new User(GUEST_ID, "Guest", "guest@gmail.com", "guest"); + + public static final List meals = Arrays.asList(meal1, meal2, meal3, meal4, meal5, meal6, meal7); + + public static User getNew() { + return new User(null, "New", "new@gmail.com", "newPass", 1555, false, new Date(), Collections.singleton(Role.USER)); + } + + public static User getUpdated() { + User updated = new User(user); + updated.setEmail("update@gmail.com"); + updated.setName("UpdatedName"); + updated.setCaloriesPerDay(330); + updated.setPassword("newPass"); + updated.setEnabled(false); + updated.setRoles(Collections.singletonList(Role.ADMIN)); + return updated; + } +} diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java new file mode 100644 index 00000000000..03770da2134 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java @@ -0,0 +1,45 @@ +package ru.javawebinar.topjava.repository.inmemory; + +import ru.javawebinar.topjava.model.AbstractBaseEntity; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + +public class InMemoryBaseRepository { + + static final AtomicInteger counter = new AtomicInteger(START_SEQ); + + final Map map = new ConcurrentHashMap<>(); + + public T save(T entity) { + Objects.requireNonNull(entity, "Entity must not be null"); + if (entity.isNew()) { + entity.setId(counter.incrementAndGet()); + map.put(entity.getId(), entity); + return entity; + } + return map.computeIfPresent(entity.getId(), (id, oldT) -> entity); + } + + public boolean delete(int id) { + return map.remove(id) != null; + } + + public T get(int id) { + return map.get(id); + } + + Collection getCollection() { + return map.values(); + } + + void put(T entity) { + Objects.requireNonNull(entity, "Entity must not be null"); + map.put(entity.id(), entity); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java new file mode 100644 index 00000000000..5c65ced864e --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -0,0 +1,80 @@ +package ru.javawebinar.topjava.repository.inmemory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.MealTestData; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; +import ru.javawebinar.topjava.util.Util; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; + +@Repository +public class InMemoryMealRepository implements MealRepository { + private static final Logger log = LoggerFactory.getLogger(InMemoryMealRepository.class); + + // Map userId -> mealRepository + private final Map> usersMealsMap = new ConcurrentHashMap<>(); + + { + var userMeals = new InMemoryBaseRepository(); + MealTestData.meals.forEach(userMeals::put); + usersMealsMap.put(UserTestData.USER_ID, userMeals); + } + + + @Override + public Meal save(Meal meal, int userId) { + Objects.requireNonNull(meal, "meal must not be null"); + var meals = usersMealsMap.computeIfAbsent(userId, uId -> new InMemoryBaseRepository<>()); + return meals.save(meal); + } + + @PostConstruct + public void postConstruct() { + log.info("+++ PostConstruct"); + } + + @PreDestroy + public void preDestroy() { + log.info("+++ PreDestroy"); + } + + @Override + public boolean delete(int id, int userId) { + var meals = usersMealsMap.get(userId); + return meals != null && meals.delete(id); + } + + @Override + public Meal get(int id, int userId) { + var meals = usersMealsMap.get(userId); + return meals == null ? null : meals.get(id); + } + + @Override + public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return filterByPredicate(userId, meal -> Util.isBetweenHalfOpen(meal.getDateTime(), startDateTime, endDateTime)); + } + + @Override + public List getAll(int userId) { + return filterByPredicate(userId, meal -> true); + } + + private List filterByPredicate(int userId, Predicate filter) { + var meals = usersMealsMap.get(userId); + return meals == null ? Collections.emptyList() : + meals.getCollection().stream() + .filter(filter) + .sorted(Comparator.comparing(Meal::getDateTime).reversed()) + .toList(); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java new file mode 100644 index 00000000000..a7e105d66d4 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -0,0 +1,45 @@ +package ru.javawebinar.topjava.repository.inmemory; + +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +import static ru.javawebinar.topjava.UserTestData.*; + + +@Repository +public class InMemoryUserRepository extends InMemoryBaseRepository implements UserRepository { + + public void init() { + map.clear(); + put(user); + put(admin); + put(guest); + counter.getAndSet(GUEST_ID + 1); + } + + @Override + public List getAll() { + return getCollection().stream() + .sorted(Comparator.comparing(User::getName).thenComparing(User::getEmail)) + .toList(); + } + + @Override + public User getByEmail(String email) { + Objects.requireNonNull(email, "email must not be null"); + return getCollection().stream() + .filter(u -> email.equals(u.getEmail())) + .findFirst() + .orElse(null); + } + + @Override + public User getByIdWithMeals(int id) { + return null; + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/BaseMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/BaseMealServiceTest.java new file mode 100644 index 00000000000..0b80fa25cdd --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/BaseMealServiceTest.java @@ -0,0 +1,122 @@ +package ru.javawebinar.topjava.service; + +import org.junit.*; +import org.junit.rules.Stopwatch; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import ru.javawebinar.topjava.ActiveDbProfileResolver; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.service.MealService; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.time.LocalDate; +import java.time.Month; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertThrows; +import static org.slf4j.LoggerFactory.getLogger; +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; +import static ru.javawebinar.topjava.UserTestData.USER_ID; + +@ContextConfiguration({ + "classpath:spring/spring-app.xml", + "classpath:spring/spring-db.xml" +}) +@RunWith(SpringRunner.class) +@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +@ActiveProfiles(resolver = ActiveDbProfileResolver.class) +@Ignore +public abstract class BaseMealServiceTest extends BaseServiceTest{ + + @Autowired + private MealService service; + + @Test + public void delete() { + service.delete(MEAL1_ID, USER_ID); + assertThrows(NotFoundException.class, () -> service.get(MEAL1_ID, USER_ID)); + } + + @Test + public void deleteNotFound() { + assertThrows(NotFoundException.class, () -> service.delete(NOT_FOUND, USER_ID)); + } + + @Test + public void deleteNotOwn() { + assertThrows(NotFoundException.class, () -> service.delete(MEAL1_ID, ADMIN_ID)); + } + + @Test + public void create() { + Meal created = service.create(getNew(), USER_ID); + int newId = created.id(); + Meal newMeal = getNew(); + newMeal.setId(newId); + MEAL_MATCHER.assertMatch(created, newMeal); + MEAL_MATCHER.assertMatch(service.get(newId, USER_ID), newMeal); + } + + @Test + public void duplicateDateTimeCreate() { + assertThrows(DataAccessException.class, () -> + service.create(new Meal(null, meal1.getDateTime(), "duplicate", 100), USER_ID)); + } + + @Test + public void get() { + Meal actual = service.get(ADMIN_MEAL_ID, ADMIN_ID); + MEAL_MATCHER.assertMatch(actual, adminMeal1); + } + + @Test + public void getNotFound() { + assertThrows(NotFoundException.class, () -> service.get(NOT_FOUND, USER_ID)); + } + + @Test + public void getNotOwn() { + assertThrows(NotFoundException.class, () -> service.get(MEAL1_ID, ADMIN_ID)); + } + + @Test + public void update() { + Meal updated = getUpdated(); + service.update(updated, USER_ID); + MEAL_MATCHER.assertMatch(service.get(MEAL1_ID, USER_ID), getUpdated()); + } + + @Test + public void updateNotOwn() { + NotFoundException exception = assertThrows(NotFoundException.class, () -> service.update(getUpdated(), ADMIN_ID)); + Assert.assertEquals("Not found entity with id=" + MEAL1_ID, exception.getMessage()); + MEAL_MATCHER.assertMatch(service.get(MEAL1_ID, USER_ID), meal1); + } + + @Test + public void getAll() { + MEAL_MATCHER.assertMatch(service.getAll(USER_ID), meals); + } + + @Test + public void getBetweenInclusive() { + MEAL_MATCHER.assertMatch(service.getBetweenInclusive( + LocalDate.of(2020, Month.JANUARY, 30), + LocalDate.of(2020, Month.JANUARY, 30), USER_ID), + meal3, meal2, meal1); + } + + @Test + public void getBetweenWithNullDates() { + MEAL_MATCHER.assertMatch(service.getBetweenInclusive(null, null, USER_ID), meals); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/BaseServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/BaseServiceTest.java new file mode 100644 index 00000000000..74ac58ffcdc --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/BaseServiceTest.java @@ -0,0 +1,35 @@ +package ru.javawebinar.topjava.service; + +import org.junit.AfterClass; +import org.junit.Rule; +import org.junit.rules.Stopwatch; +import org.junit.runner.Description; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public class BaseServiceTest { + private static final Logger log = LoggerFactory.getLogger("results"); + + private static final StringBuilder results = new StringBuilder(); + + @Rule + public final Stopwatch stopwatch = new Stopwatch() { + @Override + protected void finished(long nanos, Description description) { + String result = String.format("\n%-25s %7d", description.getMethodName(), TimeUnit.NANOSECONDS.toMillis(nanos)); + results.append(result); + log.info(result + " ms\n"); + } + }; + + @AfterClass + public static void printResults() { + log.info("\n---------------------------------" + + "\nTest Duration, ms" + + "\n---------------------------------" + + results + + "\n---------------------------------"); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/service/BaseUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/BaseUserServiceTest.java new file mode 100644 index 00000000000..d8e177f0ca2 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/BaseUserServiceTest.java @@ -0,0 +1,102 @@ +package ru.javawebinar.topjava.service; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.dao.DataAccessException; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import ru.javawebinar.topjava.ActiveDbProfileResolver; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.List; + +import static org.junit.Assert.assertThrows; +import static ru.javawebinar.topjava.MealTestData.MEAL_MATCHER; +import static ru.javawebinar.topjava.UserTestData.*; + +@ContextConfiguration({ + "classpath:spring/spring-app.xml", + "classpath:spring/spring-db.xml" +}) +@RunWith(SpringRunner.class) +@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +@ActiveProfiles(resolver = ActiveDbProfileResolver.class) +public abstract class BaseUserServiceTest extends BaseServiceTest{ + @Autowired + protected UserService service; + + @Autowired + private CacheManager cacheManager; + + @Before + public void setup() { + cacheManager.getCache("users").clear(); + } + + @Test + public void create() { + User created = service.create(getNew()); + int newId = created.id(); + User newUser = getNew(); + newUser.setId(newId); + USER_MATCHER.assertMatch(created, newUser); + USER_MATCHER.assertMatch(service.get(newId), newUser); + } + + @Test + public void duplicateMailCreate() { + assertThrows(DataAccessException.class, () -> + service.create(new User(null, "Duplicate", "user@yandex.ru", "newPass", Role.USER))); + } + + @Test + public void delete() { + service.delete(USER_ID); + assertThrows(NotFoundException.class, () -> service.get(USER_ID)); + } + + @Test + public void deletedNotFound() { + assertThrows(NotFoundException.class, () -> service.delete(NOT_FOUND)); + } + + @Test + public void get() { + User user = service.get(USER_ID); + USER_MATCHER.assertMatch(user, UserTestData.user); + } + + @Test + public void getNotFound() { + assertThrows(NotFoundException.class, () -> service.get(NOT_FOUND)); + } + + @Test + public void getByEmail() { + User user = service.getByEmail("admin@gmail.com"); + USER_MATCHER.assertMatch(user, admin); + } + + @Test + public void update() { + User updated = getUpdated(); + service.update(updated); + USER_MATCHER.assertMatch(service.get(USER_ID), getUpdated()); + } + + @Test + public void getAll() { + List all = service.getAll(); + USER_MATCHER.assertMatch(all, admin, guest, user); + } + +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java new file mode 100644 index 00000000000..697362d2b2d --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java @@ -0,0 +1,11 @@ +package ru.javawebinar.topjava.service.datajpa; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.BaseMealServiceTest; + +import static ru.javawebinar.topjava.Profiles.DATAJPA; + +@ActiveProfiles(DATAJPA) +public class DataJpaMealServiceTest extends BaseMealServiceTest { + +} diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java new file mode 100644 index 00000000000..299598ce466 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -0,0 +1,20 @@ +package ru.javawebinar.topjava.service.datajpa; + +import org.junit.Test; +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.BaseUserServiceTest; + +import static ru.javawebinar.topjava.MealTestData.MEAL_MATCHER; +import static ru.javawebinar.topjava.Profiles.DATAJPA; +import static ru.javawebinar.topjava.UserTestData.USER_ID; +import static ru.javawebinar.topjava.UserTestData.meals; + +@ActiveProfiles(DATAJPA) +public class DataJpaUserServiceTest extends BaseUserServiceTest { + @Test + public void getByIdWithMeals() { + User user = super.service.getUserWithMeals(USER_ID); + MEAL_MATCHER.assertMatch(user.getMeals(), meals); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java new file mode 100644 index 00000000000..7f4f7a4a37f --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java @@ -0,0 +1,11 @@ +package ru.javawebinar.topjava.service.jdbc; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.BaseMealServiceTest; + +import static ru.javawebinar.topjava.Profiles.JDBC; + +@ActiveProfiles(JDBC) +public class JdbcMealServiceTest extends BaseMealServiceTest { + +} diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java new file mode 100644 index 00000000000..2a9eac64a5d --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java @@ -0,0 +1,11 @@ +package ru.javawebinar.topjava.service.jdbc; + + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.BaseUserServiceTest; + +import static ru.javawebinar.topjava.Profiles.JDBC; + +@ActiveProfiles(JDBC) +public class JdbcUserServiceTest extends BaseUserServiceTest { +} diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java new file mode 100644 index 00000000000..6190b339c25 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java @@ -0,0 +1,12 @@ +package ru.javawebinar.topjava.service.jpa; + + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.BaseMealServiceTest; + +import static ru.javawebinar.topjava.Profiles.JPA; + +@ActiveProfiles(JPA) +public class JpaMealServiceTest extends BaseMealServiceTest { + +} diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java new file mode 100644 index 00000000000..f713638ac6b --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java @@ -0,0 +1,11 @@ +package ru.javawebinar.topjava.service.jpa; + + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.BaseUserServiceTest; + +import static ru.javawebinar.topjava.Profiles.JPA; + +@ActiveProfiles(JPA) +public class JpaUserServiceTest extends BaseUserServiceTest { +} diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java new file mode 100644 index 00000000000..3e26c2e61d8 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -0,0 +1,42 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import static ru.javawebinar.topjava.UserTestData.NOT_FOUND; +import static ru.javawebinar.topjava.UserTestData.USER_ID; + +@ContextConfiguration({"classpath:spring/spring-app.xml", "classpath:spring/inmemory.xml"}) +@RunWith(SpringRunner.class) +public class InMemoryAdminRestControllerSpringTest { + + @Autowired + private AdminRestController controller; + + @Autowired + private InMemoryUserRepository repository; + + @Before + public void setup() { + repository.init(); + } + + @Test + public void delete() { + controller.delete(USER_ID); + Assert.assertNull(repository.get(USER_ID)); + } + + @Test + public void deleteNotFound() { + Assert.assertThrows(NotFoundException.class, () -> controller.delete(NOT_FOUND)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java new file mode 100644 index 00000000000..14c60d960a0 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -0,0 +1,52 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.Arrays; + +import static ru.javawebinar.topjava.UserTestData.NOT_FOUND; +import static ru.javawebinar.topjava.UserTestData.USER_ID; + +public class InMemoryAdminRestControllerTest { + private static final Logger log = LoggerFactory.getLogger(InMemoryAdminRestControllerTest.class); + + private static ConfigurableApplicationContext appCtx; + private static AdminRestController controller; + private static InMemoryUserRepository repository; + + @BeforeClass + public static void beforeClass() { + appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/inmemory.xml"); + log.info("\n{}\n", Arrays.toString(appCtx.getBeanDefinitionNames())); + controller = appCtx.getBean(AdminRestController.class); + repository = appCtx.getBean(InMemoryUserRepository.class); + } + + @AfterClass + public static void afterClass() { + appCtx.close(); + } + + @Before + public void setup() { + // re-initialize + repository.init(); + } + + @Test + public void delete() { + controller.delete(USER_ID); + Assert.assertNull(repository.get(USER_ID)); + } + + @Test + public void deleteNotFound() { + Assert.assertThrows(NotFoundException.class, () -> controller.delete(NOT_FOUND)); + } +} \ No newline at end of file diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..b692361f8e9 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,33 @@ + + + + true + + + + + UTF-8 + %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n + + + + + + UTF-8 + %magenta(%msg%n) + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/spring/inmemory.xml b/src/test/resources/spring/inmemory.xml new file mode 100644 index 00000000000..c6a2710cbd8 --- /dev/null +++ b/src/test/resources/spring/inmemory.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file