diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/.gitignore b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/pom.xml b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/pom.xml
new file mode 100644
index 00000000..6a637106
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+
+ ru.otus
+ jpql-exercise
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.1
+
+
+
+
+ 17
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
new file mode 100644
index 00000000..8d4b413c
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
@@ -0,0 +1,13 @@
+package ru.otus.example.ormdemo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class OrmDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(OrmDemoApplication.class, args);
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
new file mode 100644
index 00000000..c382bc41
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "avatars")
+public class Avatar {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "photo_url", nullable = false, unique = true)
+ private String photoUrl;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/Course.java
new file mode 100644
index 00000000..6c9c819a
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/Course.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "courses")
+public class Course {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/EMail.java
new file mode 100644
index 00000000..69a92ff3
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/EMail.java
@@ -0,0 +1,26 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "emails")
+public class EMail {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "email", nullable = false, unique = true)
+ private String email;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
new file mode 100644
index 00000000..71e85478
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
@@ -0,0 +1,53 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity // Указывает, что данный класс является сущностью
+@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
+public class OtusStudent {
+ @Id // Позволяет указать какое поле является идентификатором
+ @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
+ private long id;
+
+ // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+
+ // Указывает на связь между таблицами "один к одному"
+ @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
+ // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
+ @JoinColumn(name = "avatar_id")
+ private Avatar avatar;
+
+ // Указывает на связь между таблицами "один ко многим"
+ @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ @JoinColumn(name = "student_id")
+ private List emails;
+
+ // Указывает на связь между таблицами "многие ко многим"
+ @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
+ // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
+ @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
+ inverseJoinColumns = @JoinColumn(name = "course_id"))
+ private List courses;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
new file mode 100644
index 00000000..f279aa39
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
@@ -0,0 +1,58 @@
+package ru.otus.example.ormdemo.repositories;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+// @Transactional должна стоять на методе сервиса.
+// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
+// выставить у аннотации параметр readOnly в true.
+// Но это только упражнение и транзакции мы пока не проходили.
+// Поэтому, для упрощения, пока вешаем над классом репозитория
+@Transactional
+@Repository
+public class JpaOtusStudentRepository implements OtusStudentRepository {
+
+ @PersistenceContext
+ private final EntityManager em;
+
+ public JpaOtusStudentRepository(EntityManager em) {
+ this.em = em;
+ }
+
+ @Override
+ public OtusStudent save(OtusStudent student) {
+ return null;
+ }
+
+ @Override
+ public Optional findById(long id) {
+ return Optional.empty();
+ }
+
+ @Override
+ public List findAll() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List findByName(String name) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void updateNameById(long id, String name) {
+
+ }
+
+ @Override
+ public void deleteById(long id) {
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
new file mode 100644
index 00000000..44cc6a58
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
@@ -0,0 +1,18 @@
+package ru.otus.example.ormdemo.repositories;
+
+
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface OtusStudentRepository {
+ OtusStudent save(OtusStudent student);
+ Optional findById(long id);
+
+ List findAll();
+ List findByName(String name);
+
+ void updateNameById(long id, String name);
+ void deleteById(long id);
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/resources/application.yml
new file mode 100644
index 00000000..ab48d518
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/resources/application.yml
@@ -0,0 +1,18 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ sql:
+ init:
+ mode: always
+
+
+ jpa:
+ generate-ddl: false
+ hibernate:
+ ddl-auto: none
+
+ show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/resources/schema.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/resources/schema.sql
new file mode 100644
index 00000000..43a684bb
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/main/resources/schema.sql
@@ -0,0 +1,31 @@
+create table avatars(
+ id bigserial,
+ photo_url varchar(8000),
+ primary key (id)
+);
+
+create table courses(
+ id bigserial,
+ name varchar(255),
+ primary key (id)
+);
+
+create table otus_students(
+ id bigserial,
+ name varchar(255),
+ avatar_id bigint references avatars (id),
+ primary key (id)
+);
+
+create table emails(
+ id bigserial,
+ student_id bigint references otus_students(id) on delete cascade,
+ email varchar(255),
+ primary key (id)
+);
+
+create table student_courses(
+ student_id bigint references otus_students(id) on delete cascade,
+ course_id bigint references courses(id),
+ primary key (student_id, course_id)
+);
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
new file mode 100644
index 00000000..f41c7dd5
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
@@ -0,0 +1,126 @@
+package ru.otus.example.ormdemo.repositories;
+
+import lombok.val;
+import org.hibernate.SessionFactory;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+import org.springframework.context.annotation.Import;
+import ru.otus.example.ormdemo.models.OtusStudent;
+import ru.otus.example.ormdemo.models.Avatar;
+import ru.otus.example.ormdemo.models.Course;
+import ru.otus.example.ormdemo.models.EMail;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
+@DataJpaTest
+@Import(JpaOtusStudentRepository.class)
+class JpaOtusStudentRepositoryTest {
+
+ private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
+ private static final long FIRST_STUDENT_ID = 1L;
+ private static final String FIRST_STUDENT_NAME = "student_01";
+
+ private static final int EXPECTED_QUERIES_COUNT = 31;
+
+ private static final String STUDENT_AVATAR_URL = "где-то там";
+ private static final String STUDENT_EMAIL = "any@mail.com";
+ private static final String COURSE_NAME = "Spring";
+ private static final String STUDENT_NAME = "Вася";
+
+ @Autowired
+ private JpaOtusStudentRepository repositoryJpa;
+
+ @Autowired
+ private TestEntityManager em;
+
+ @DisplayName(" должен загружать информацию о нужном студенте по его id")
+ @Test
+ void shouldFindExpectedStudentById() {
+ val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
+ val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ assertThat(optionalActualStudent).isPresent().get()
+ .usingRecursiveComparison().isEqualTo(expectedStudent);
+ }
+
+ @DisplayName("должен загружать список всех студентов с полной информацией о них")
+ @Test
+ void shouldReturnCorrectStudentsListWithAllInfo() {
+ SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
+ .unwrap(SessionFactory.class);
+ sessionFactory.getStatistics().setStatisticsEnabled(true);
+
+
+ System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
+ val students = repositoryJpa.findAll();
+ assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
+ .allMatch(s -> !s.getName().equals(""))
+ .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
+ .allMatch(s -> s.getAvatar().getPhotoUrl() != null)
+ .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
+ System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
+ assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
+ }
+
+ @DisplayName(" должен корректно сохранять всю информацию о студенте")
+ @Test
+ void shouldSaveAllStudentInfo() {
+ val avatar = new Avatar(0, STUDENT_AVATAR_URL);
+ val email = new EMail(0, STUDENT_EMAIL);
+ val emails = Collections.singletonList(email);
+
+ val course = new Course(0, COURSE_NAME);
+ val courses = Collections.singletonList(course);
+
+
+ val vasya = new OtusStudent(0, STUDENT_NAME, avatar, emails, courses);
+ repositoryJpa.save(vasya);
+ assertThat(vasya.getId()).isGreaterThan(0);
+
+ val actualStudent = em.find(OtusStudent.class, vasya.getId());
+ assertThat(actualStudent).isNotNull().matches(s -> !s.getName().equals(""))
+ .matches(s -> s.getCourses() != null && s.getCourses().size() > 0 && s.getCourses().get(0).getId() > 0)
+ .matches(s -> s.getAvatar() != null)
+ .matches(s -> s.getEmails() != null && s.getEmails().size() > 0);
+ }
+
+ @DisplayName(" должен загружать информацию о нужном студенте по его имени")
+ @Test
+ void shouldFindExpectedStudentByName() {
+ val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ List students = repositoryJpa.findByName(FIRST_STUDENT_NAME);
+ assertThat(students).containsOnlyOnce(firstStudent);
+ }
+
+ @DisplayName(" должен изменять имя заданного студента по его id")
+ @Test
+ void shouldUpdateStudentNameById() {
+ val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ String oldName = firstStudent.getName();
+ em.detach(firstStudent);
+
+ repositoryJpa.updateNameById(FIRST_STUDENT_ID, STUDENT_NAME);
+ val updatedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+
+ assertThat(updatedStudent.getName()).isNotEqualTo(oldName).isEqualTo(STUDENT_NAME);
+ }
+
+ @DisplayName(" должен удалять заданного студента по его id")
+ @Test
+ void shouldDeleteStudentNameById() {
+ val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ assertThat(firstStudent).isNotNull();
+ em.detach(firstStudent);
+
+ repositoryJpa.deleteById(FIRST_STUDENT_ID);
+ val deletedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+
+ assertThat(deletedStudent).isNull();
+ }
+}
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/resources/application.yml
new file mode 100644
index 00000000..7d216f89
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/resources/application.yml
@@ -0,0 +1,20 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ sql:
+ init:
+ mode: always
+
+
+ jpa:
+ generate-ddl: false
+ #generate-ddl: true
+ hibernate:
+ ddl-auto: none
+ #ddl-auto: create-drop
+
+ #show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/resources/data.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/resources/data.sql
new file mode 100644
index 00000000..a8db6b85
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-exercise/src/test/resources/data.sql
@@ -0,0 +1,29 @@
+insert into avatars(photo_url)
+values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'),
+ ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10');
+
+insert into courses(name)
+values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'),
+ ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11');
+
+insert into otus_students(name, avatar_id)
+values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5),
+ ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10);
+
+
+insert into emails(email, student_id)
+values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4),
+ ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10);
+
+
+insert into student_courses(student_id, course_id)
+values (1, 1), (1, 2), (1, 3),
+ (2, 2), (2, 4), (2, 5),
+ (3, 3), (3, 6), (3, 7),
+ (4, 4), (4, 8), (4, 9),
+ (5, 5), (5, 10), (5, 1),
+ (6, 6), (6, 2), (6, 3),
+ (7, 7), (7, 4), (7, 5),
+ (8, 8), (8, 6), (8, 7),
+ (9, 9), (9, 8), (9, 10),
+ (10, 10), (10, 1), (10, 2);
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/.gitignore b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/pom.xml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/pom.xml
new file mode 100644
index 00000000..a8ec8ffd
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+
+ ru.otus
+ jpql-solution-01
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.1
+
+
+
+
+ 17
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
new file mode 100644
index 00000000..8d4b413c
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
@@ -0,0 +1,13 @@
+package ru.otus.example.ormdemo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class OrmDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(OrmDemoApplication.class, args);
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
new file mode 100644
index 00000000..c382bc41
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "avatars")
+public class Avatar {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "photo_url", nullable = false, unique = true)
+ private String photoUrl;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/Course.java
new file mode 100644
index 00000000..5ee3b725
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/Course.java
@@ -0,0 +1,26 @@
+package ru.otus.example.ormdemo.models;
+
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "courses")
+public class Course {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/EMail.java
new file mode 100644
index 00000000..69a92ff3
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/EMail.java
@@ -0,0 +1,26 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "emails")
+public class EMail {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "email", nullable = false, unique = true)
+ private String email;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
new file mode 100644
index 00000000..71e85478
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
@@ -0,0 +1,53 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity // Указывает, что данный класс является сущностью
+@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
+public class OtusStudent {
+ @Id // Позволяет указать какое поле является идентификатором
+ @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
+ private long id;
+
+ // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+
+ // Указывает на связь между таблицами "один к одному"
+ @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
+ // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
+ @JoinColumn(name = "avatar_id")
+ private Avatar avatar;
+
+ // Указывает на связь между таблицами "один ко многим"
+ @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ @JoinColumn(name = "student_id")
+ private List emails;
+
+ // Указывает на связь между таблицами "многие ко многим"
+ @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
+ // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
+ @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
+ inverseJoinColumns = @JoinColumn(name = "course_id"))
+ private List courses;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
new file mode 100644
index 00000000..8b8b767f
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
@@ -0,0 +1,64 @@
+package ru.otus.example.ormdemo.repositories;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+// @Transactional должна стоять на методе сервиса.
+// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
+// выставить у аннотации параметр readOnly в true.
+// Но это только упражнение и транзакции мы пока не проходили.
+// Поэтому, для упрощения, пока вешаем над классом репозитория
+@Transactional
+@Repository
+public class JpaOtusStudentRepository implements OtusStudentRepository {
+
+ @PersistenceContext
+ private final EntityManager em;
+
+ public JpaOtusStudentRepository(EntityManager em) {
+ this.em = em;
+ }
+
+ @Override
+ public OtusStudent save(OtusStudent student) {
+ if (student.getId() == 0) {
+ em.persist(student);
+ return student;
+ }
+ return em.merge(student);
+ }
+
+ @Override
+ public Optional findById(long id) {
+ return Optional.ofNullable(em.find(OtusStudent.class, id));
+ }
+
+ @Override
+ public List findAll() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List findByName(String name) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void updateNameById(long id, String name) {
+
+ }
+
+ @Override
+ public void deleteById(long id) {
+
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
new file mode 100644
index 00000000..44cc6a58
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
@@ -0,0 +1,18 @@
+package ru.otus.example.ormdemo.repositories;
+
+
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface OtusStudentRepository {
+ OtusStudent save(OtusStudent student);
+ Optional findById(long id);
+
+ List findAll();
+ List findByName(String name);
+
+ void updateNameById(long id, String name);
+ void deleteById(long id);
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/resources/application.yml
new file mode 100644
index 00000000..ab48d518
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/resources/application.yml
@@ -0,0 +1,18 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ sql:
+ init:
+ mode: always
+
+
+ jpa:
+ generate-ddl: false
+ hibernate:
+ ddl-auto: none
+
+ show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/resources/schema.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/resources/schema.sql
new file mode 100644
index 00000000..43a684bb
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/main/resources/schema.sql
@@ -0,0 +1,31 @@
+create table avatars(
+ id bigserial,
+ photo_url varchar(8000),
+ primary key (id)
+);
+
+create table courses(
+ id bigserial,
+ name varchar(255),
+ primary key (id)
+);
+
+create table otus_students(
+ id bigserial,
+ name varchar(255),
+ avatar_id bigint references avatars (id),
+ primary key (id)
+);
+
+create table emails(
+ id bigserial,
+ student_id bigint references otus_students(id) on delete cascade,
+ email varchar(255),
+ primary key (id)
+);
+
+create table student_courses(
+ student_id bigint references otus_students(id) on delete cascade,
+ course_id bigint references courses(id),
+ primary key (student_id, course_id)
+);
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
new file mode 100644
index 00000000..f41c7dd5
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
@@ -0,0 +1,126 @@
+package ru.otus.example.ormdemo.repositories;
+
+import lombok.val;
+import org.hibernate.SessionFactory;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+import org.springframework.context.annotation.Import;
+import ru.otus.example.ormdemo.models.OtusStudent;
+import ru.otus.example.ormdemo.models.Avatar;
+import ru.otus.example.ormdemo.models.Course;
+import ru.otus.example.ormdemo.models.EMail;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
+@DataJpaTest
+@Import(JpaOtusStudentRepository.class)
+class JpaOtusStudentRepositoryTest {
+
+ private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
+ private static final long FIRST_STUDENT_ID = 1L;
+ private static final String FIRST_STUDENT_NAME = "student_01";
+
+ private static final int EXPECTED_QUERIES_COUNT = 31;
+
+ private static final String STUDENT_AVATAR_URL = "где-то там";
+ private static final String STUDENT_EMAIL = "any@mail.com";
+ private static final String COURSE_NAME = "Spring";
+ private static final String STUDENT_NAME = "Вася";
+
+ @Autowired
+ private JpaOtusStudentRepository repositoryJpa;
+
+ @Autowired
+ private TestEntityManager em;
+
+ @DisplayName(" должен загружать информацию о нужном студенте по его id")
+ @Test
+ void shouldFindExpectedStudentById() {
+ val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
+ val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ assertThat(optionalActualStudent).isPresent().get()
+ .usingRecursiveComparison().isEqualTo(expectedStudent);
+ }
+
+ @DisplayName("должен загружать список всех студентов с полной информацией о них")
+ @Test
+ void shouldReturnCorrectStudentsListWithAllInfo() {
+ SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
+ .unwrap(SessionFactory.class);
+ sessionFactory.getStatistics().setStatisticsEnabled(true);
+
+
+ System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
+ val students = repositoryJpa.findAll();
+ assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
+ .allMatch(s -> !s.getName().equals(""))
+ .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
+ .allMatch(s -> s.getAvatar().getPhotoUrl() != null)
+ .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
+ System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
+ assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
+ }
+
+ @DisplayName(" должен корректно сохранять всю информацию о студенте")
+ @Test
+ void shouldSaveAllStudentInfo() {
+ val avatar = new Avatar(0, STUDENT_AVATAR_URL);
+ val email = new EMail(0, STUDENT_EMAIL);
+ val emails = Collections.singletonList(email);
+
+ val course = new Course(0, COURSE_NAME);
+ val courses = Collections.singletonList(course);
+
+
+ val vasya = new OtusStudent(0, STUDENT_NAME, avatar, emails, courses);
+ repositoryJpa.save(vasya);
+ assertThat(vasya.getId()).isGreaterThan(0);
+
+ val actualStudent = em.find(OtusStudent.class, vasya.getId());
+ assertThat(actualStudent).isNotNull().matches(s -> !s.getName().equals(""))
+ .matches(s -> s.getCourses() != null && s.getCourses().size() > 0 && s.getCourses().get(0).getId() > 0)
+ .matches(s -> s.getAvatar() != null)
+ .matches(s -> s.getEmails() != null && s.getEmails().size() > 0);
+ }
+
+ @DisplayName(" должен загружать информацию о нужном студенте по его имени")
+ @Test
+ void shouldFindExpectedStudentByName() {
+ val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ List students = repositoryJpa.findByName(FIRST_STUDENT_NAME);
+ assertThat(students).containsOnlyOnce(firstStudent);
+ }
+
+ @DisplayName(" должен изменять имя заданного студента по его id")
+ @Test
+ void shouldUpdateStudentNameById() {
+ val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ String oldName = firstStudent.getName();
+ em.detach(firstStudent);
+
+ repositoryJpa.updateNameById(FIRST_STUDENT_ID, STUDENT_NAME);
+ val updatedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+
+ assertThat(updatedStudent.getName()).isNotEqualTo(oldName).isEqualTo(STUDENT_NAME);
+ }
+
+ @DisplayName(" должен удалять заданного студента по его id")
+ @Test
+ void shouldDeleteStudentNameById() {
+ val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ assertThat(firstStudent).isNotNull();
+ em.detach(firstStudent);
+
+ repositoryJpa.deleteById(FIRST_STUDENT_ID);
+ val deletedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+
+ assertThat(deletedStudent).isNull();
+ }
+}
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/resources/application.yml
new file mode 100644
index 00000000..7d216f89
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/resources/application.yml
@@ -0,0 +1,20 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ sql:
+ init:
+ mode: always
+
+
+ jpa:
+ generate-ddl: false
+ #generate-ddl: true
+ hibernate:
+ ddl-auto: none
+ #ddl-auto: create-drop
+
+ #show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/resources/data.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/resources/data.sql
new file mode 100644
index 00000000..a8db6b85
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-01/src/test/resources/data.sql
@@ -0,0 +1,29 @@
+insert into avatars(photo_url)
+values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'),
+ ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10');
+
+insert into courses(name)
+values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'),
+ ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11');
+
+insert into otus_students(name, avatar_id)
+values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5),
+ ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10);
+
+
+insert into emails(email, student_id)
+values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4),
+ ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10);
+
+
+insert into student_courses(student_id, course_id)
+values (1, 1), (1, 2), (1, 3),
+ (2, 2), (2, 4), (2, 5),
+ (3, 3), (3, 6), (3, 7),
+ (4, 4), (4, 8), (4, 9),
+ (5, 5), (5, 10), (5, 1),
+ (6, 6), (6, 2), (6, 3),
+ (7, 7), (7, 4), (7, 5),
+ (8, 8), (8, 6), (8, 7),
+ (9, 9), (9, 8), (9, 10),
+ (10, 10), (10, 1), (10, 2);
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/.gitignore b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/pom.xml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/pom.xml
new file mode 100644
index 00000000..f52eb76d
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+
+ ru.otus
+ jpql-solution-02
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.1
+
+
+
+
+ 17
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
new file mode 100644
index 00000000..8d4b413c
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
@@ -0,0 +1,13 @@
+package ru.otus.example.ormdemo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class OrmDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(OrmDemoApplication.class, args);
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
new file mode 100644
index 00000000..c382bc41
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "avatars")
+public class Avatar {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "photo_url", nullable = false, unique = true)
+ private String photoUrl;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/Course.java
new file mode 100644
index 00000000..6c9c819a
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/Course.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "courses")
+public class Course {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/EMail.java
new file mode 100644
index 00000000..69a92ff3
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/EMail.java
@@ -0,0 +1,26 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "emails")
+public class EMail {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "email", nullable = false, unique = true)
+ private String email;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
new file mode 100644
index 00000000..71e85478
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
@@ -0,0 +1,53 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity // Указывает, что данный класс является сущностью
+@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
+public class OtusStudent {
+ @Id // Позволяет указать какое поле является идентификатором
+ @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
+ private long id;
+
+ // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+
+ // Указывает на связь между таблицами "один к одному"
+ @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
+ // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
+ @JoinColumn(name = "avatar_id")
+ private Avatar avatar;
+
+ // Указывает на связь между таблицами "один ко многим"
+ @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ @JoinColumn(name = "student_id")
+ private List emails;
+
+ // Указывает на связь между таблицами "многие ко многим"
+ @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
+ // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
+ @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
+ inverseJoinColumns = @JoinColumn(name = "course_id"))
+ private List courses;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
new file mode 100644
index 00000000..182ed8c1
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
@@ -0,0 +1,69 @@
+package ru.otus.example.ormdemo.repositories;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.TypedQuery;
+import java.util.List;
+import java.util.Optional;
+
+// @Transactional должна стоять на методе сервиса.
+// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
+// выставить у аннотации параметр readOnly в true.
+// Но это только упражнение и транзакции мы пока не проходили.
+// Поэтому, для упрощения, пока вешаем над классом репозитория
+@Transactional
+@Repository
+public class JpaOtusStudentRepository implements OtusStudentRepository {
+
+ @PersistenceContext
+ private final EntityManager em;
+
+ public JpaOtusStudentRepository(EntityManager em) {
+ this.em = em;
+ }
+
+ @Override
+ public OtusStudent save(OtusStudent student) {
+ if (student.getId() == 0) {
+ em.persist(student);
+ return student;
+ }
+ return em.merge(student);
+ }
+
+ @Override
+ public Optional findById(long id) {
+ return Optional.ofNullable(em.find(OtusStudent.class, id));
+ }
+
+ @Override
+ public List findAll() {
+ return em.createQuery("select s from OtusStudent s", OtusStudent.class)
+ .getResultList();
+ }
+
+ @Override
+ public List findByName(String name) {
+ TypedQuery query = em.createQuery("select s " +
+ "from OtusStudent s " +
+ "where s.name = :name",
+ OtusStudent.class);
+ query.setParameter("name", name);
+ return query.getResultList();
+ }
+
+ @Override
+ public void updateNameById(long id, String name) {
+
+ }
+
+ @Override
+ public void deleteById(long id) {
+
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
new file mode 100644
index 00000000..44cc6a58
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
@@ -0,0 +1,18 @@
+package ru.otus.example.ormdemo.repositories;
+
+
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface OtusStudentRepository {
+ OtusStudent save(OtusStudent student);
+ Optional findById(long id);
+
+ List findAll();
+ List findByName(String name);
+
+ void updateNameById(long id, String name);
+ void deleteById(long id);
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/resources/application.yml
new file mode 100644
index 00000000..ab48d518
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/resources/application.yml
@@ -0,0 +1,18 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ sql:
+ init:
+ mode: always
+
+
+ jpa:
+ generate-ddl: false
+ hibernate:
+ ddl-auto: none
+
+ show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/resources/schema.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/resources/schema.sql
new file mode 100644
index 00000000..43a684bb
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/main/resources/schema.sql
@@ -0,0 +1,31 @@
+create table avatars(
+ id bigserial,
+ photo_url varchar(8000),
+ primary key (id)
+);
+
+create table courses(
+ id bigserial,
+ name varchar(255),
+ primary key (id)
+);
+
+create table otus_students(
+ id bigserial,
+ name varchar(255),
+ avatar_id bigint references avatars (id),
+ primary key (id)
+);
+
+create table emails(
+ id bigserial,
+ student_id bigint references otus_students(id) on delete cascade,
+ email varchar(255),
+ primary key (id)
+);
+
+create table student_courses(
+ student_id bigint references otus_students(id) on delete cascade,
+ course_id bigint references courses(id),
+ primary key (student_id, course_id)
+);
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
new file mode 100644
index 00000000..f41c7dd5
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
@@ -0,0 +1,126 @@
+package ru.otus.example.ormdemo.repositories;
+
+import lombok.val;
+import org.hibernate.SessionFactory;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+import org.springframework.context.annotation.Import;
+import ru.otus.example.ormdemo.models.OtusStudent;
+import ru.otus.example.ormdemo.models.Avatar;
+import ru.otus.example.ormdemo.models.Course;
+import ru.otus.example.ormdemo.models.EMail;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
+@DataJpaTest
+@Import(JpaOtusStudentRepository.class)
+class JpaOtusStudentRepositoryTest {
+
+ private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
+ private static final long FIRST_STUDENT_ID = 1L;
+ private static final String FIRST_STUDENT_NAME = "student_01";
+
+ private static final int EXPECTED_QUERIES_COUNT = 31;
+
+ private static final String STUDENT_AVATAR_URL = "где-то там";
+ private static final String STUDENT_EMAIL = "any@mail.com";
+ private static final String COURSE_NAME = "Spring";
+ private static final String STUDENT_NAME = "Вася";
+
+ @Autowired
+ private JpaOtusStudentRepository repositoryJpa;
+
+ @Autowired
+ private TestEntityManager em;
+
+ @DisplayName(" должен загружать информацию о нужном студенте по его id")
+ @Test
+ void shouldFindExpectedStudentById() {
+ val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
+ val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ assertThat(optionalActualStudent).isPresent().get()
+ .usingRecursiveComparison().isEqualTo(expectedStudent);
+ }
+
+ @DisplayName("должен загружать список всех студентов с полной информацией о них")
+ @Test
+ void shouldReturnCorrectStudentsListWithAllInfo() {
+ SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
+ .unwrap(SessionFactory.class);
+ sessionFactory.getStatistics().setStatisticsEnabled(true);
+
+
+ System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
+ val students = repositoryJpa.findAll();
+ assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
+ .allMatch(s -> !s.getName().equals(""))
+ .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
+ .allMatch(s -> s.getAvatar().getPhotoUrl() != null)
+ .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
+ System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
+ assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
+ }
+
+ @DisplayName(" должен корректно сохранять всю информацию о студенте")
+ @Test
+ void shouldSaveAllStudentInfo() {
+ val avatar = new Avatar(0, STUDENT_AVATAR_URL);
+ val email = new EMail(0, STUDENT_EMAIL);
+ val emails = Collections.singletonList(email);
+
+ val course = new Course(0, COURSE_NAME);
+ val courses = Collections.singletonList(course);
+
+
+ val vasya = new OtusStudent(0, STUDENT_NAME, avatar, emails, courses);
+ repositoryJpa.save(vasya);
+ assertThat(vasya.getId()).isGreaterThan(0);
+
+ val actualStudent = em.find(OtusStudent.class, vasya.getId());
+ assertThat(actualStudent).isNotNull().matches(s -> !s.getName().equals(""))
+ .matches(s -> s.getCourses() != null && s.getCourses().size() > 0 && s.getCourses().get(0).getId() > 0)
+ .matches(s -> s.getAvatar() != null)
+ .matches(s -> s.getEmails() != null && s.getEmails().size() > 0);
+ }
+
+ @DisplayName(" должен загружать информацию о нужном студенте по его имени")
+ @Test
+ void shouldFindExpectedStudentByName() {
+ val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ List students = repositoryJpa.findByName(FIRST_STUDENT_NAME);
+ assertThat(students).containsOnlyOnce(firstStudent);
+ }
+
+ @DisplayName(" должен изменять имя заданного студента по его id")
+ @Test
+ void shouldUpdateStudentNameById() {
+ val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ String oldName = firstStudent.getName();
+ em.detach(firstStudent);
+
+ repositoryJpa.updateNameById(FIRST_STUDENT_ID, STUDENT_NAME);
+ val updatedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+
+ assertThat(updatedStudent.getName()).isNotEqualTo(oldName).isEqualTo(STUDENT_NAME);
+ }
+
+ @DisplayName(" должен удалять заданного студента по его id")
+ @Test
+ void shouldDeleteStudentNameById() {
+ val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ assertThat(firstStudent).isNotNull();
+ em.detach(firstStudent);
+
+ repositoryJpa.deleteById(FIRST_STUDENT_ID);
+ val deletedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+
+ assertThat(deletedStudent).isNull();
+ }
+}
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/resources/application.yml
new file mode 100644
index 00000000..7d216f89
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/resources/application.yml
@@ -0,0 +1,20 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ sql:
+ init:
+ mode: always
+
+
+ jpa:
+ generate-ddl: false
+ #generate-ddl: true
+ hibernate:
+ ddl-auto: none
+ #ddl-auto: create-drop
+
+ #show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/resources/data.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/resources/data.sql
new file mode 100644
index 00000000..a8db6b85
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-02/src/test/resources/data.sql
@@ -0,0 +1,29 @@
+insert into avatars(photo_url)
+values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'),
+ ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10');
+
+insert into courses(name)
+values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'),
+ ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11');
+
+insert into otus_students(name, avatar_id)
+values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5),
+ ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10);
+
+
+insert into emails(email, student_id)
+values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4),
+ ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10);
+
+
+insert into student_courses(student_id, course_id)
+values (1, 1), (1, 2), (1, 3),
+ (2, 2), (2, 4), (2, 5),
+ (3, 3), (3, 6), (3, 7),
+ (4, 4), (4, 8), (4, 9),
+ (5, 5), (5, 10), (5, 1),
+ (6, 6), (6, 2), (6, 3),
+ (7, 7), (7, 4), (7, 5),
+ (8, 8), (8, 6), (8, 7),
+ (9, 9), (9, 8), (9, 10),
+ (10, 10), (10, 1), (10, 2);
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/.gitignore b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/pom.xml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/pom.xml
new file mode 100644
index 00000000..c63730e4
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+
+ ru.otus
+ jpql-solution-03
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.1
+
+
+
+
+ 17
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
new file mode 100644
index 00000000..8d4b413c
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
@@ -0,0 +1,13 @@
+package ru.otus.example.ormdemo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class OrmDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(OrmDemoApplication.class, args);
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
new file mode 100644
index 00000000..c382bc41
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "avatars")
+public class Avatar {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "photo_url", nullable = false, unique = true)
+ private String photoUrl;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/Course.java
new file mode 100644
index 00000000..6c9c819a
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/Course.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "courses")
+public class Course {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/EMail.java
new file mode 100644
index 00000000..69a92ff3
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/EMail.java
@@ -0,0 +1,26 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "emails")
+public class EMail {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "email", nullable = false, unique = true)
+ private String email;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
new file mode 100644
index 00000000..71e85478
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
@@ -0,0 +1,53 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity // Указывает, что данный класс является сущностью
+@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
+public class OtusStudent {
+ @Id // Позволяет указать какое поле является идентификатором
+ @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
+ private long id;
+
+ // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+
+ // Указывает на связь между таблицами "один к одному"
+ @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
+ // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
+ @JoinColumn(name = "avatar_id")
+ private Avatar avatar;
+
+ // Указывает на связь между таблицами "один ко многим"
+ @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ @JoinColumn(name = "student_id")
+ private List emails;
+
+ // Указывает на связь между таблицами "многие ко многим"
+ @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
+ // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
+ @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
+ inverseJoinColumns = @JoinColumn(name = "course_id"))
+ private List courses;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
new file mode 100644
index 00000000..df02c2d9
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
@@ -0,0 +1,77 @@
+package ru.otus.example.ormdemo.repositories;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import java.util.List;
+import java.util.Optional;
+
+// @Transactional должна стоять на методе сервиса.
+// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
+// выставить у аннотации параметр readOnly в true.
+// Но это только упражнение и транзакции мы пока не проходили.
+// Поэтому, для упрощения, пока вешаем над классом репозитория
+@Transactional
+@Repository
+public class JpaOtusStudentRepository implements OtusStudentRepository {
+
+ @PersistenceContext
+ private EntityManager em;
+
+ @Override
+ public OtusStudent save(OtusStudent student) {
+ if (student.getId() == 0) {
+ em.persist(student);
+ return student;
+ }
+ return em.merge(student);
+ }
+
+ @Override
+ public Optional findById(long id) {
+ return Optional.ofNullable(em.find(OtusStudent.class, id));
+ }
+
+ @Override
+ public List findAll() {
+ return em.createQuery("select s from OtusStudent s", OtusStudent.class)
+ .getResultList();
+ }
+
+ @Override
+ public List findByName(String name) {
+ TypedQuery query = em.createQuery("select s " +
+ "from OtusStudent s " +
+ "where s.name = :name",
+ OtusStudent.class);
+ query.setParameter("name", name);
+ return query.getResultList();
+ }
+
+ // Только для примера, в реальности JPQL лучше использовать только для массовых операций
+ @Override
+ public void updateNameById(long id, String name) {
+ Query query = em.createQuery("update OtusStudent s " +
+ "set s.name = :name " +
+ "where s.id = :id");
+ query.setParameter("name", name);
+ query.setParameter("id", id);
+ query.executeUpdate();
+ }
+
+ // Только для примера, в реальности JPQL лучше использовать только для массовых операций
+ @Override
+ public void deleteById(long id) {
+ Query query = em.createQuery("delete " +
+ "from OtusStudent s " +
+ "where s.id = :id");
+ query.setParameter("id", id);
+ query.executeUpdate();
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
new file mode 100644
index 00000000..44cc6a58
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
@@ -0,0 +1,18 @@
+package ru.otus.example.ormdemo.repositories;
+
+
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface OtusStudentRepository {
+ OtusStudent save(OtusStudent student);
+ Optional findById(long id);
+
+ List findAll();
+ List findByName(String name);
+
+ void updateNameById(long id, String name);
+ void deleteById(long id);
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/resources/application.yml
new file mode 100644
index 00000000..ab48d518
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/resources/application.yml
@@ -0,0 +1,18 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ sql:
+ init:
+ mode: always
+
+
+ jpa:
+ generate-ddl: false
+ hibernate:
+ ddl-auto: none
+
+ show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/resources/schema.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/resources/schema.sql
new file mode 100644
index 00000000..43a684bb
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/main/resources/schema.sql
@@ -0,0 +1,31 @@
+create table avatars(
+ id bigserial,
+ photo_url varchar(8000),
+ primary key (id)
+);
+
+create table courses(
+ id bigserial,
+ name varchar(255),
+ primary key (id)
+);
+
+create table otus_students(
+ id bigserial,
+ name varchar(255),
+ avatar_id bigint references avatars (id),
+ primary key (id)
+);
+
+create table emails(
+ id bigserial,
+ student_id bigint references otus_students(id) on delete cascade,
+ email varchar(255),
+ primary key (id)
+);
+
+create table student_courses(
+ student_id bigint references otus_students(id) on delete cascade,
+ course_id bigint references courses(id),
+ primary key (student_id, course_id)
+);
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
new file mode 100644
index 00000000..89482782
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
@@ -0,0 +1,127 @@
+package ru.otus.example.ormdemo.repositories;
+
+import lombok.val;
+import org.hibernate.SessionFactory;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+import org.springframework.context.annotation.Import;
+import ru.otus.example.ormdemo.models.OtusStudent;
+import ru.otus.example.ormdemo.models.Avatar;
+import ru.otus.example.ormdemo.models.Course;
+import ru.otus.example.ormdemo.models.EMail;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
+@DataJpaTest
+@Import(JpaOtusStudentRepository.class)
+class JpaOtusStudentRepositoryTest {
+
+ private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
+ private static final long FIRST_STUDENT_ID = 1L;
+ private static final String FIRST_STUDENT_NAME = "student_01";
+
+ private static final int EXPECTED_QUERIES_COUNT = 31;
+
+ private static final String STUDENT_AVATAR_URL = "где-то там";
+ private static final String STUDENT_EMAIL = "any@mail.com";
+ private static final String COURSE_NAME = "Spring";
+ private static final String STUDENT_NAME = "Вася";
+
+ @Autowired
+ private JpaOtusStudentRepository repositoryJpa;
+
+ @Autowired
+ private TestEntityManager em;
+
+ @DisplayName(" должен загружать информацию о нужном студенте по его id")
+ @Test
+ void shouldFindExpectedStudentById() {
+ val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
+ val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ assertThat(optionalActualStudent).isPresent().get()
+ .usingRecursiveComparison().isEqualTo(expectedStudent);
+ }
+
+ @DisplayName("должен загружать список всех студентов с полной информацией о них")
+ @Test
+ void shouldReturnCorrectStudentsListWithAllInfo() {
+ SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
+ .unwrap(SessionFactory.class);
+ sessionFactory.getStatistics().setStatisticsEnabled(true);
+
+
+ System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
+ val students = repositoryJpa.findAll();
+ assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
+ .allMatch(s -> !s.getName().equals(""))
+ .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
+ .allMatch(s -> s.getAvatar().getPhotoUrl() != null)
+ .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
+ System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
+ assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
+ }
+
+ @DisplayName(" должен корректно сохранять всю информацию о студенте")
+ @Test
+ void shouldSaveAllStudentInfo() {
+ val avatar = new Avatar(0, STUDENT_AVATAR_URL);
+ val email = new EMail(0, STUDENT_EMAIL);
+ val emails = Collections.singletonList(email);
+
+ val course = new Course(0, COURSE_NAME);
+ val courses = Collections.singletonList(course);
+
+
+ val vasya = new OtusStudent(0, STUDENT_NAME, avatar, emails, courses);
+ repositoryJpa.save(vasya);
+ assertThat(vasya.getId()).isGreaterThan(0);
+
+ val actualStudent = em.find(OtusStudent.class, vasya.getId());
+ assertThat(actualStudent).isNotNull().matches(s -> !s.getName().equals(""))
+ .matches(s -> s.getCourses() != null && s.getCourses().size() > 0 && s.getCourses().get(0).getId() > 0)
+ .matches(s -> s.getAvatar() != null)
+ .matches(s -> s.getEmails() != null && s.getEmails().size() > 0);
+ }
+
+ @DisplayName(" должен загружать информацию о нужном студенте по его имени")
+ @Test
+ void shouldFindExpectedStudentByName() {
+ val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ List students = repositoryJpa.findByName(FIRST_STUDENT_NAME);
+ assertThat(students).containsOnlyOnce(firstStudent);
+ }
+
+ @DisplayName(" должен изменять имя заданного студента по его id")
+ @Test
+ void shouldUpdateStudentNameById() {
+ val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ String oldName = firstStudent.getName();
+ em.detach(firstStudent);
+
+ repositoryJpa.updateNameById(FIRST_STUDENT_ID, STUDENT_NAME);
+ val updatedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+
+
+ assertThat(updatedStudent.getName()).isNotEqualTo(oldName).isEqualTo(STUDENT_NAME);
+ }
+
+ @DisplayName(" должен удалять заданного студента по его id")
+ @Test
+ void shouldDeleteStudentNameById() {
+ val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ assertThat(firstStudent).isNotNull();
+ em.detach(firstStudent);
+
+ repositoryJpa.deleteById(FIRST_STUDENT_ID);
+ val deletedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+
+ assertThat(deletedStudent).isNull();
+ }
+}
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/resources/application.yml
new file mode 100644
index 00000000..7d216f89
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/resources/application.yml
@@ -0,0 +1,20 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ sql:
+ init:
+ mode: always
+
+
+ jpa:
+ generate-ddl: false
+ #generate-ddl: true
+ hibernate:
+ ddl-auto: none
+ #ddl-auto: create-drop
+
+ #show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/resources/data.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/resources/data.sql
new file mode 100644
index 00000000..a8db6b85
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-03/src/test/resources/data.sql
@@ -0,0 +1,29 @@
+insert into avatars(photo_url)
+values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'),
+ ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10');
+
+insert into courses(name)
+values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'),
+ ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11');
+
+insert into otus_students(name, avatar_id)
+values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5),
+ ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10);
+
+
+insert into emails(email, student_id)
+values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4),
+ ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10);
+
+
+insert into student_courses(student_id, course_id)
+values (1, 1), (1, 2), (1, 3),
+ (2, 2), (2, 4), (2, 5),
+ (3, 3), (3, 6), (3, 7),
+ (4, 4), (4, 8), (4, 9),
+ (5, 5), (5, 10), (5, 1),
+ (6, 6), (6, 2), (6, 3),
+ (7, 7), (7, 4), (7, 5),
+ (8, 8), (8, 6), (8, 7),
+ (9, 9), (9, 8), (9, 10),
+ (10, 10), (10, 1), (10, 2);
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/.gitignore b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/pom.xml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/pom.xml
new file mode 100644
index 00000000..96c0ae71
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+
+ ru.otus
+ jpql-solution-04
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.1
+
+
+
+
+ 17
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
new file mode 100644
index 00000000..8d4b413c
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
@@ -0,0 +1,13 @@
+package ru.otus.example.ormdemo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class OrmDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(OrmDemoApplication.class, args);
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
new file mode 100644
index 00000000..c382bc41
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "avatars")
+public class Avatar {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "photo_url", nullable = false, unique = true)
+ private String photoUrl;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/Course.java
new file mode 100644
index 00000000..6c9c819a
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/Course.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "courses")
+public class Course {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/EMail.java
new file mode 100644
index 00000000..69a92ff3
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/EMail.java
@@ -0,0 +1,26 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "emails")
+public class EMail {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "email", nullable = false, unique = true)
+ private String email;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
new file mode 100644
index 00000000..357c2224
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
@@ -0,0 +1,58 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.NamedAttributeNode;
+import jakarta.persistence.NamedEntityGraph;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity // Указывает, что данный класс является сущностью
+@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
+// Позволяет указать какие связи родительской сущности загружать в одном с ней запросе
+@NamedEntityGraph(name = "otus-student-avatars-entity-graph",
+ attributeNodes = {@NamedAttributeNode("avatar")})
+public class OtusStudent {
+ @Id // Позволяет указать какое поле является идентификатором
+ @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
+ private long id;
+
+ // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+
+ // Указывает на связь между таблицами "один к одному"
+ @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
+ // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
+ @JoinColumn(name = "avatar_id")
+ private Avatar avatar;
+
+ // Указывает на связь между таблицами "один ко многим"
+ @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ @JoinColumn(name = "student_id")
+ private List emails;
+
+ // Указывает на связь между таблицами "многие ко многим"
+ @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
+ // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
+ @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
+ inverseJoinColumns = @JoinColumn(name = "course_id"))
+ private List courses;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
new file mode 100644
index 00000000..868da17b
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
@@ -0,0 +1,86 @@
+package ru.otus.example.ormdemo.repositories;
+
+import jakarta.persistence.EntityGraph;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH;
+
+// @Transactional должна стоять на методе сервиса.
+// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
+// выставить у аннотации параметр readOnly в true.
+// Но это только упражнение и транзакции мы пока не проходили.
+// Поэтому, для упрощения, пока вешаем над классом репозитория
+@Transactional
+@Repository
+public class JpaOtusStudentRepository implements OtusStudentRepository {
+
+ @PersistenceContext
+ private final EntityManager em;
+
+ public JpaOtusStudentRepository(EntityManager em) {
+ this.em = em;
+ }
+
+ @Override
+ public OtusStudent save(OtusStudent student) {
+ if (student.getId() == 0) {
+ em.persist(student);
+ return student;
+ }
+ return em.merge(student);
+ }
+
+ @Override
+ public Optional findById(long id) {
+ return Optional.ofNullable(em.find(OtusStudent.class, id));
+ }
+
+ @Override
+ public List findAll() {
+ EntityGraph> entityGraph = em.getEntityGraph("otus-student-avatars-entity-graph");
+ TypedQuery query = em.createQuery("select s from OtusStudent s", OtusStudent.class);
+ query.setHint(FETCH.getKey(), entityGraph);
+ return query.getResultList();
+ }
+
+ @Override
+ public List findByName(String name) {
+ TypedQuery query = em.createQuery("select s " +
+ "from OtusStudent s " +
+ "where s.name = :name",
+ OtusStudent.class);
+ query.setParameter("name", name);
+ return query.getResultList();
+ }
+
+ // Только для примера, в реальности JPQL лучше использовать только для массовых операций
+ @Override
+ public void updateNameById(long id, String name) {
+ Query query = em.createQuery("update OtusStudent s " +
+ "set s.name = :name " +
+ "where s.id = :id");
+ query.setParameter("name", name);
+ query.setParameter("id", id);
+ query.executeUpdate();
+ }
+
+ // Только для примера, в реальности JPQL лучше использовать только для массовых операций
+ @Override
+ public void deleteById(long id) {
+ Query query = em.createQuery("delete " +
+ "from OtusStudent s " +
+ "where s.id = :id");
+ query.setParameter("id", id);
+ query.executeUpdate();
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
new file mode 100644
index 00000000..44cc6a58
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
@@ -0,0 +1,18 @@
+package ru.otus.example.ormdemo.repositories;
+
+
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface OtusStudentRepository {
+ OtusStudent save(OtusStudent student);
+ Optional findById(long id);
+
+ List findAll();
+ List findByName(String name);
+
+ void updateNameById(long id, String name);
+ void deleteById(long id);
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/resources/application.yml
new file mode 100644
index 00000000..ab48d518
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/resources/application.yml
@@ -0,0 +1,18 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ sql:
+ init:
+ mode: always
+
+
+ jpa:
+ generate-ddl: false
+ hibernate:
+ ddl-auto: none
+
+ show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/resources/schema.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/resources/schema.sql
new file mode 100644
index 00000000..43a684bb
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/main/resources/schema.sql
@@ -0,0 +1,31 @@
+create table avatars(
+ id bigserial,
+ photo_url varchar(8000),
+ primary key (id)
+);
+
+create table courses(
+ id bigserial,
+ name varchar(255),
+ primary key (id)
+);
+
+create table otus_students(
+ id bigserial,
+ name varchar(255),
+ avatar_id bigint references avatars (id),
+ primary key (id)
+);
+
+create table emails(
+ id bigserial,
+ student_id bigint references otus_students(id) on delete cascade,
+ email varchar(255),
+ primary key (id)
+);
+
+create table student_courses(
+ student_id bigint references otus_students(id) on delete cascade,
+ course_id bigint references courses(id),
+ primary key (student_id, course_id)
+);
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
new file mode 100644
index 00000000..c935924c
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
@@ -0,0 +1,58 @@
+package ru.otus.example.ormdemo.repositories;
+
+import lombok.val;
+import org.hibernate.SessionFactory;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+import org.springframework.context.annotation.Import;
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
+@DataJpaTest
+@Import(JpaOtusStudentRepository.class)
+class JpaOtusStudentRepositoryTest {
+
+ private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
+ private static final long FIRST_STUDENT_ID = 1L;
+
+ private static final int EXPECTED_QUERIES_COUNT = 21;
+
+ @Autowired
+ private JpaOtusStudentRepository repositoryJpa;
+
+ @Autowired
+ private TestEntityManager em;
+
+ @DisplayName(" должен загружать информацию о нужном студенте по его id")
+ @Test
+ void shouldFindExpectedStudentById() {
+ val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
+ val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ assertThat(optionalActualStudent).isPresent().get()
+ .usingRecursiveComparison().isEqualTo(expectedStudent);
+ }
+
+ @DisplayName("должен загружать список всех студентов с полной информацией о них")
+ @Test
+ void shouldReturnCorrectStudentsListWithAllInfo() {
+ SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
+ .unwrap(SessionFactory.class);
+ sessionFactory.getStatistics().setStatisticsEnabled(true);
+
+
+ System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
+ val students = repositoryJpa.findAll();
+ assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
+ .allMatch(s -> !s.getName().equals(""))
+ .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
+ .allMatch(s -> s.getAvatar().getPhotoUrl() != null)
+ .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
+ System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
+ assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
+ }
+}
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/resources/application.yml
new file mode 100644
index 00000000..7d216f89
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/resources/application.yml
@@ -0,0 +1,20 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ sql:
+ init:
+ mode: always
+
+
+ jpa:
+ generate-ddl: false
+ #generate-ddl: true
+ hibernate:
+ ddl-auto: none
+ #ddl-auto: create-drop
+
+ #show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/resources/data.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/resources/data.sql
new file mode 100644
index 00000000..a8db6b85
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-04/src/test/resources/data.sql
@@ -0,0 +1,29 @@
+insert into avatars(photo_url)
+values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'),
+ ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10');
+
+insert into courses(name)
+values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'),
+ ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11');
+
+insert into otus_students(name, avatar_id)
+values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5),
+ ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10);
+
+
+insert into emails(email, student_id)
+values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4),
+ ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10);
+
+
+insert into student_courses(student_id, course_id)
+values (1, 1), (1, 2), (1, 3),
+ (2, 2), (2, 4), (2, 5),
+ (3, 3), (3, 6), (3, 7),
+ (4, 4), (4, 8), (4, 9),
+ (5, 5), (5, 10), (5, 1),
+ (6, 6), (6, 2), (6, 3),
+ (7, 7), (7, 4), (7, 5),
+ (8, 8), (8, 6), (8, 7),
+ (9, 9), (9, 8), (9, 10),
+ (10, 10), (10, 1), (10, 2);
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/.gitignore b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/pom.xml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/pom.xml
new file mode 100644
index 00000000..c2445844
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+
+ ru.otus
+ jpql-solution-05
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.1
+
+
+
+
+ 17
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
new file mode 100644
index 00000000..8d4b413c
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
@@ -0,0 +1,13 @@
+package ru.otus.example.ormdemo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class OrmDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(OrmDemoApplication.class, args);
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
new file mode 100644
index 00000000..c382bc41
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "avatars")
+public class Avatar {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "photo_url", nullable = false, unique = true)
+ private String photoUrl;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/Course.java
new file mode 100644
index 00000000..6c9c819a
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/Course.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "courses")
+public class Course {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/EMail.java
new file mode 100644
index 00000000..69a92ff3
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/EMail.java
@@ -0,0 +1,26 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "emails")
+public class EMail {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "email", nullable = false, unique = true)
+ private String email;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
new file mode 100644
index 00000000..357c2224
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
@@ -0,0 +1,58 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.NamedAttributeNode;
+import jakarta.persistence.NamedEntityGraph;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity // Указывает, что данный класс является сущностью
+@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
+// Позволяет указать какие связи родительской сущности загружать в одном с ней запросе
+@NamedEntityGraph(name = "otus-student-avatars-entity-graph",
+ attributeNodes = {@NamedAttributeNode("avatar")})
+public class OtusStudent {
+ @Id // Позволяет указать какое поле является идентификатором
+ @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
+ private long id;
+
+ // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+
+ // Указывает на связь между таблицами "один к одному"
+ @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
+ // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
+ @JoinColumn(name = "avatar_id")
+ private Avatar avatar;
+
+ // Указывает на связь между таблицами "один ко многим"
+ @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ @JoinColumn(name = "student_id")
+ private List emails;
+
+ // Указывает на связь между таблицами "многие ко многим"
+ @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
+ // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
+ @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
+ inverseJoinColumns = @JoinColumn(name = "course_id"))
+ private List courses;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
new file mode 100644
index 00000000..b76135a6
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
@@ -0,0 +1,87 @@
+package ru.otus.example.ormdemo.repositories;
+
+import jakarta.persistence.EntityGraph;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH;
+
+// @Transactional должна стоять на методе сервиса.
+// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
+// выставить у аннотации параметр readOnly в true.
+// Но это только упражнение и транзакции мы пока не проходили.
+// Поэтому, для упрощения, пока вешаем над классом репозитория
+@Transactional
+@Repository
+public class JpaOtusStudentRepository implements OtusStudentRepository {
+
+ @PersistenceContext
+ private final EntityManager em;
+
+ public JpaOtusStudentRepository(EntityManager em) {
+ this.em = em;
+ }
+
+ @Override
+ public OtusStudent save(OtusStudent student) {
+ if (student.getId() == 0) {
+ em.persist(student);
+ return student;
+ }
+ return em.merge(student);
+ }
+
+ @Override
+ public Optional findById(long id) {
+ return Optional.ofNullable(em.find(OtusStudent.class, id));
+ }
+
+ @Override
+ public List findAll() {
+ EntityGraph> entityGraph = em.getEntityGraph("otus-student-avatars-entity-graph");
+ TypedQuery query = em.createQuery("select distinct s from OtusStudent s " +
+ "left join fetch s.emails", OtusStudent.class);
+ query.setHint(FETCH.getKey(), entityGraph);
+ return query.getResultList();
+ }
+
+ @Override
+ public List findByName(String name) {
+ TypedQuery query = em.createQuery("select s " +
+ "from OtusStudent s " +
+ "where s.name = :name",
+ OtusStudent.class);
+ query.setParameter("name", name);
+ return query.getResultList();
+ }
+
+ // Только для примера, в реальности JPQL лучше использовать только для массовых операций
+ @Override
+ public void updateNameById(long id, String name) {
+ Query query = em.createQuery("update OtusStudent s " +
+ "set s.name = :name " +
+ "where s.id = :id");
+ query.setParameter("name", name);
+ query.setParameter("id", id);
+ query.executeUpdate();
+ }
+
+ // Только для примера, в реальности JPQL лучше использовать только для массовых операций
+ @Override
+ public void deleteById(long id) {
+ Query query = em.createQuery("delete " +
+ "from OtusStudent s " +
+ "where s.id = :id");
+ query.setParameter("id", id);
+ query.executeUpdate();
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
new file mode 100644
index 00000000..44cc6a58
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
@@ -0,0 +1,18 @@
+package ru.otus.example.ormdemo.repositories;
+
+
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface OtusStudentRepository {
+ OtusStudent save(OtusStudent student);
+ Optional findById(long id);
+
+ List findAll();
+ List findByName(String name);
+
+ void updateNameById(long id, String name);
+ void deleteById(long id);
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/resources/application.yml
new file mode 100644
index 00000000..8d633961
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/resources/application.yml
@@ -0,0 +1,15 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ initialization-mode: always
+
+ jpa:
+ generate-ddl: false
+ hibernate:
+ ddl-auto: none
+
+ show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/resources/schema.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/resources/schema.sql
new file mode 100644
index 00000000..43a684bb
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/main/resources/schema.sql
@@ -0,0 +1,31 @@
+create table avatars(
+ id bigserial,
+ photo_url varchar(8000),
+ primary key (id)
+);
+
+create table courses(
+ id bigserial,
+ name varchar(255),
+ primary key (id)
+);
+
+create table otus_students(
+ id bigserial,
+ name varchar(255),
+ avatar_id bigint references avatars (id),
+ primary key (id)
+);
+
+create table emails(
+ id bigserial,
+ student_id bigint references otus_students(id) on delete cascade,
+ email varchar(255),
+ primary key (id)
+);
+
+create table student_courses(
+ student_id bigint references otus_students(id) on delete cascade,
+ course_id bigint references courses(id),
+ primary key (student_id, course_id)
+);
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
new file mode 100644
index 00000000..557be9da
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
@@ -0,0 +1,58 @@
+package ru.otus.example.ormdemo.repositories;
+
+import lombok.val;
+import org.hibernate.SessionFactory;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+import org.springframework.context.annotation.Import;
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
+@DataJpaTest
+@Import(JpaOtusStudentRepository.class)
+class JpaOtusStudentRepositoryTest {
+
+ private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
+ private static final long FIRST_STUDENT_ID = 1L;
+
+ private static final int EXPECTED_QUERIES_COUNT = 11;
+
+ @Autowired
+ private JpaOtusStudentRepository repositoryJpa;
+
+ @Autowired
+ private TestEntityManager em;
+
+ @DisplayName(" должен загружать информацию о нужном студенте по его id")
+ @Test
+ void shouldFindExpectedStudentById() {
+ val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
+ val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ assertThat(optionalActualStudent).isPresent().get()
+ .usingRecursiveComparison().isEqualTo(expectedStudent);
+ }
+
+ @DisplayName("должен загружать список всех студентов с полной информацией о них")
+ @Test
+ void shouldReturnCorrectStudentsListWithAllInfo() {
+ SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
+ .unwrap(SessionFactory.class);
+ sessionFactory.getStatistics().setStatisticsEnabled(true);
+
+
+ System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
+ val students = repositoryJpa.findAll();
+ assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
+ .allMatch(s -> !s.getName().equals(""))
+ .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
+ .allMatch(s -> s.getAvatar().getPhotoUrl() != null)
+ .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
+ System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
+ assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
+ }
+}
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/resources/application.yml
new file mode 100644
index 00000000..d2b811a4
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/resources/application.yml
@@ -0,0 +1,17 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ initialization-mode: always
+
+ jpa:
+ generate-ddl: false
+ #generate-ddl: true
+ hibernate:
+ ddl-auto: none
+ #ddl-auto: create-drop
+
+ #show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/resources/data.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/resources/data.sql
new file mode 100644
index 00000000..a8db6b85
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-05/src/test/resources/data.sql
@@ -0,0 +1,29 @@
+insert into avatars(photo_url)
+values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'),
+ ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10');
+
+insert into courses(name)
+values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'),
+ ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11');
+
+insert into otus_students(name, avatar_id)
+values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5),
+ ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10);
+
+
+insert into emails(email, student_id)
+values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4),
+ ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10);
+
+
+insert into student_courses(student_id, course_id)
+values (1, 1), (1, 2), (1, 3),
+ (2, 2), (2, 4), (2, 5),
+ (3, 3), (3, 6), (3, 7),
+ (4, 4), (4, 8), (4, 9),
+ (5, 5), (5, 10), (5, 1),
+ (6, 6), (6, 2), (6, 3),
+ (7, 7), (7, 4), (7, 5),
+ (8, 8), (8, 6), (8, 7),
+ (9, 9), (9, 8), (9, 10),
+ (10, 10), (10, 1), (10, 2);
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/.gitignore b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/pom.xml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/pom.xml
new file mode 100644
index 00000000..44822b54
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+
+ ru.otus
+ jpql-solution-06
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.1
+
+
+
+
+ 17
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
new file mode 100644
index 00000000..8d4b413c
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
@@ -0,0 +1,13 @@
+package ru.otus.example.ormdemo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class OrmDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(OrmDemoApplication.class, args);
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
new file mode 100644
index 00000000..c382bc41
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "avatars")
+public class Avatar {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "photo_url", nullable = false, unique = true)
+ private String photoUrl;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/Course.java
new file mode 100644
index 00000000..6c9c819a
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/Course.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "courses")
+public class Course {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/EMail.java
new file mode 100644
index 00000000..69a92ff3
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/EMail.java
@@ -0,0 +1,26 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "emails")
+public class EMail {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "email", nullable = false, unique = true)
+ private String email;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
new file mode 100644
index 00000000..c28f2655
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
@@ -0,0 +1,62 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.NamedAttributeNode;
+import jakarta.persistence.NamedEntityGraph;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.annotations.Fetch;
+import org.hibernate.annotations.FetchMode;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity // Указывает, что данный класс является сущностью
+@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
+// Позволяет указать какие связи родительской сущности загружать в одном с ней запросе
+@NamedEntityGraph(name = "otus-student-avatars-entity-graph",
+ attributeNodes = {@NamedAttributeNode("avatar")})
+public class OtusStudent {
+ @Id // Позволяет указать какое поле является идентификатором
+ @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
+ private long id;
+
+ // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+
+ // Указывает на связь между таблицами "один к одному"
+ @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
+ // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
+ @JoinColumn(name = "avatar_id")
+ private Avatar avatar;
+
+ // Указывает на связь между таблицами "один ко многим"
+ @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ @JoinColumn(name = "student_id")
+ private List emails;
+
+ // Все данные талицы будут загружены в память отдельным запросом и соединены с родительской сущностью
+ @Fetch(FetchMode.SUBSELECT)
+ // Указывает на связь между таблицами "многие ко многим"
+ @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
+ // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
+ @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
+ inverseJoinColumns = @JoinColumn(name = "course_id"))
+ private List courses;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
new file mode 100644
index 00000000..b76135a6
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
@@ -0,0 +1,87 @@
+package ru.otus.example.ormdemo.repositories;
+
+import jakarta.persistence.EntityGraph;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH;
+
+// @Transactional должна стоять на методе сервиса.
+// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
+// выставить у аннотации параметр readOnly в true.
+// Но это только упражнение и транзакции мы пока не проходили.
+// Поэтому, для упрощения, пока вешаем над классом репозитория
+@Transactional
+@Repository
+public class JpaOtusStudentRepository implements OtusStudentRepository {
+
+ @PersistenceContext
+ private final EntityManager em;
+
+ public JpaOtusStudentRepository(EntityManager em) {
+ this.em = em;
+ }
+
+ @Override
+ public OtusStudent save(OtusStudent student) {
+ if (student.getId() == 0) {
+ em.persist(student);
+ return student;
+ }
+ return em.merge(student);
+ }
+
+ @Override
+ public Optional findById(long id) {
+ return Optional.ofNullable(em.find(OtusStudent.class, id));
+ }
+
+ @Override
+ public List findAll() {
+ EntityGraph> entityGraph = em.getEntityGraph("otus-student-avatars-entity-graph");
+ TypedQuery query = em.createQuery("select distinct s from OtusStudent s " +
+ "left join fetch s.emails", OtusStudent.class);
+ query.setHint(FETCH.getKey(), entityGraph);
+ return query.getResultList();
+ }
+
+ @Override
+ public List findByName(String name) {
+ TypedQuery query = em.createQuery("select s " +
+ "from OtusStudent s " +
+ "where s.name = :name",
+ OtusStudent.class);
+ query.setParameter("name", name);
+ return query.getResultList();
+ }
+
+ // Только для примера, в реальности JPQL лучше использовать только для массовых операций
+ @Override
+ public void updateNameById(long id, String name) {
+ Query query = em.createQuery("update OtusStudent s " +
+ "set s.name = :name " +
+ "where s.id = :id");
+ query.setParameter("name", name);
+ query.setParameter("id", id);
+ query.executeUpdate();
+ }
+
+ // Только для примера, в реальности JPQL лучше использовать только для массовых операций
+ @Override
+ public void deleteById(long id) {
+ Query query = em.createQuery("delete " +
+ "from OtusStudent s " +
+ "where s.id = :id");
+ query.setParameter("id", id);
+ query.executeUpdate();
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
new file mode 100644
index 00000000..44cc6a58
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
@@ -0,0 +1,18 @@
+package ru.otus.example.ormdemo.repositories;
+
+
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface OtusStudentRepository {
+ OtusStudent save(OtusStudent student);
+ Optional findById(long id);
+
+ List findAll();
+ List findByName(String name);
+
+ void updateNameById(long id, String name);
+ void deleteById(long id);
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/resources/application.yml
new file mode 100644
index 00000000..ab48d518
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/resources/application.yml
@@ -0,0 +1,18 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ sql:
+ init:
+ mode: always
+
+
+ jpa:
+ generate-ddl: false
+ hibernate:
+ ddl-auto: none
+
+ show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/resources/schema.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/resources/schema.sql
new file mode 100644
index 00000000..43a684bb
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/main/resources/schema.sql
@@ -0,0 +1,31 @@
+create table avatars(
+ id bigserial,
+ photo_url varchar(8000),
+ primary key (id)
+);
+
+create table courses(
+ id bigserial,
+ name varchar(255),
+ primary key (id)
+);
+
+create table otus_students(
+ id bigserial,
+ name varchar(255),
+ avatar_id bigint references avatars (id),
+ primary key (id)
+);
+
+create table emails(
+ id bigserial,
+ student_id bigint references otus_students(id) on delete cascade,
+ email varchar(255),
+ primary key (id)
+);
+
+create table student_courses(
+ student_id bigint references otus_students(id) on delete cascade,
+ course_id bigint references courses(id),
+ primary key (student_id, course_id)
+);
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
new file mode 100644
index 00000000..84ddb33b
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
@@ -0,0 +1,58 @@
+package ru.otus.example.ormdemo.repositories;
+
+import lombok.val;
+import org.hibernate.SessionFactory;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+import org.springframework.context.annotation.Import;
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
+@DataJpaTest
+@Import(JpaOtusStudentRepository.class)
+class JpaOtusStudentRepositoryTest {
+
+ private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
+ private static final long FIRST_STUDENT_ID = 1L;
+
+ private static final int EXPECTED_QUERIES_COUNT = 2;
+
+ @Autowired
+ private JpaOtusStudentRepository repositoryJpa;
+
+ @Autowired
+ private TestEntityManager em;
+
+ @DisplayName(" должен загружать информацию о нужном студенте по его id")
+ @Test
+ void shouldFindExpectedStudentById() {
+ val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
+ val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ assertThat(optionalActualStudent).isPresent().get()
+ .usingRecursiveComparison().isEqualTo(expectedStudent);
+ }
+
+ @DisplayName("должен загружать список всех студентов с полной информацией о них")
+ @Test
+ void shouldReturnCorrectStudentsListWithAllInfo() {
+ SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
+ .unwrap(SessionFactory.class);
+ sessionFactory.getStatistics().setStatisticsEnabled(true);
+
+
+ System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
+ val students = repositoryJpa.findAll();
+ assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
+ .allMatch(s -> !s.getName().equals(""))
+ .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
+ .allMatch(s -> s.getAvatar().getPhotoUrl() != null)
+ .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
+ System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
+ assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
+ }
+}
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/resources/application.yml
new file mode 100644
index 00000000..02f7edee
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/resources/application.yml
@@ -0,0 +1,24 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ sql:
+ init:
+ mode: always
+
+
+ jpa:
+ generate-ddl: false
+ #generate-ddl: true
+ hibernate:
+ ddl-auto: none
+ #ddl-auto: create-drop
+
+ #show-sql: true
+
+ properties:
+ hibernate:
+ format_sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/resources/data.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/resources/data.sql
new file mode 100644
index 00000000..a8db6b85
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-06/src/test/resources/data.sql
@@ -0,0 +1,29 @@
+insert into avatars(photo_url)
+values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'),
+ ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10');
+
+insert into courses(name)
+values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'),
+ ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11');
+
+insert into otus_students(name, avatar_id)
+values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5),
+ ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10);
+
+
+insert into emails(email, student_id)
+values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4),
+ ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10);
+
+
+insert into student_courses(student_id, course_id)
+values (1, 1), (1, 2), (1, 3),
+ (2, 2), (2, 4), (2, 5),
+ (3, 3), (3, 6), (3, 7),
+ (4, 4), (4, 8), (4, 9),
+ (5, 5), (5, 10), (5, 1),
+ (6, 6), (6, 2), (6, 3),
+ (7, 7), (7, 4), (7, 5),
+ (8, 8), (8, 6), (8, 7),
+ (9, 9), (9, 8), (9, 10),
+ (10, 10), (10, 1), (10, 2);
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/.gitignore b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/pom.xml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/pom.xml
new file mode 100644
index 00000000..ecfb870d
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+
+ ru.otus
+ jpql-solution-final
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.1
+
+
+
+
+ 17
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
new file mode 100644
index 00000000..8d4b413c
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/OrmDemoApplication.java
@@ -0,0 +1,13 @@
+package ru.otus.example.ormdemo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class OrmDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(OrmDemoApplication.class, args);
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/Avatar.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
new file mode 100644
index 00000000..c382bc41
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/Avatar.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "avatars")
+public class Avatar {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "photo_url", nullable = false, unique = true)
+ private String photoUrl;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/Course.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/Course.java
new file mode 100644
index 00000000..6c9c819a
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/Course.java
@@ -0,0 +1,25 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "courses")
+public class Course {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/EMail.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/EMail.java
new file mode 100644
index 00000000..69a92ff3
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/EMail.java
@@ -0,0 +1,26 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "emails")
+public class EMail {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "email", nullable = false, unique = true)
+ private String email;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
new file mode 100644
index 00000000..2d74413e
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/models/OtusStudent.java
@@ -0,0 +1,62 @@
+package ru.otus.example.ormdemo.models;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.NamedAttributeNode;
+import jakarta.persistence.NamedEntityGraph;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.annotations.BatchSize;
+import org.hibernate.annotations.Fetch;
+import org.hibernate.annotations.FetchMode;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity // Указывает, что данный класс является сущностью
+@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
+@NamedEntityGraph(name = "otus-student-avatars-entity-graph",
+ attributeNodes = {@NamedAttributeNode("avatar")})
+public class OtusStudent {
+ @Id // Позволяет указать какое поле является идентификатором
+ @GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
+ private long id;
+
+ // Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
+ @Column(name = "name", nullable = false, unique = true)
+ private String name;
+
+ // Указывает на связь между таблицами "один к одному"
+ @OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
+ // Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
+ @JoinColumn(name = "avatar_id")
+ private Avatar avatar;
+
+ // Указывает на связь между таблицами "один ко многим"
+ @OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ @JoinColumn(name = "student_id")
+ private List emails;
+
+ @Fetch(FetchMode.SELECT)
+ @BatchSize(size = 5)
+ // Указывает на связь между таблицами "многие ко многим"
+ @ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
+ // Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
+ @JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
+ inverseJoinColumns = @JoinColumn(name = "course_id"))
+ private List courses;
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
new file mode 100644
index 00000000..d2ab60e1
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepository.java
@@ -0,0 +1,86 @@
+package ru.otus.example.ormdemo.repositories;
+
+import jakarta.persistence.EntityGraph;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH;
+
+// @Transactional должна стоять на методе сервиса.
+// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
+// выставить у аннотации параметр readOnly в true.
+// Но это только упражнение и транзакции мы пока не проходили.
+// Поэтому, для упрощения, пока вешаем над классом репозитория
+@Transactional
+@Repository
+public class JpaOtusStudentRepository implements OtusStudentRepository {
+
+ @PersistenceContext
+ private final EntityManager em;
+
+ public JpaOtusStudentRepository(EntityManager em) {
+ this.em = em;
+ }
+
+ @Override
+ public OtusStudent save(OtusStudent student) {
+ if (student.getId() == 0) {
+ em.persist(student);
+ return student;
+ }
+ return em.merge(student);
+ }
+
+ @Override
+ public Optional findById(long id) {
+ return Optional.ofNullable(em.find(OtusStudent.class, id));
+ }
+
+ @Override
+ public List findAll() {
+ EntityGraph> entityGraph = em.getEntityGraph("otus-student-avatars-entity-graph");
+ TypedQuery query = em.createQuery("select distinct s from OtusStudent s left join fetch s.emails", OtusStudent.class);
+ query.setHint(FETCH.getKey(), entityGraph);
+ return query.getResultList();
+ }
+
+ @Override
+ public List findByName(String name) {
+ TypedQuery query = em.createQuery("select s " +
+ "from OtusStudent s " +
+ "where s.name = :name",
+ OtusStudent.class);
+ query.setParameter("name", name);
+ return query.getResultList();
+ }
+
+ // Только для примера, в реальности JPQL лучше использовать только для массовых операций
+ @Override
+ public void updateNameById(long id, String name) {
+ Query query = em.createQuery("update OtusStudent s " +
+ "set s.name = :name " +
+ "where s.id = :id");
+ query.setParameter("name", name);
+ query.setParameter("id", id);
+ query.executeUpdate();
+ }
+
+ // Только для примера, в реальности JPQL лучше использовать только для массовых операций
+ @Override
+ public void deleteById(long id) {
+ Query query = em.createQuery("delete " +
+ "from OtusStudent s " +
+ "where s.id = :id");
+ query.setParameter("id", id);
+ query.executeUpdate();
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
new file mode 100644
index 00000000..44cc6a58
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/java/ru/otus/example/ormdemo/repositories/OtusStudentRepository.java
@@ -0,0 +1,18 @@
+package ru.otus.example.ormdemo.repositories;
+
+
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface OtusStudentRepository {
+ OtusStudent save(OtusStudent student);
+ Optional findById(long id);
+
+ List findAll();
+ List findByName(String name);
+
+ void updateNameById(long id, String name);
+ void deleteById(long id);
+}
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/resources/application.yml
new file mode 100644
index 00000000..ab48d518
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/resources/application.yml
@@ -0,0 +1,18 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ sql:
+ init:
+ mode: always
+
+
+ jpa:
+ generate-ddl: false
+ hibernate:
+ ddl-auto: none
+
+ show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/resources/schema.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/resources/schema.sql
new file mode 100644
index 00000000..43a684bb
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/main/resources/schema.sql
@@ -0,0 +1,31 @@
+create table avatars(
+ id bigserial,
+ photo_url varchar(8000),
+ primary key (id)
+);
+
+create table courses(
+ id bigserial,
+ name varchar(255),
+ primary key (id)
+);
+
+create table otus_students(
+ id bigserial,
+ name varchar(255),
+ avatar_id bigint references avatars (id),
+ primary key (id)
+);
+
+create table emails(
+ id bigserial,
+ student_id bigint references otus_students(id) on delete cascade,
+ email varchar(255),
+ primary key (id)
+);
+
+create table student_courses(
+ student_id bigint references otus_students(id) on delete cascade,
+ course_id bigint references courses(id),
+ primary key (student_id, course_id)
+);
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
new file mode 100644
index 00000000..ea8b4d65
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/java/ru/otus/example/ormdemo/repositories/JpaOtusStudentRepositoryTest.java
@@ -0,0 +1,113 @@
+package ru.otus.example.ormdemo.repositories;
+
+import lombok.val;
+import org.hibernate.SessionFactory;
+import org.hibernate.boot.registry.StandardServiceRegistry;
+import org.hibernate.engine.jdbc.spi.JdbcServices;
+import org.hibernate.engine.jdbc.spi.SqlStatementLogger;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.annotation.DirtiesContext;
+import ru.otus.example.ormdemo.models.OtusStudent;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
+@DataJpaTest
+@Import(JpaOtusStudentRepository.class)
+class JpaOtusStudentRepositoryTest {
+
+ private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
+ private static final long FIRST_STUDENT_ID = 1L;
+
+ private static final int EXPECTED_QUERIES_COUNT = 3;
+
+ @Autowired
+ private JpaOtusStudentRepository repositoryJpa;
+
+ @Autowired
+ private TestEntityManager em;
+
+ @DisplayName(" должен загружать информацию о нужном студенте по его id")
+ @Test
+ void shouldFindExpectedStudentById() {
+ val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
+ val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
+ assertThat(optionalActualStudent).isPresent().get()
+ .usingRecursiveComparison().isEqualTo(expectedStudent);
+ }
+
+
+ @DisplayName("должен загружать список всех студентов с полной информацией о них")
+ @Test
+ void shouldReturnCorrectStudentsListWithAllInfo() {
+ SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
+ .unwrap(SessionFactory.class);
+ sessionFactory.getStatistics().setStatisticsEnabled(true);
+
+
+ System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
+ val students = repositoryJpa.findAll();
+ assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
+ .allMatch(s -> !s.getName().equals(""))
+ .allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
+ .allMatch(s -> s.getAvatar().getPhotoUrl() != null)
+ .allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
+ System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
+ assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
+ }
+
+ @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
+ @DisplayName("должен загружать ожидаемый список студентов по номеру страницы")
+ @Test
+ void shouldReturnCorrectStudentsListByPage() {
+ AtomicInteger studentsSelectionsCount = new AtomicInteger(0);
+ applyCustomSqlStatementLogger(new SqlStatementLogger(true, false, false, 0) {
+ @Override
+ public void logStatement(String statement) {
+ super.logStatement(statement);
+ if (!statement.contains("count") && statement.contains("from otus_students")) {
+ studentsSelectionsCount.incrementAndGet();
+ assertThat(statement).contains("offset").contains("rows only");
+ }
+ }
+ });
+
+
+ var studentsCount = em.getEntityManager()
+ .createQuery("select count(s) from OtusStudent s", Long.class).getSingleResult();
+ var pageNum = 2;
+ var pageSize = 3;
+ var pagesCount = (long) Math.ceil(studentsCount * 1d / pageSize);
+
+ var query = em.getEntityManager().createQuery("select s from OtusStudent s ", OtusStudent.class);
+ //var query = em.getEntityManager().createQuery("select distinct s from OtusStudent s " +
+ // "left join fetch s.courses c", OtusStudent.class);
+ var students = query.setFirstResult(pageNum * pageSize).setMaxResults(pageSize).getResultList();
+
+ assertThat(pagesCount).isEqualTo(4);
+ assertThat(students).isNotNull().hasSize(pageSize);
+ assertThat(studentsSelectionsCount.get()).isEqualTo(1);
+ }
+
+ private void applyCustomSqlStatementLogger(SqlStatementLogger customSqlStatementLogger) {
+ StandardServiceRegistry serviceRegistry = em.getEntityManager().getEntityManagerFactory()
+ .unwrap(SessionFactory.class).getSessionFactoryOptions().getServiceRegistry();
+ var jdbcServices = serviceRegistry.getService(JdbcServices.class);
+ try {
+ Field field = jdbcServices.getClass().getDeclaredField("sqlStatementLogger");
+ field.setAccessible(true);
+ field.set(jdbcServices, customSqlStatementLogger);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/resources/application.yml b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/resources/application.yml
new file mode 100644
index 00000000..9ba1bec9
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/resources/application.yml
@@ -0,0 +1,19 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ sql:
+ init:
+ mode: always
+
+ jpa:
+ generate-ddl: false
+ #generate-ddl: true
+ hibernate:
+ ddl-auto: none
+ #ddl-auto: create-drop
+
+ #show-sql: true
+
+logging:
+ level:
+ ROOT: ERROR
\ No newline at end of file
diff --git a/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/resources/data.sql b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/resources/data.sql
new file mode 100644
index 00000000..a8db6b85
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/jpql-solution-final/src/test/resources/data.sql
@@ -0,0 +1,29 @@
+insert into avatars(photo_url)
+values ('photoUrl_01'), ('photoUrl_02'), ('photoUrl_03'), ('photoUrl_04'), ('photoUrl_05'),
+ ('photoUrl_06'), ('photoUrl_07'), ('photoUrl_08'), ('photoUrl_09'), ('photoUrl_10');
+
+insert into courses(name)
+values ('course_name_01'), ('course_name_02'), ('course_name_03'), ('course_name_04'), ('course_name_05'),
+ ('course_name_06'), ('course_name_07'), ('course_name_08'), ('course_name_09'), ('course_name_10'), ('not_used_11');
+
+insert into otus_students(name, avatar_id)
+values ('student_01', 1), ('student_02', 2), ('student_03', 3), ('student_04', 4), ('student_05', 5),
+ ('student_06', 6), ('student_07', 7), ('student_08', 8), ('student_09', 9), ('student_10', 10);
+
+
+insert into emails(email, student_id)
+values ('email_01', 1), ('email_02', 1), ('email_03', 2), ('email_04', 2), ('email_05', 3), ('email_06', 4),
+ ('email_07', 5), ('email_08', 6), ('email_09', 7), ('email_10', 8), ('email_11', 9), ('email_12', 10);
+
+
+insert into student_courses(student_id, course_id)
+values (1, 1), (1, 2), (1, 3),
+ (2, 2), (2, 4), (2, 5),
+ (3, 3), (3, 6), (3, 7),
+ (4, 4), (4, 8), (4, 9),
+ (5, 5), (5, 10), (5, 1),
+ (6, 6), (6, 2), (6, 3),
+ (7, 7), (7, 4), (7, 5),
+ (8, 8), (8, 6), (8, 7),
+ (9, 9), (9, 8), (9, 10),
+ (10, 10), (10, 1), (10, 2);
diff --git a/2024-11/spring-11-jpql/jpql-class-work/pom.xml b/2024-11/spring-11-jpql/jpql-class-work/pom.xml
new file mode 100644
index 00000000..6f781fad
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-class-work/pom.xml
@@ -0,0 +1,23 @@
+
+
+ 4.0.0
+
+ ru.otus
+ jpql-class-work
+ 1.0
+
+ pom
+
+
+ jpql-exercise
+ jpql-solution-01
+ jpql-solution-02
+ jpql-solution-03
+ jpql-solution-04
+ jpql-solution-05
+ jpql-solution-06
+ jpql-solution-final
+
+
diff --git a/2024-11/spring-11-jpql/jpql-demo/.gitignore b/2024-11/spring-11-jpql/jpql-demo/.gitignore
new file mode 100644
index 00000000..153c9335
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-demo/.gitignore
@@ -0,0 +1,29 @@
+HELP.md
+/target/
+!.mvn/wrapper/maven-wrapper.jar
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+/build/
+
+### VS Code ###
+.vscode/
diff --git a/2024-11/spring-11-jpql/jpql-demo/README.md b/2024-11/spring-11-jpql/jpql-demo/README.md
new file mode 100644
index 00000000..67632929
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-demo/README.md
@@ -0,0 +1,6 @@
+## Пример работы с JPQL
+
+В примере демонстрируется:
+* *репозитории на Spring ORM с использованием JPA и JPQL*
+* *использование JPQL для написания разного рода запросов (в т.ч. для выборки, агрегации, изменения и удаления данных)*
+* *тестирование репозиториев на Spring ORM с использованием @DataJpaTest*
diff --git a/2024-11/spring-11-jpql/jpql-demo/pom.xml b/2024-11/spring-11-jpql/jpql-demo/pom.xml
new file mode 100644
index 00000000..865803c6
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-demo/pom.xml
@@ -0,0 +1,58 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.1
+
+
+
+ ru.otus.example
+ jpql-demo
+ 0.0.1-SNAPSHOT
+ jpql-demo
+ Demo project for Spring Boot
+
+
+ 17
+ 17
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/JpqlDemoApplication.java b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/JpqlDemoApplication.java
new file mode 100644
index 00000000..5588b288
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/JpqlDemoApplication.java
@@ -0,0 +1,13 @@
+package ru.otus.example.jpql_demo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class JpqlDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(JpqlDemoApplication.class, args);
+ }
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/dto/CitySalary.java b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/dto/CitySalary.java
new file mode 100644
index 00000000..29078c2f
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/dto/CitySalary.java
@@ -0,0 +1,15 @@
+package ru.otus.example.jpql_demo.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class CitySalary {
+ private String city;
+ private Double salary;
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/dto/EmployeeProjects.java b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/dto/EmployeeProjects.java
new file mode 100644
index 00000000..7f73ba5c
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/dto/EmployeeProjects.java
@@ -0,0 +1,16 @@
+package ru.otus.example.jpql_demo.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import ru.otus.example.jpql_demo.models.Employee;
+
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class EmployeeProjects {
+ private Employee employee;
+ private long projectsCount;
+
+}
diff --git a/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Address.java b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Address.java
new file mode 100644
index 00000000..438f8efe
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Address.java
@@ -0,0 +1,25 @@
+package ru.otus.example.jpql_demo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Table(name = "addresses")
+public class Address {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "city")
+ private String city;
+}
diff --git a/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Category.java b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Category.java
new file mode 100644
index 00000000..4b342522
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Category.java
@@ -0,0 +1,29 @@
+package ru.otus.example.jpql_demo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Table(name = "categories")
+@Entity
+public class Category {
+ @Id
+ private long id;
+
+ @Column(name = "name")
+ private String name;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "parent_category_id")
+ private Category parent;
+}
diff --git a/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Department.java b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Department.java
new file mode 100644
index 00000000..27d90c76
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Department.java
@@ -0,0 +1,25 @@
+package ru.otus.example.jpql_demo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Table(name = "departments")
+public class Department {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column
+ private String name;
+}
diff --git a/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Employee.java b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Employee.java
new file mode 100644
index 00000000..dd62e1f1
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Employee.java
@@ -0,0 +1,60 @@
+package ru.otus.example.jpql_demo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.annotations.BatchSize;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Table(name = "employees")
+public class Employee {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "first_name")
+ private String firstName;
+
+ @Column(name = "last_name")
+ private String lastName;
+
+ @Column(name = "salary")
+ private BigDecimal salary;
+
+ @ManyToOne
+ @JoinColumn(name = "department_id", referencedColumnName = "id")
+ private Department department;
+
+ @ManyToOne
+ @JoinColumn(name = "address_id", referencedColumnName = "id")
+ private Address address;
+
+ @BatchSize(size = 100)
+ @ManyToMany
+ @JoinTable(name = "employees_projects",
+ joinColumns = @JoinColumn(name = "employee_id", referencedColumnName = "id"),
+ inverseJoinColumns = @JoinColumn(name = "project_id", referencedColumnName = "id"))
+ private List projects;
+
+
+ public Employee(String firstName, String lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+}
diff --git a/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Project.java b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Project.java
new file mode 100644
index 00000000..474ac321
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/models/Project.java
@@ -0,0 +1,25 @@
+package ru.otus.example.jpql_demo.models;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Table(name = "projects")
+public class Project {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "name")
+ private String name;
+}
diff --git a/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/CategoryRepository.java b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/CategoryRepository.java
new file mode 100644
index 00000000..385cc6a4
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/CategoryRepository.java
@@ -0,0 +1,9 @@
+package ru.otus.example.jpql_demo.repositories;
+
+import ru.otus.example.jpql_demo.models.Category;
+
+import java.util.List;
+
+public interface CategoryRepository {
+ List findAll();
+}
diff --git a/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/EmployeeRepository.java b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/EmployeeRepository.java
new file mode 100644
index 00000000..3c00881c
--- /dev/null
+++ b/2024-11/spring-11-jpql/jpql-demo/src/main/java/ru/otus/example/jpql_demo/repositories/EmployeeRepository.java
@@ -0,0 +1,49 @@
+package ru.otus.example.jpql_demo.repositories;
+
+import ru.otus.example.jpql_demo.dto.CitySalary;
+import ru.otus.example.jpql_demo.dto.EmployeeProjects;
+import ru.otus.example.jpql_demo.models.Employee;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Optional;
+
+public interface EmployeeRepository {
+
+ List findAll();
+ Optional findEmployeeById(long id);
+ List findAllEmployeesWithSalaryOver100000();
+ List findEmployeesFirstNames();
+ List