From ef6cdb5d5fb182bf1387e77206ddf174ce4ed005 Mon Sep 17 00:00:00 2001 From: StringerDM <103386227+StringerDM@users.noreply.github.com> Date: Sun, 2 Oct 2022 21:05:28 +0300 Subject: [PATCH 01/10] 1_01_user_with_lombok.patch --- pom.xml | 8 +++++++ .../java/ru/javaops/bootjava/model/Role.java | 6 +++++ .../java/ru/javaops/bootjava/model/User.java | 23 +++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 src/main/java/ru/javaops/bootjava/model/Role.java create mode 100644 src/main/java/ru/javaops/bootjava/model/User.java diff --git a/pom.xml b/pom.xml index ca66a72..5be7a42 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,14 @@ org.springframework.boot spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + diff --git a/src/main/java/ru/javaops/bootjava/model/Role.java b/src/main/java/ru/javaops/bootjava/model/Role.java new file mode 100644 index 0000000..432dde8 --- /dev/null +++ b/src/main/java/ru/javaops/bootjava/model/Role.java @@ -0,0 +1,6 @@ +package ru.javaops.bootjava.model; + +public enum Role { + ROLE_USER, + ROLE_ADMIN +} \ No newline at end of file diff --git a/src/main/java/ru/javaops/bootjava/model/User.java b/src/main/java/ru/javaops/bootjava/model/User.java new file mode 100644 index 0000000..b475761 --- /dev/null +++ b/src/main/java/ru/javaops/bootjava/model/User.java @@ -0,0 +1,23 @@ +package ru.javaops.bootjava.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class User { + + private String email; + + private String firstName; + + private String lastName; + + private String password; + + private Set roles; +} \ No newline at end of file From 530474b5f8ac9f85dd89284476fcb42685cb7aba Mon Sep 17 00:00:00 2001 From: StringerDM <103386227+StringerDM@users.noreply.github.com> Date: Sun, 2 Oct 2022 21:25:03 +0300 Subject: [PATCH 02/10] 2_01_data_jpa.patch --- pom.xml | 4 +++ .../bootjava/RestaurantVotingApplication.java | 17 +++++++++- .../java/ru/javaops/bootjava/model/User.java | 33 +++++++++++++++---- .../bootjava/repository/UserRepository.java | 7 ++++ src/main/resources/application.properties | 9 +++++ 5 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 src/main/java/ru/javaops/bootjava/repository/UserRepository.java diff --git a/pom.xml b/pom.xml index 5be7a42..bf8aa32 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-validation + com.h2database diff --git a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java b/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java index 3326420..61d8ff8 100644 --- a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java +++ b/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java @@ -1,14 +1,29 @@ package ru.javaops.bootjava; import lombok.AllArgsConstructor; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import ru.javaops.bootjava.model.Role; +import ru.javaops.bootjava.model.User; +import ru.javaops.bootjava.repository.UserRepository; + +import java.util.Set; @SpringBootApplication @AllArgsConstructor -public class RestaurantVotingApplication { +public class RestaurantVotingApplication implements ApplicationRunner { + private final UserRepository userRepository; public static void main(String[] args) { SpringApplication.run(RestaurantVotingApplication.class, args); } + + @Override + public void run(ApplicationArguments args) { + userRepository.save(new User("user@gmail.com", "User_First", "User_Last", "password", Set.of(Role.ROLE_USER))); + userRepository.save(new User("admin@javaops.ru", "Admin_First", "Admin_Last", "admin", Set.of(Role.ROLE_USER, Role.ROLE_ADMIN))); + System.out.println(userRepository.findAll()); + } } diff --git a/src/main/java/ru/javaops/bootjava/model/User.java b/src/main/java/ru/javaops/bootjava/model/User.java index b475761..284f632 100644 --- a/src/main/java/ru/javaops/bootjava/model/User.java +++ b/src/main/java/ru/javaops/bootjava/model/User.java @@ -1,23 +1,44 @@ package ru.javaops.bootjava.model; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; +import org.springframework.data.jpa.domain.AbstractPersistable; +import javax.persistence.*; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Size; import java.util.Set; -@Data -@NoArgsConstructor +@Entity +@Table(name = "users") +@Getter +@Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -public class User { +@ToString(callSuper = true, exclude = {"password"}) +public class User extends AbstractPersistable { + @Column(name = "email", nullable = false, unique = true) + @Email + @NotEmpty + @Size(max = 128) private String email; + @Column(name = "first_name") + @Size(max = 128) private String firstName; + @Column(name = "last_name") + @Size(max = 128) private String lastName; + @Column(name = "password") + @Size(max = 256) private String password; + @Enumerated(EnumType.STRING) + @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "user_roles_unique")}) + @Column(name = "role") + @ElementCollection(fetch = FetchType.EAGER) private Set roles; } \ No newline at end of file diff --git a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java new file mode 100644 index 0000000..590c614 --- /dev/null +++ b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java @@ -0,0 +1,7 @@ +package ru.javaops.bootjava.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.javaops.bootjava.model.User; + +public interface UserRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e69de29..3492ea9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -0,0 +1,9 @@ +# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# JPA +spring.jpa.show-sql=true +spring.jpa.open-in-view=false +# http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#configurations +spring.jpa.properties.hibernate.default_batch_fetch_size=20 +spring.jpa.properties.hibernate.format_sql=true +# https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc +spring.jpa.properties.hibernate.jdbc.batch_size=20 From 2e03672e1984c941211e37256e7b07eaea5445a3 Mon Sep 17 00:00:00 2001 From: StringerDM <103386227+StringerDM@users.noreply.github.com> Date: Sun, 2 Oct 2022 21:38:34 +0300 Subject: [PATCH 03/10] 2_02_h2_init.patch --- pom.xml | 1 - .../bootjava/RestaurantVotingApplication.java | 6 ---- .../ru/javaops/bootjava/config/AppConfig.java | 26 ++++++++++++++++ src/main/resources/application.properties | 9 ------ src/main/resources/application.yaml | 30 +++++++++++++++++++ src/main/resources/data.sql | 8 +++++ 6 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 src/main/java/ru/javaops/bootjava/config/AppConfig.java delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yaml create mode 100644 src/main/resources/data.sql diff --git a/pom.xml b/pom.xml index bf8aa32..0fe4458 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,6 @@ com.h2database h2 - runtime org.projectlombok diff --git a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java b/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java index 61d8ff8..d3b1792 100644 --- a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java +++ b/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java @@ -5,12 +5,8 @@ import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import ru.javaops.bootjava.model.Role; -import ru.javaops.bootjava.model.User; import ru.javaops.bootjava.repository.UserRepository; -import java.util.Set; - @SpringBootApplication @AllArgsConstructor public class RestaurantVotingApplication implements ApplicationRunner { @@ -22,8 +18,6 @@ public static void main(String[] args) { @Override public void run(ApplicationArguments args) { - userRepository.save(new User("user@gmail.com", "User_First", "User_Last", "password", Set.of(Role.ROLE_USER))); - userRepository.save(new User("admin@javaops.ru", "Admin_First", "Admin_Last", "admin", Set.of(Role.ROLE_USER, Role.ROLE_ADMIN))); System.out.println(userRepository.findAll()); } } diff --git a/src/main/java/ru/javaops/bootjava/config/AppConfig.java b/src/main/java/ru/javaops/bootjava/config/AppConfig.java new file mode 100644 index 0000000..19dbc45 --- /dev/null +++ b/src/main/java/ru/javaops/bootjava/config/AppConfig.java @@ -0,0 +1,26 @@ +package ru.javaops.bootjava.config; + +import lombok.extern.slf4j.Slf4j; +import org.h2.tools.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.sql.SQLException; + +@Configuration +@Slf4j +public class AppConfig { + +/* + @Bean(initMethod = "start", destroyMethod = "stop") + public Server h2WebServer() throws SQLException { + return Server.createWebServer("-web", "-webAllowOthers", "-webPort", "8082"); + } +*/ + + @Bean(initMethod = "start", destroyMethod = "stop") + public Server h2Server() throws SQLException { + log.info("Start H2 TCP server"); + return Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", "9092"); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 3492ea9..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,9 +0,0 @@ -# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html -# JPA -spring.jpa.show-sql=true -spring.jpa.open-in-view=false -# http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#configurations -spring.jpa.properties.hibernate.default_batch_fetch_size=20 -spring.jpa.properties.hibernate.format_sql=true -# https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc -spring.jpa.properties.hibernate.jdbc.batch_size=20 diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..6c6343d --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,30 @@ +# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +spring: + jpa: + show-sql: true + open-in-view: false + hibernate: + ddl-auto: create-drop + properties: + # http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#configurations + hibernate: + format_sql: true + default_batch_fetch_size: 20 + # https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc + jdbc.batch_size: 20 + id.new_generator_mappings: false + datasource: + # ImMemory + url: jdbc:h2:mem:voting + # tcp: jdbc:h2:tcp://localhost:9092/mem:voting + # Absolute path + # url: jdbc:h2:C:/projects/bootjava/restorant-voting/db/voting + # tcp: jdbc:h2:tcp://localhost:9092/C:/projects/bootjava/restorant-voting/db/voting + # Relative path form current dir + # url: jdbc:h2:./db/voting + # Relative path from home + # url: jdbc:h2:~/voting + # tcp: jdbc:h2:tcp://localhost:9092/~/voting + username: sa + password: + h2.console.enabled: true \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..0fe391f --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,8 @@ +INSERT INTO USERS (EMAIL, FIRST_NAME, LAST_NAME, PASSWORD) +VALUES ('user@gmail.com', 'User_First', 'User_Last', 'password'), + ('admin@javaops.ru', 'Admin_First', 'Admin_Last', 'admin'); + +INSERT INTO USER_ROLE (ROLE, USER_ID) +VALUES ('ROLE_USER', 1), + ('ROLE_ADMIN', 2), + ('ROLE_USER', 2); \ No newline at end of file From f789d22071f65c732533c9b512015e8a05b8ede5 Mon Sep 17 00:00:00 2001 From: StringerDM <103386227+StringerDM@users.noreply.github.com> Date: Sun, 2 Oct 2022 22:29:54 +0300 Subject: [PATCH 04/10] 2_03_model_query.patch --- .../bootjava/RestaurantVotingApplication.java | 2 +- .../ru/javaops/bootjava/model/BaseEntity.java | 52 +++++++++++++++++++ .../java/ru/javaops/bootjava/model/User.java | 3 +- .../bootjava/repository/UserRepository.java | 11 ++++ src/main/resources/application.yaml | 1 - 5 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 src/main/java/ru/javaops/bootjava/model/BaseEntity.java diff --git a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java b/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java index d3b1792..fa56af5 100644 --- a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java +++ b/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java @@ -18,6 +18,6 @@ public static void main(String[] args) { @Override public void run(ApplicationArguments args) { - System.out.println(userRepository.findAll()); + System.out.println(userRepository.findByLastNameContainingIgnoreCase("last")); } } diff --git a/src/main/java/ru/javaops/bootjava/model/BaseEntity.java b/src/main/java/ru/javaops/bootjava/model/BaseEntity.java new file mode 100644 index 0000000..4a697e5 --- /dev/null +++ b/src/main/java/ru/javaops/bootjava/model/BaseEntity.java @@ -0,0 +1,52 @@ +package ru.javaops.bootjava.model; + +import lombok.*; +import org.springframework.data.domain.Persistable; +import org.springframework.data.util.ProxyUtils; +import org.springframework.util.Assert; + +import javax.persistence.*; + +@MappedSuperclass +// https://stackoverflow.com/a/6084701/548473 +@Access(AccessType.FIELD) +@Getter +@Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@ToString +public abstract class BaseEntity implements Persistable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + protected Integer id; + + // doesn't work for hibernate lazy proxy + public int id() { + Assert.notNull(id, "Entity must have id"); + return id; + } + + @Override + public boolean isNew() { + return id == null; + } + + // https://stackoverflow.com/questions/1638723 + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !getClass().equals(ProxyUtils.getUserClass(o))) { + return false; + } + BaseEntity that = (BaseEntity) o; + return id != null && id.equals(that.id); + } + + @Override + public int hashCode() { + return id == null ? 0 : id; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javaops/bootjava/model/User.java b/src/main/java/ru/javaops/bootjava/model/User.java index 284f632..575aaff 100644 --- a/src/main/java/ru/javaops/bootjava/model/User.java +++ b/src/main/java/ru/javaops/bootjava/model/User.java @@ -1,7 +1,6 @@ package ru.javaops.bootjava.model; import lombok.*; -import org.springframework.data.jpa.domain.AbstractPersistable; import javax.persistence.*; import javax.validation.constraints.Email; @@ -16,7 +15,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @ToString(callSuper = true, exclude = {"password"}) -public class User extends AbstractPersistable { +public class User extends BaseEntity { @Column(name = "email", nullable = false, unique = true) @Email diff --git a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java index 590c614..f5d1f0e 100644 --- a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java +++ b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java @@ -1,7 +1,18 @@ package ru.javaops.bootjava.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; import ru.javaops.bootjava.model.User; +import java.util.List; +import java.util.Optional; + +@Transactional(readOnly = true) public interface UserRepository extends JpaRepository { + + @Query("SELECT u FROM User u WHERE u.email = LOWER(:email)") + Optional findByEmailIgnoreCase(String email); + + List findByLastNameContainingIgnoreCase(String lastName); } \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 6c6343d..7a0d777 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -12,7 +12,6 @@ spring: default_batch_fetch_size: 20 # https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc jdbc.batch_size: 20 - id.new_generator_mappings: false datasource: # ImMemory url: jdbc:h2:mem:voting From b11d9ab9f5f52bc7b4c580177bc6273704966f44 Mon Sep 17 00:00:00 2001 From: StringerDM <103386227+StringerDM@users.noreply.github.com> Date: Thu, 6 Oct 2022 06:53:08 +0300 Subject: [PATCH 05/10] Update README.md --- README.md | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 22f19d8..a467188 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,133 @@ ## [Программа](http://javaops.ru/view/bootjava#program) ### Java приложения на самом современном и востребованном стеке: Spring Boot 2.x, Spring Data Rest/HATEOAS, Lombok, JPA, H2, .... -Мы создадим с нуля основу любого современного REST веб-приложения: аутентификация и авторизация на основе ролей, регистрация пользователя в приложении, управление своим профилем и администрирование пользователей. \ No newline at end of file +Мы создадим с нуля основу любого современного REST веб-приложения: аутентификация и авторизация на основе ролей, регистрация пользователя в приложении, управление своим профилем и администрирование пользователей. + +Конспект: +
+ 1. Основы Spring Boot + 1.1 Создаем проект через Spring Initializer + - Подключаем зависимости: + - Lombock + - Spring Web + - H2 database + - Spring Data JPA + + По умолчанию приложение открывается по адресу localhost:8080 + + Ссылки: + Spring Initializrs: https://start.spring.io/ + + Commit: https://github.com/StringerDM/bootjava/commit/35a21d499357b464ebb5b571cb97ac0bc5e57f01 + + 1.2 Spring Boot maven plugin. Конвертация в WAR + + Ссылки: + Конвертация JAR приложения в WAR http://spring-projects.ru/guides/convert-jar-to-war-maven/ + + 1.3 Настройка проекта + Готовый проект с патчами находится в ветке patched: git clone --branch patched https://github.com/JavaOPs/bootjava.git + + 1.4 Проект Lombok + В Pom.xml он уже у нас есть, причем true : + + org.projectlombok + lombok + true + + + Если мы посмотрим, что такое optional dependencies: http://maven.apache.org/guides/introduction/introduction-to-optional-and-excludes-dependencies.html то увидим, что оно используется для библиотек, у которых есть много транзитивных зависимостей и подключая эти библиотеки с optional мы избавляемся от их зависимостей которые нам возможно не понадобятся. У нас совсем не библиотека, а собственный проект поэтому использование optional достаточно сомнительно. + + Кроме того, если мы посмотрим: Maven Scope for Lombok (Compile vs. Provided) https://stackoverflow.com/questions/29385921/548473 то увидим что в оф документации Lombok нужно подключать со скопом provided. То есть lombok на нужен только на этапе компиляции и из сборки он исключается. + + И еще одна ссылка Exclude lombok in Spring Boot https://stackoverflow.com/questions/45202639/548473 где говорится что если мы делаем JAR то туда включается embedded Tomcat и все зависимости даже со скопом provided также попадают в нашу сборку. Для того чтобы исключить lombock из сборки нужно явно добавить в pom.xml в boot maven plugin явную конфигурацию : + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + Добавляем getters and setters и пустой + со всем аргументами конструктор используя аннотации Lombok. + @Data + @NoArgsConstructor + @AllArgsConstructor + + Полезная аннотация которая добавляет логгер классу. + @Log + + Ссылки: Фичи Lombok https://urvanov.ru/2015/09/22/project-lombok/ + + Commit: https://github.com/StringerDM/bootjava/commit/ef6cdb5d5fb182bf1387e77206ddf174ce4ed005 + +
+ +
+ 2. Работа с DB (H2, Spring Data JPA) + 2.1 Spring Data JPA. ApplicationRunner + В проекте у нас уже есть подключенный spring-boot-starter-data-jpa, также подключина БД H2 и при запуске sping boot уже может сразу поднять БД с настройками по умолчанию. База embedded т.е. она работает в тойже JVM что и наше приложение и по умолчанию spring boot создает ее прямо и entites (классы отмеченные @Entity). + + Добавляем требуемые аннотации в модель для валидации, названия таблиц и колонок (не обязательно, по умолчанию по имени полей). См. commit. + + @Entity + @Table(name = "") + @Column(name = "") + + @Size(max = 128) + @NotEmpty + @NotNull + @Email + + и т.д. + + Чтобы не создавать поле Id можно унаследоваться от класса AbstractPersistable который уже содержит поле Id с нужными аннотациями для генерации ключей в базе и методами setId, isNew, equals, heshcode, toString. + + Также добавим lombok аннотацию @ToString(callSuper = true, exclude = {"password"}) с параметрами "callSuper = true" для включения поля id из суперкласса и exclude = {"password"} для исключения из строки поля password. + + Для ролей мы не делаем отдельное entity а указываем их как @ElementCollection(fetch = FetchType.EAGER) + + C spring boot v2.3 убрали валидацию по умолчанию, поэтому добавили в pom.xml: + + + org.springframework.boot + spring-boot-starter-validation + + + Далее определяем интерфейс userRepository extends JpaRepository. Имплементация по умолчанию JpaRepository это класс SimpleJpaRepository, сбда можно брейк поинты ставить для дебага. + + В aplication.property сделаем одну настройку (Common application Data properties - https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#data-properties все настройки spring boot и по ключевому слов JPA мы можем найти все конфигурационный классы и что можно объявлять): + + spring.jpa.show-sql=true - для отображения запросов в базу. + + Запускаем приложение и смотрим как наша таблица создается. По умолчанию для embedded БД таблицы сначало дропаются, затем создается общий для всех hibernate siquence и создаются таблицы. + + Зделаем сначало заполнение таблиц програмно. В spring boot есть 2 интерфейса ApplicationRunner and CommandLineRunner которые позволяют выполнять произвольный код после старта приложения. Разница между ними в том что ApplicationRunner мы принимае массив аргументов обернутый в класс который позовляет нам выполнять какието удобные вещи например getOptional value. Реализовывать интерфейсы можно в любом из бинов spring, мы реализуем его в главном RestaurantVotingApplication: + + //реализуем интерфейс ApplicationRunner + @SpringBootApplication + @AllArgsConstructor + public class RestaurantVotingApplication implements ApplicationRunner { + + //инжектим userRepository через аннотацию @AllArgsConstructor + private final UserRepository userRepository; + + Ссылки: : + + + Commit: https://github.com/StringerDM/bootjava/commit/530474b5f8ac9f85dd89284476fcb42685cb7aba + +
+
+ 1 Основы Spring Boot + +Ссылки: + +Commit: +
From 2a23b271f0acee23a86d267ec7e81b86ffc52058 Mon Sep 17 00:00:00 2001 From: StringerDM <103386227+StringerDM@users.noreply.github.com> Date: Sun, 9 Oct 2022 16:49:38 +0300 Subject: [PATCH 06/10] Update README.md --- README.md | 117 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 108 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a467188..b0663a9 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@
1. Основы Spring Boot 1.1 Создаем проект через Spring Initializer + + Commit: https://github.com/StringerDM/bootjava/commit/35a21d499357b464ebb5b571cb97ac0bc5e57f01 + - Подключаем зависимости: - Lombock - Spring Web @@ -22,8 +25,6 @@ Ссылки: Spring Initializrs: https://start.spring.io/ - Commit: https://github.com/StringerDM/bootjava/commit/35a21d499357b464ebb5b571cb97ac0bc5e57f01 - 1.2 Spring Boot maven plugin. Конвертация в WAR Ссылки: @@ -33,6 +34,9 @@ Готовый проект с патчами находится в ветке patched: git clone --branch patched https://github.com/JavaOPs/bootjava.git 1.4 Проект Lombok + + Commit: https://github.com/StringerDM/bootjava/commit/ef6cdb5d5fb182bf1387e77206ddf174ce4ed005 + В Pom.xml он уже у нас есть, причем true : org.projectlombok @@ -69,13 +73,14 @@ Ссылки: Фичи Lombok https://urvanov.ru/2015/09/22/project-lombok/ - Commit: https://github.com/StringerDM/bootjava/commit/ef6cdb5d5fb182bf1387e77206ddf174ce4ed005 -
2. Работа с DB (H2, Spring Data JPA) 2.1 Spring Data JPA. ApplicationRunner + + Commit: https://github.com/StringerDM/bootjava/commit/530474b5f8ac9f85dd89284476fcb42685cb7aba + В проекте у нас уже есть подключенный spring-boot-starter-data-jpa, также подключина БД H2 и при запуске sping boot уже может сразу поднять БД с настройками по умолчанию. База embedded т.е. она работает в тойже JVM что и наше приложение и по умолчанию spring boot создает ее прямо и entites (классы отмеченные @Entity). Добавляем требуемые аннотации в модель для валидации, названия таблиц и колонок (не обязательно, по умолчанию по имени полей). См. commit. @@ -97,7 +102,7 @@ Для ролей мы не делаем отдельное entity а указываем их как @ElementCollection(fetch = FetchType.EAGER) - C spring boot v2.3 убрали валидацию по умолчанию, поэтому добавили в pom.xml: + Cо spring boot v2.3 убрали валидацию по умолчанию, поэтому добавили в pom.xml: org.springframework.boot @@ -108,7 +113,7 @@ В aplication.property сделаем одну настройку (Common application Data properties - https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#data-properties все настройки spring boot и по ключевому слов JPA мы можем найти все конфигурационный классы и что можно объявлять): - spring.jpa.show-sql=true - для отображения запросов в базу. + spring.jpa.show-sql=true - для отображения запросов в базу. (это крайне полезно для Hibernate во время разработки). Запускаем приложение и смотрим как наша таблица создается. По умолчанию для embedded БД таблицы сначало дропаются, затем создается общий для всех hibernate siquence и создаются таблицы. @@ -122,10 +127,104 @@ //инжектим userRepository через аннотацию @AllArgsConstructor private final UserRepository userRepository; - Ссылки: : + //вставляем в базу 2х юзеров: + + @Override + public void run(ApplicationArguments args) { + userRepository.save(new User("user@gmail.com", "User_First", "User_Last", "password", Set.of(Role.ROLE_USER))); + userRepository.save(new User("admin@javaops.ru", "Admin_First", "Admin_Last", "admin", Set.of(Role.ROLE_USER, Role.ROLE_ADMIN))); + } + + Запускаем приложение и видимо что Hibernat делает 3 запроса, 1м он достает 2х юзеров и потом на каждого юзера он достает роли. Это измвестная проблема n+1, если бы у нас было 10 тысяч юзеров то Hibernate сгенерил бы 10 001 запрос. + Проблема N+1. Стратегии загрузки коллекций + N+1 selects issue https://stackoverflow.com/questions/97197/548473 + в JPA https://dou.ua/lenta/articles/jpa-fetch-types/ + в Hibernate https://dou.ua/lenta/articles/hibernate-fetch-types/ + если ссылки выше не открываются: Runet Censorship Bypass https://chrome.google.com/webstore/detail/%D0%BE%D0%B1%D1%85%D0%BE%D0%B4-%D0%B1%D0%BB%D0%BE%D0%BA%D0%B8%D1%80%D0%BE%D0%B2%D0%BE%D0%BA-%D1%80%D1%83%D0%BD%D0%B5%D1%82%D0%B0/npgcnondjocldhldegnakemclmfkngch + В TopJava мы решали её тремя сопособами: + - Через fetch Join + - Entity Graff + - И для ролей в Юзере мы делали @BatchSize(size = 20) + + В Hibernate есть настрока которая позволяет выставлять batch size глобально для всего приложения. + Hibernate configurations - http://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#configurations - по ссылке можно найти настройку spring.jpa.properties.hibernate.default_batch_fetch_size=20 (укажем 20 по размеру колонок в таблице на странице). + hibernate.jdbc.fetch_size vs hibernate.jdbc.batch_size - https://stackoverflow.com/questions/21257819/548473 + + Также добавим spring.jpa.properties.hibernate.format_sql=true - форматирование sql запросов в выводе (запросы читать легче) + и spring.jpa.properties.hibernate.jdbc.batch_size=20 это количество в баче апрдейтов и инсертов хибернейта. + # https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc + + И последняя настройка, если мы посмотрим на лог то мы увидим Warning - spring.jpa.open-in-view is enabled by default и нужно его выключить: + spring.jpa.open-in-view=false + Open Session In View Anti-Pattern - # https://vladmihalcea.com/the-open-session-in-view-anti-pattern/ + spring.jpa.open-in-view - # https://stackoverflow.com/a/48222934/548473 + Это антипаттерн - если в модели при преобразовании view остались какието не проинициализированный поля которые lazy proxy то открывается транзакция и делаются еще дополнительный запросы в базу чтобы проинициализировать эти поля. + Запускаем приложение и смотрим на отработку запроса findAll и видем что теперь только 2 запроса.1й для юзеров и 1 запрос для всех ролей. Если юзеров будет много то роли будут доставаться пачками по 20 юзеров. + + 2.2 H2. Популирование и конфигурирование + + Commit: https://github.com/StringerDM/bootjava/commit/2e03672e1984c941211e37256e7b07eaea5445a3 + + Открытая СУБД написанная полностью на Java не смотря на малый размер, поддерживает много возможностей... + + Первое что мы сделаем это перейдем с формата .properties на формат .yaml + Явно объявим то что было по дефолту + Встроенная база + hibernate: + ddl-auto: create-drop + datasource: + url: jdbc:h2:mem:voting + username: sa + password: + # tcp: jdbc:h2:tcp://localhost:9092/mem:voting + # Absolute path + # url: jdbc:h2:C:/projects/bootjava/restorant-voting/db/voting + # tcp: jdbc:h2:tcp://localhost:9092/C:/projects/bootjava/restorant-voting/db/voting + # Relative path form current dir + # url: jdbc:h2:./db/voting + # Relative path from home + # url: jdbc:h2:~/voting + # tcp: jdbc:h2:tcp://localhost:9092/~/voting + h2.console.enabled: true + +!!! если у вас версия spring-boot 2.5.0 и выше, добавьте в application.yaml: !!! +spring.jpa.defer-datasource-initialization: true + + Чтобы поднять H2 TCP сервер мы делаем конф. класс и объявляем там + @Bean(initMethod = "start", destroyMethod = "stop") + public Server h2Server() throws SQLException { + log.info("Start H2 TCP server"); + return Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", "9092"); + } + + При этом в pom нам нужно убрать runtime зависимости h2 потомучто классы h2 теперь понадобились на этапе компиляции. + + Запускаем приложение и подключаемся к базе через idea. Если мы попробуем приконектится по url то ничего не выйдет, конект пройдет но если мы на неё посмотрим то никаких баз не увидим. База данных к которой мы приконектились поднимается в памяти в процессе JVM idea и никакой отношение к БД приложения не имеет. Поэтому мы подняли TCP сервер чтобы мы могли приконектится извне - jdbc:h2:tcp://localhost:9092/mem:voting + + Подключаемся к базе и делаем интеграцию с Idea выбирая в persistence/springboot -> data source – H2. + + H2 console также доступна по http://localhost:8080/h2-console + + Давайте пропопулируем нашу БД не через приложение а через скрипт как это обычно делается. + Из applicationRunner удаляем save user и добавляем в ресурсы файл data.sql где популируем users и userRoles (у spring boot 2 файла который он автоматически исполняет data.sql и schema.sql schema нам не требуется т.к. за создание схемы базы отвечает hibernate). + Loading Initial Data https://www.baeldung.com/spring-boot-data-sql-and-schema-sql + Запускаем приложение и сталкиваемся с проблемой что ID у нас должно быть NotNull но оно автоматически не генерится. Смотрим на лог генерации таблицы и видимо что ID сгенерировалось как обычное поле. + H2: NULL not allowed for column “ID” - https://stackoverflow.com/a/54697387/548473 + Смотрим решение проблемы на stackoverflow и видим 3 варианта: + + 1. Поменять @GeneratedValue с авто, как у нас в наследуемом AbstractPersistable классе на + change @GeneratedValue to strategy = GenerationType.IDENTITY + + 2. Set spring.jpa.properties.hibernate.id.new_generator_mappings=false (spring-boot alias spring.jpa.hibernate.use-new-id-generator-mappings) это означает + работу по старой стратегии не по sequence а по identity + + 3. insert with nextval: INSERT INTO TABLE(ID, ...) VALUES (hibernate_sequence.nextval, ...) – вставлять в базу ID сгенерированный hibernate. + + Для нас самое просто использовать 2й вариант. Теперь все работает. Со старой стратегии ID генерится как identity. + + 2.3 Рефакторинг model. Spring Data JPA @Query + - - Commit: https://github.com/StringerDM/bootjava/commit/530474b5f8ac9f85dd89284476fcb42685cb7aba
From 584d0e053cd69be3ba105b46ede07e803cb7b50c Mon Sep 17 00:00:00 2001 From: StringerDM <103386227+StringerDM@users.noreply.github.com> Date: Sun, 9 Oct 2022 18:46:44 +0300 Subject: [PATCH 07/10] Update README.md --- README.md | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b0663a9..5dbcc78 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ 1. Основы Spring Boot 1.1 Создаем проект через Spring Initializer - Commit: https://github.com/StringerDM/bootjava/commit/35a21d499357b464ebb5b571cb97ac0bc5e57f01 + commit: https://github.com/StringerDM/bootjava/commit/35a21d499357b464ebb5b571cb97ac0bc5e57f01 - Подключаем зависимости: - Lombock @@ -187,8 +187,8 @@ # tcp: jdbc:h2:tcp://localhost:9092/~/voting h2.console.enabled: true -!!! если у вас версия spring-boot 2.5.0 и выше, добавьте в application.yaml: !!! -spring.jpa.defer-datasource-initialization: true + если у вас версия spring-boot 2.5.0 и выше, добавьте в application.yaml: + spring.jpa.defer-datasource-initialization: true Чтобы поднять H2 TCP сервер мы делаем конф. класс и объявляем там @Bean(initMethod = "start", destroyMethod = "stop") @@ -224,13 +224,41 @@ spring.jpa.defer-datasource-initialization: true 2.3 Рефакторинг model. Spring Data JPA @Query - - + commit: https://github.com/StringerDM/bootjava/commit/f789d22071f65c732533c9b512015e8a05b8ede5 + Заменим стандартный AbstractPersistable собственным классом BaseEntity: + @Access(AccessType.FIELD) + Здесь объявляем чтобы hibernate работал с entity по полям - https://stackoverflow.com/a/6084701/548473 + Методы тип isNew() не нужно помечать что они transient. + Методы equals и hashCode сделаны попроще. + И в equal эту строчку взяли из класса AbstractPersistable: + + if (o == null || !getClass().equals(ProxyUtils.getUserClass(o))) { + return false; + } + + Т.к. hibernate может проектировать классы и перед сравнением их нужно развернуть. + Ссылка как правильно в Entity hibernate переопределять equals и hashCode (очень частая ошибка) + https://stackoverflow.com/questions/1638723 + + По правилам рекомендуется делать уникальное неизменяемое бизнес поле, а обычно такого нет и во всех проектах использовался primary key. На primary key сделали @GeneratedValue(strategy = GenerationType.IDENTITY) как у нас и генирурется на данный момент, поэтому в файле конфигурации id.new_generator_mappings: false уже не требуется. + + Все наши Entity классы будем наследовать он BaseEntity. + + interface UserRepository { + В репозиториях в запросе @Query для именованных параметров (:email) теперь в методе можно не указывать аннотацию @Param(“email”), hibernate теперь берет имя параметра через отражение. + + @Query("SELECT u FROM User u WHERE u.email = LOWER(:email)") + Optional findByEmailIgnoreCase(String email); + } + + Также как и в контроллерах в аннотациях @Pasthariable и @RequestParam атрибуты nameValue не требуется. +
- 1 Основы Spring Boot + 3 Spring Data REST + HATEOAS Ссылки: Commit:
+ From 8671606a67ce4d9e57da95c30c0a736508804e0f Mon Sep 17 00:00:00 2001 From: StringerDM <103386227+StringerDM@users.noreply.github.com> Date: Mon, 10 Oct 2022 21:27:54 +0300 Subject: [PATCH 08/10] 3_01_jpa_data_rest.patch --- pom.xml | 12 +++++++++++- .../javaops/bootjava/repository/UserRepository.java | 3 +++ src/main/resources/application.yaml | 9 +++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 0fe4458..5aabae1 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,17 @@ org.springframework.boot spring-boot-starter-validation
- + + org.springframework.boot + spring-boot-starter-data-rest + + com.h2database h2 diff --git a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java index f5d1f0e..dc2a413 100644 --- a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java +++ b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java @@ -2,6 +2,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.rest.core.annotation.RestResource; import org.springframework.transaction.annotation.Transactional; import ru.javaops.bootjava.model.User; @@ -11,8 +12,10 @@ @Transactional(readOnly = true) public interface UserRepository extends JpaRepository { + @RestResource(rel = "by-email", path = "by-email") @Query("SELECT u FROM User u WHERE u.email = LOWER(:email)") Optional findByEmailIgnoreCase(String email); + @RestResource(rel = "by-lastname", path = "by-lastname") List findByLastNameContainingIgnoreCase(String lastName); } \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 7a0d777..74b63ee 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -17,7 +17,7 @@ spring: url: jdbc:h2:mem:voting # tcp: jdbc:h2:tcp://localhost:9092/mem:voting # Absolute path - # url: jdbc:h2:C:/projects/bootjava/restorant-voting/db/voting + # url: jdbc:h2:E:/projects/bootjava/restorant-voting/db/voting # tcp: jdbc:h2:tcp://localhost:9092/C:/projects/bootjava/restorant-voting/db/voting # Relative path form current dir # url: jdbc:h2:./db/voting @@ -26,4 +26,9 @@ spring: # tcp: jdbc:h2:tcp://localhost:9092/~/voting username: sa password: - h2.console.enabled: true \ No newline at end of file + h2.console.enabled: true + + data.rest: + # https://docs.spring.io/spring-data/rest/docs/current/reference/html/#getting-started.basic-settings + basePath: /api + returnBodyOnCreate: true \ No newline at end of file From 3cba92bbb7c392e258c9ff86cfbd0ea39a2cb895 Mon Sep 17 00:00:00 2001 From: StringerDM <103386227+StringerDM@users.noreply.github.com> Date: Mon, 10 Oct 2022 22:09:03 +0300 Subject: [PATCH 09/10] 3_02_jackson.patch --- src/main/java/ru/javaops/bootjava/model/BaseEntity.java | 2 ++ src/main/resources/application.yaml | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/javaops/bootjava/model/BaseEntity.java b/src/main/java/ru/javaops/bootjava/model/BaseEntity.java index 4a697e5..72ed0fc 100644 --- a/src/main/java/ru/javaops/bootjava/model/BaseEntity.java +++ b/src/main/java/ru/javaops/bootjava/model/BaseEntity.java @@ -1,5 +1,6 @@ package ru.javaops.bootjava.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.*; import org.springframework.data.domain.Persistable; import org.springframework.data.util.ProxyUtils; @@ -27,6 +28,7 @@ public int id() { return id; } + @JsonIgnore @Override public boolean isNew() { return id == null; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 74b63ee..bfdf4b7 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -31,4 +31,11 @@ spring: data.rest: # https://docs.spring.io/spring-data/rest/docs/current/reference/html/#getting-started.basic-settings basePath: /api - returnBodyOnCreate: true \ No newline at end of file + returnBodyOnCreate: true + +# Jackson Serialization Issue Resolver +# jackson: +# visibility.field: any +# visibility.getter: none +# visibility.setter: none +# visibility.is-getter: none \ No newline at end of file From 59a2d0acabd1ac9dc044c83dcde5ae46dc920898 Mon Sep 17 00:00:00 2001 From: StringerDM <103386227+StringerDM@users.noreply.github.com> Date: Mon, 10 Oct 2022 22:27:51 +0300 Subject: [PATCH 10/10] Update README.md --- README.md | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 175 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5dbcc78..8f7b018 100644 --- a/README.md +++ b/README.md @@ -240,12 +240,16 @@ Ссылка как правильно в Entity hibernate переопределять equals и hashCode (очень частая ошибка) https://stackoverflow.com/questions/1638723 - По правилам рекомендуется делать уникальное неизменяемое бизнес поле, а обычно такого нет и во всех проектах использовался primary key. На primary key сделали @GeneratedValue(strategy = GenerationType.IDENTITY) как у нас и генирурется на данный момент, поэтому в файле конфигурации id.new_generator_mappings: false уже не требуется. + По правилам рекомендуется делать уникальное неизменяемое бизнес поле, а обычно такого нет и во всех + проектах использовался primary key. На primary key сделали @GeneratedValue(strategy = GenerationType.IDENTITY) + как у нас и генирурется на данный момент, поэтому в файле конфигурации id.new_generator_mappings: false уже не + требуется. Все наши Entity классы будем наследовать он BaseEntity. interface UserRepository { - В репозиториях в запросе @Query для именованных параметров (:email) теперь в методе можно не указывать аннотацию @Param(“email”), hibernate теперь берет имя параметра через отражение. + В репозиториях в запросе @Query для именованных параметров (:email) теперь в методе можно не указывать аннотацию + @Param(“email”), hibernate теперь берет имя параметра через отражение. @Query("SELECT u FROM User u WHERE u.email = LOWER(:email)") Optional findByEmailIgnoreCase(String email); @@ -256,9 +260,175 @@
3 Spring Data REST + HATEOAS + 3.1 Spring Data REST + + commit: https://github.com/StringerDM/bootjava/commit/8671606a67ce4d9e57da95c30c0a736508804e0f + + Оживим наше приложение, добавим зависимость + + org.springframework.boot + spring-boot-starter-data-rest + + + Теперь в браузере стали доступны следующие странички: + GET http://localhost:8080/api + GET http://localhost:8080/api/users + GET http://localhost:8080/api/users/1 + GET http://localhost:8080/api/users/search + GET http://localhost:8080/api/users/search/by-email?email=User@gmail.com + GET http://localhost:8080/api/users/search/by-lastname?lastName=Admin + GET http://localhost:8080/api/users/search/by-lastname?lastName=last + POST http://localhost:8080/api/users + Content-Type: application/json -Ссылки: + { + "email": "test@test.com", + "firstName": "Test", + "lastName": "Test", + "password": "test", + "roles": [ "ROLE_USER"] + } -Commit: -
+ ### + PATCH http://localhost:8080/api/users/1 + Content-Type: application/json + + { + "lastName": "User+Last" + } + + Spring Data Rest - это модуль который входит в семейство Spring Data, анализирует репозитории и доменную модель и + проанализированный результат выставляет наружу через контроллеры как hypermedia driven HTTP resources. + + Понимание HATEOAS (Hypermedia as the Engine of Application State) - http://spring-projects.ru/understanding/hateoas/ + Это правило создания REST приложений когда нам возвращаются не только результаты но и еще URL на ресурсы. + Все данные представляются как набор ресурсов, к ним есть URL и в более сложном случае к нам возвращается набор разных + URL по которым мы можем достать все возможные в данном контексте ресурсы. Таким образом мы можем общаться с сервисом + без спецификации, все действия с резурсами на клиенте производятся через URL. + Id на клиенте не выводится - Spring Data REST expose ids - https://stackoverflow.com/questions/24936636/548473/33744785#33744785 + + Сделаем небольшую кастомизацию. + Spring Data REST settings - https://docs.spring.io/spring-data/rest/docs/current/reference/html/#getting-started.basic-settings + По ссылке есть различные настройки, по которым мы можем менять поведение Data Rest: + Сделаем: + data.rest: + basePath: /api + returnBodyOnCreate: true // возращать дело при создании ресурса. + + В низу http://localhost:8080/api/users Spring Data Rest также нам выставил ссылку по которой мы можем делать операции с users: + http://localhost:8080/api/users/search + Он проанализировал методы репозитория и выставил их наружу, имена их совпадают с именем методов и мы таже можем их кастомизировать: + + Делается это через аннотации @RestResource: + + @RestResource(rel = "by-email", path = "by-email") + @Query("SELECT u FROM User u WHERE u.email = LOWER(:email)") + Optional findByEmailIgnoreCase(String email); + + @RestResource(rel = "by-lastname", path = "by-lastname") + List findByLastNameContainingIgnoreCase(String lastName); + + Теперь этим методы буду выставлены на ружу по именам которые мы задали: + + "_links" : { + "by-email" : { + "href" : "http://localhost:8080/api/users/search/by-email{?email}", + "templated" : true + }, + "by-lastname" : { + "href" : "http://localhost:8080/api/users/search/by-lastname{?lastName}", + "templated" : true + }, + "self" : { + "href" : "http://localhost:8080/api/users/search" + } + } + } + + Тажке можно подключить зависимость: + Spring REST and HAL Browser - https://www.baeldung.com/spring-rest-hal + + org.springframework.data + spring-data-rest-hal-browser + runtime + + Заглавная страница будет доступна через API здесь hal browser в таком виде позваляет отдавать не только Get запросы но и другие запросы. + + В свежих версиях Spring, вместо spring-data-rest-hal-browser нужно использовать spring-data-rest-hal-explorer + При проблеме с Lombok с новыми JDK поднимите его версию до последней. + + В IDEA появился инструмент который позволяет отправлять запросы Tools / HTTP client / Show HTTP Request Hostory + Можно скопировать + POST http://localhost:8080/api/users + Content-Type: application/json + + { + "email": "test@test.com", + "firstName": "Test", + "lastName": "Test", + "password": "test", + "roles": [ "ROLE_USER"] + } + Что создаст нового юзера + + PATCH http://localhost:8080/api/users/1 + Content-Type: application/json + { + "lastName": "User+Last" + } + Данным запросом поменяем lastName у user 1 + HAL vs HATEOAS - https://stackoverflow.com/questions/25819477/548473 (HAL реализация правила HATEOAS в виде запросов такого вида). + + Сколько кода надобыло написать чтобы сделать это вручную, и сколько мы написали используя Spring Data Rest + + 3.2 Конфигурирование Jackson + + commit: https://github.com/StringerDM/bootjava/commit/3cba92bbb7c392e258c9ff86cfbd0ea39a2cb895 + + Поговорим немножко про сериализацию / десериализацию Jackson - это библиотека которая по умолчанию используется Spring Boot + Если мы посмотрим на вывод юзеров в нашем приложении то увидим здесь поле new: + + http://localhost:8080/api/users/ + ... + "roles" : [ "ROLE_USER" ], + "new" : false, + "_links" : { + ... + + Это метод isNew в нашем BaseEntity, по умолчанию Jackson сериализует / десериализует через getters / setters + В курсе TopJava мы решали это через переопределение ObjectMapper - для всего приложения запрещали смотреть на getters / setters + и разрешали поля. + + В Spring Boot можно сделать эти настроки через config application, мы можем сказать что + + # Jackson Serialization Issue Resolver + # jackson: + # visibility.field: any - сериализуем / десериализуем только поля + # visibility.getter: none + # visibility.setter: none - не смотрим на getters / setters + # visibility.is-getter: none и is getters (для boolean полей). + + Запустим приложение и увидим что поля isNew уйдут но зато появятся поля links - Spring Data Rest наши Entity оборачивает в ресурс + в этом ресурсе есть линки соответственно есть такие поля и он их выводит, т.е. через Spring Data Rest у нас не полчается сделатьэ + общее решение для всего приложения (поэтому уберем эту конфигурацию). И мы вместо общего решение сделаем стандартное: + @JsonIgnore + @Override + public boolean isNew() { + return id == null; + } + + Common application JSON properties - https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#json-properties + Аннотации Jackson - https://nsergey.com/jackson-annotations/ + + Также наш метод не работает для hibernate lazy объектов - используем его только для проинициализированных сущностей. + // doesn't work for hibernate lazy proxy + public int id() { + Assert.notNull(id, "Entity must have id"); + return id; + } + +
+ 3 Spring Data REST + HATEOAS + 3.1 Spring Data REST +