2021-08 spring-09-jpql added

This commit is contained in:
stvort
2021-09-25 00:57:23 +04:00
parent 9f06ef6241
commit a02d466496
131 changed files with 4657 additions and 0 deletions
@@ -0,0 +1,4 @@
.idea/
*.iml
target/
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>jpql-exercise</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -0,0 +1,21 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,21 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,22 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,39 @@
package ru.otus.example.ormdemo.models;
import lombok.*;
import javax.persistence.*;
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.EAGER)
@JoinColumn(name = "student_id")
private List<EMail> emails;
// Указывает на связь между таблицами "многие ко многим"
@ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
// Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
@JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@@ -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<OtusStudent> findById(long id);
List<OtusStudent> findAll();
List<OtusStudent> findByName(String name);
void updateNameById(long id, String name);
void deleteById(long id);
}
@@ -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 javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
// @Transactional должна стоять на методе сервиса.
// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
// выставить у аннотации параметр readOnly в true.
// Но это только упражнение и транзакции мы пока не проходили.
// Поэтому, для упрощения, пока вешаем над классом репозитория
@Transactional
@Repository
public class OtusStudentRepositoryJpa implements OtusStudentRepository {
@PersistenceContext
private final EntityManager em;
public OtusStudentRepositoryJpa(EntityManager em) {
this.em = em;
}
@Override
public OtusStudent save(OtusStudent student) {
return null;
}
@Override
public Optional<OtusStudent> findById(long id) {
return Optional.empty();
}
@Override
public List<OtusStudent> findAll() {
return Collections.emptyList();
}
@Override
public List<OtusStudent> findByName(String name) {
return Collections.emptyList();
}
@Override
public void updateNameById(long id, String name) {
}
@Override
public void deleteById(long id) {
}
}
@@ -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
@@ -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)
);
@@ -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(OtusStudentRepositoryJpa.class)
class OtusStudentRepositoryJpaTest {
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 OtusStudentRepositoryJpa 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() != 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<OtusStudent> 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();
}
}
@@ -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
@@ -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);
@@ -0,0 +1,4 @@
.idea/
*.iml
target/
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>jpql-solution-01</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -0,0 +1,21 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,21 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,22 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,39 @@
package ru.otus.example.ormdemo.models;
import lombok.*;
import javax.persistence.*;
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.EAGER)
@JoinColumn(name = "student_id")
private List<EMail> emails;
// Указывает на связь между таблицами "многие ко многим"
@ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
// Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
@JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@@ -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<OtusStudent> findById(long id);
List<OtusStudent> findAll();
List<OtusStudent> findByName(String name);
void updateNameById(long id, String name);
void deleteById(long id);
}
@@ -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 javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
// @Transactional должна стоять на методе сервиса.
// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
// выставить у аннотации параметр readOnly в true.
// Но это только упражнение и транзакции мы пока не проходили.
// Поэтому, для упрощения, пока вешаем над классом репозитория
@Transactional
@Repository
public class OtusStudentRepositoryJpa implements OtusStudentRepository {
@PersistenceContext
private final EntityManager em;
public OtusStudentRepositoryJpa(EntityManager em) {
this.em = em;
}
@Override
public OtusStudent save(OtusStudent student) {
if (student.getId() <= 0) {
em.persist(student);
return student;
} else {
return em.merge(student);
}
}
@Override
public Optional<OtusStudent> findById(long id) {
return Optional.ofNullable(em.find(OtusStudent.class, id));
}
@Override
public List<OtusStudent> findAll() {
return Collections.emptyList();
}
@Override
public List<OtusStudent> findByName(String name) {
return Collections.emptyList();
}
@Override
public void updateNameById(long id, String name) {
}
@Override
public void deleteById(long id) {
}
}
@@ -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
@@ -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)
);
@@ -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(OtusStudentRepositoryJpa.class)
class OtusStudentRepositoryJpaTest {
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 OtusStudentRepositoryJpa 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() != 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<OtusStudent> 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();
}
}
@@ -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
@@ -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);
@@ -0,0 +1,4 @@
.idea/
*.iml
target/
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>jpql-solution-02</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -0,0 +1,21 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,21 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,22 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,39 @@
package ru.otus.example.ormdemo.models;
import lombok.*;
import javax.persistence.*;
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.EAGER)
@JoinColumn(name = "student_id")
private List<EMail> emails;
// Указывает на связь между таблицами "многие ко многим"
@ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
// Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
@JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@@ -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<OtusStudent> findById(long id);
List<OtusStudent> findAll();
List<OtusStudent> findByName(String name);
void updateNameById(long id, String name);
void deleteById(long id);
}
@@ -0,0 +1,70 @@
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 javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import java.util.List;
import java.util.Optional;
// @Transactional должна стоять на методе сервиса.
// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
// выставить у аннотации параметр readOnly в true.
// Но это только упражнение и транзакции мы пока не проходили.
// Поэтому, для упрощения, пока вешаем над классом репозитория
@Transactional
@Repository
public class OtusStudentRepositoryJpa implements OtusStudentRepository {
@PersistenceContext
private final EntityManager em;
public OtusStudentRepositoryJpa(EntityManager em) {
this.em = em;
}
@Override
public OtusStudent save(OtusStudent student) {
if (student.getId() <= 0) {
em.persist(student);
return student;
} else {
return em.merge(student);
}
}
@Override
public Optional<OtusStudent> findById(long id) {
return Optional.ofNullable(em.find(OtusStudent.class, id));
}
@Override
public List<OtusStudent> findAll() {
return em.createQuery("select s from OtusStudent s", OtusStudent.class)
.getResultList();
}
@Override
public List<OtusStudent> findByName(String name) {
TypedQuery<OtusStudent> 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) {
}
}
@@ -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
@@ -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)
);
@@ -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(OtusStudentRepositoryJpa.class)
class OtusStudentRepositoryJpaTest {
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 OtusStudentRepositoryJpa 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() != 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<OtusStudent> 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();
}
}
@@ -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
@@ -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);
@@ -0,0 +1,4 @@
.idea/
*.iml
target/
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>jpql-solution-03</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -0,0 +1,21 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,21 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,22 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,39 @@
package ru.otus.example.ormdemo.models;
import lombok.*;
import javax.persistence.*;
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.EAGER)
@JoinColumn(name = "student_id")
private List<EMail> emails;
// Указывает на связь между таблицами "многие ко многим"
@ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
// Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
@JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@@ -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<OtusStudent> findById(long id);
List<OtusStudent> findAll();
List<OtusStudent> findByName(String name);
void updateNameById(long id, String name);
void deleteById(long id);
}
@@ -0,0 +1,76 @@
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 javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import java.util.List;
import java.util.Optional;
// @Transactional должна стоять на методе сервиса.
// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
// выставить у аннотации параметр readOnly в true.
// Но это только упражнение и транзакции мы пока не проходили.
// Поэтому, для упрощения, пока вешаем над классом репозитория
@Transactional
@Repository
public class OtusStudentRepositoryJpa implements OtusStudentRepository {
@PersistenceContext
private EntityManager em;
@Override
public OtusStudent save(OtusStudent student) {
if (student.getId() <= 0) {
em.persist(student);
return student;
} else {
return em.merge(student);
}
}
@Override
public Optional<OtusStudent> findById(long id) {
return Optional.ofNullable(em.find(OtusStudent.class, id));
}
@Override
public List<OtusStudent> findAll() {
return em.createQuery("select s from OtusStudent s", OtusStudent.class)
.getResultList();
}
@Override
public List<OtusStudent> findByName(String name) {
TypedQuery<OtusStudent> 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) {
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();
}
@Override
public void deleteById(long id) {
Query query = em.createQuery("delete " +
"from OtusStudent s " +
"where s.id = :id");
query.setParameter("id", id);
query.executeUpdate();
}
}
@@ -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
@@ -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)
);
@@ -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(OtusStudentRepositoryJpa.class)
class OtusStudentRepositoryJpaTest {
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 OtusStudentRepositoryJpa 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() != 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<OtusStudent> 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();
}
}
@@ -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
@@ -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);
@@ -0,0 +1,4 @@
.idea/
*.iml
target/
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>jpql-solution-04</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -0,0 +1,21 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,21 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,22 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,42 @@
package ru.otus.example.ormdemo.models;
import lombok.*;
import javax.persistence.*;
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.EAGER)
@JoinColumn(name = "student_id")
private List<EMail> emails;
// Указывает на связь между таблицами "многие ко многим"
@ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
// Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
@JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@@ -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<OtusStudent> findById(long id);
List<OtusStudent> findAll();
List<OtusStudent> findByName(String name);
void updateNameById(long id, String name);
void deleteById(long id);
}
@@ -0,0 +1,79 @@
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 javax.persistence.*;
import java.util.List;
import java.util.Optional;
// @Transactional должна стоять на методе сервиса.
// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
// выставить у аннотации параметр readOnly в true.
// Но это только упражнение и транзакции мы пока не проходили.
// Поэтому, для упрощения, пока вешаем над классом репозитория
@Transactional
@Repository
public class OtusStudentRepositoryJpa implements OtusStudentRepository {
@PersistenceContext
private final EntityManager em;
public OtusStudentRepositoryJpa(EntityManager em) {
this.em = em;
}
@Override
public OtusStudent save(OtusStudent student) {
if (student.getId() <= 0) {
em.persist(student);
return student;
} else {
return em.merge(student);
}
}
@Override
public Optional<OtusStudent> findById(long id) {
return Optional.ofNullable(em.find(OtusStudent.class, id));
}
@Override
public List<OtusStudent> findAll() {
EntityGraph<?> entityGraph = em.getEntityGraph("otus-student-avatars-entity-graph");
TypedQuery<OtusStudent> query = em.createQuery("select s from OtusStudent s", OtusStudent.class);
query.setHint("javax.persistence.fetchgraph", entityGraph);
return query.getResultList();
}
@Override
public List<OtusStudent> findByName(String name) {
TypedQuery<OtusStudent> 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) {
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();
}
@Override
public void deleteById(long id) {
Query query = em.createQuery("delete " +
"from OtusStudent s " +
"where s.id = :id");
query.setParameter("id", id);
query.executeUpdate();
}
}
@@ -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
@@ -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)
);
@@ -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(OtusStudentRepositoryJpa.class)
class OtusStudentRepositoryJpaTest {
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 OtusStudentRepositoryJpa 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() != 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);
}
}
@@ -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
@@ -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);
@@ -0,0 +1,4 @@
.idea/
*.iml
target/
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>jpql-solution-05</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -0,0 +1,21 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,21 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,22 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,42 @@
package ru.otus.example.ormdemo.models;
import lombok.*;
import javax.persistence.*;
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.EAGER)
@JoinColumn(name = "student_id")
private List<EMail> emails;
// Указывает на связь между таблицами "многие ко многим"
@ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
// Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
@JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@@ -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<OtusStudent> findById(long id);
List<OtusStudent> findAll();
List<OtusStudent> findByName(String name);
void updateNameById(long id, String name);
void deleteById(long id);
}
@@ -0,0 +1,79 @@
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 javax.persistence.*;
import java.util.List;
import java.util.Optional;
// @Transactional должна стоять на методе сервиса.
// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
// выставить у аннотации параметр readOnly в true.
// Но это только упражнение и транзакции мы пока не проходили.
// Поэтому, для упрощения, пока вешаем над классом репозитория
@Transactional
@Repository
public class OtusStudentRepositoryJpa implements OtusStudentRepository {
@PersistenceContext
private final EntityManager em;
public OtusStudentRepositoryJpa(EntityManager em) {
this.em = em;
}
@Override
public OtusStudent save(OtusStudent student) {
if (student.getId() <= 0) {
em.persist(student);
return student;
} else {
return em.merge(student);
}
}
@Override
public Optional<OtusStudent> findById(long id) {
return Optional.ofNullable(em.find(OtusStudent.class, id));
}
@Override
public List<OtusStudent> findAll() {
EntityGraph<?> entityGraph = em.getEntityGraph("otus-student-avatars-entity-graph");
TypedQuery<OtusStudent> query = em.createQuery("select s from OtusStudent s join fetch s.emails", OtusStudent.class);
query.setHint("javax.persistence.fetchgraph", entityGraph);
return query.getResultList();
}
@Override
public List<OtusStudent> findByName(String name) {
TypedQuery<OtusStudent> 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) {
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();
}
@Override
public void deleteById(long id) {
Query query = em.createQuery("delete " +
"from OtusStudent s " +
"where s.id = :id");
query.setParameter("id", id);
query.executeUpdate();
}
}
@@ -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
@@ -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)
);
@@ -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(OtusStudentRepositoryJpa.class)
class OtusStudentRepositoryJpaTest {
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 OtusStudentRepositoryJpa 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() != 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);
}
}
@@ -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
@@ -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);
@@ -0,0 +1,4 @@
.idea/
*.iml
target/
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>jpql-solution-06</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -0,0 +1,21 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,21 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,22 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
@@ -0,0 +1,48 @@
package ru.otus.example.ormdemo.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import javax.persistence.*;
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.EAGER)
@JoinColumn(name = "student_id")
private List<EMail> emails;
// Все данные талицы будут загружены в память отдельным запросом и соединены с родительской сущностью
@Fetch(FetchMode.SUBSELECT)
// Указывает на связь между таблицами "многие ко многим"
@ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
// Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
@JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@@ -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<OtusStudent> findById(long id);
List<OtusStudent> findAll();
List<OtusStudent> findByName(String name);
void updateNameById(long id, String name);
void deleteById(long id);
}
@@ -0,0 +1,79 @@
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 javax.persistence.*;
import java.util.List;
import java.util.Optional;
// @Transactional должна стоять на методе сервиса.
// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
// выставить у аннотации параметр readOnly в true.
// Но это только упражнение и транзакции мы пока не проходили.
// Поэтому, для упрощения, пока вешаем над классом репозитория
@Transactional
@Repository
public class OtusStudentRepositoryJpa implements OtusStudentRepository {
@PersistenceContext
private final EntityManager em;
public OtusStudentRepositoryJpa(EntityManager em) {
this.em = em;
}
@Override
public OtusStudent save(OtusStudent student) {
if (student.getId() <= 0) {
em.persist(student);
return student;
} else {
return em.merge(student);
}
}
@Override
public Optional<OtusStudent> findById(long id) {
return Optional.ofNullable(em.find(OtusStudent.class, id));
}
@Override
public List<OtusStudent> findAll() {
EntityGraph<?> entityGraph = em.getEntityGraph("otus-student-avatars-entity-graph");
TypedQuery<OtusStudent> query = em.createQuery("select s from OtusStudent s join fetch s.emails", OtusStudent.class);
query.setHint("javax.persistence.fetchgraph", entityGraph);
return query.getResultList();
}
@Override
public List<OtusStudent> findByName(String name) {
TypedQuery<OtusStudent> 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) {
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();
}
@Override
public void deleteById(long id) {
Query query = em.createQuery("delete " +
"from OtusStudent s " +
"where s.id = :id");
query.setParameter("id", id);
query.executeUpdate();
}
}
@@ -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
@@ -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)
);
@@ -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(OtusStudentRepositoryJpa.class)
class OtusStudentRepositoryJpaTest {
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 OtusStudentRepositoryJpa 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() != 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);
}
}
@@ -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
@@ -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);
@@ -0,0 +1,4 @@
.idea/
*.iml
target/
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus</groupId>
<artifactId>jpql-solution-final</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

Some files were not shown because too many files have changed in this diff Show More