mirror of
https://github.com/OtusTeam/Spring.git
synced 2026-05-30 10:50:42 +00:00
2021-08 spring-09-jpql added
This commit is contained in:
@@ -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>
|
||||
+13
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+22
@@ -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;
|
||||
}
|
||||
+39
@@ -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;
|
||||
}
|
||||
+18
@@ -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);
|
||||
}
|
||||
+58
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -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)
|
||||
);
|
||||
+126
@@ -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();
|
||||
}
|
||||
}
|
||||
+17
@@ -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>
|
||||
+13
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+22
@@ -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;
|
||||
}
|
||||
+39
@@ -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;
|
||||
}
|
||||
+18
@@ -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);
|
||||
}
|
||||
+64
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -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)
|
||||
);
|
||||
+126
@@ -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();
|
||||
}
|
||||
}
|
||||
+17
@@ -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>
|
||||
+13
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+22
@@ -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;
|
||||
}
|
||||
+39
@@ -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;
|
||||
}
|
||||
+18
@@ -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);
|
||||
}
|
||||
+70
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -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)
|
||||
);
|
||||
+126
@@ -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();
|
||||
}
|
||||
}
|
||||
+17
@@ -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>
|
||||
+13
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+22
@@ -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;
|
||||
}
|
||||
+39
@@ -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;
|
||||
}
|
||||
+18
@@ -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);
|
||||
}
|
||||
+76
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -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)
|
||||
);
|
||||
+126
@@ -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();
|
||||
}
|
||||
}
|
||||
+17
@@ -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>
|
||||
+13
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+22
@@ -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;
|
||||
}
|
||||
+42
@@ -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;
|
||||
}
|
||||
+18
@@ -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);
|
||||
}
|
||||
+79
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -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)
|
||||
);
|
||||
+58
@@ -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);
|
||||
}
|
||||
}
|
||||
+17
@@ -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>
|
||||
+13
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+22
@@ -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;
|
||||
}
|
||||
+42
@@ -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;
|
||||
}
|
||||
+18
@@ -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);
|
||||
}
|
||||
+79
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -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)
|
||||
);
|
||||
+58
@@ -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);
|
||||
}
|
||||
}
|
||||
+17
@@ -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>
|
||||
+13
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+22
@@ -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;
|
||||
}
|
||||
+48
@@ -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;
|
||||
}
|
||||
+18
@@ -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);
|
||||
}
|
||||
+79
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -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)
|
||||
);
|
||||
+58
@@ -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);
|
||||
}
|
||||
}
|
||||
+17
@@ -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
Reference in New Issue
Block a user