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
+