2024-11 spring-11-jpql added

This commit is contained in:
stvort
2025-01-17 20:30:56 +04:00
parent 4156858562
commit c057e1dbf8
134 changed files with 5055 additions and 0 deletions
@@ -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-exercise</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.1</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package ru.otus.example.ormdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrmDemoApplication {
public static void main(String[] args) {
SpringApplication.run(OrmDemoApplication.class, args);
}
}
@@ -0,0 +1,25 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "avatars")
public class Avatar {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "photo_url", nullable = false, unique = true)
private String photoUrl;
}
@@ -0,0 +1,25 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name", nullable = false, unique = true)
private String name;
}
@@ -0,0 +1,26 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "emails")
public class EMail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "email", nullable = false, unique = true)
private String email;
}
@@ -0,0 +1,53 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity // Указывает, что данный класс является сущностью
@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
public class OtusStudent {
@Id // Позволяет указать какое поле является идентификатором
@GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
private long id;
// Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
@Column(name = "name", nullable = false, unique = true)
private String name;
// Указывает на связь между таблицами "один к одному"
@OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
// Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
@JoinColumn(name = "avatar_id")
private Avatar avatar;
// Указывает на связь между таблицами "один ко многим"
@OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "student_id")
private List<EMail> emails;
// Указывает на связь между таблицами "многие ко многим"
@ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
// Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
@JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@@ -0,0 +1,58 @@
package ru.otus.example.ormdemo.repositories;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import ru.otus.example.ormdemo.models.OtusStudent;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
// @Transactional должна стоять на методе сервиса.
// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
// выставить у аннотации параметр readOnly в true.
// Но это только упражнение и транзакции мы пока не проходили.
// Поэтому, для упрощения, пока вешаем над классом репозитория
@Transactional
@Repository
public class JpaOtusStudentRepository implements OtusStudentRepository {
@PersistenceContext
private final EntityManager em;
public JpaOtusStudentRepository(EntityManager em) {
this.em = em;
}
@Override
public OtusStudent save(OtusStudent student) {
return null;
}
@Override
public Optional<OtusStudent> findById(long id) {
return Optional.empty();
}
@Override
public List<OtusStudent> findAll() {
return Collections.emptyList();
}
@Override
public List<OtusStudent> findByName(String name) {
return Collections.emptyList();
}
@Override
public void updateNameById(long id, String name) {
}
@Override
public void deleteById(long id) {
}
}
@@ -0,0 +1,18 @@
package ru.otus.example.ormdemo.repositories;
import ru.otus.example.ormdemo.models.OtusStudent;
import java.util.List;
import java.util.Optional;
public interface OtusStudentRepository {
OtusStudent save(OtusStudent student);
Optional<OtusStudent> findById(long id);
List<OtusStudent> findAll();
List<OtusStudent> findByName(String name);
void updateNameById(long id, String name);
void deleteById(long id);
}
@@ -0,0 +1,18 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
sql:
init:
mode: always
jpa:
generate-ddl: false
hibernate:
ddl-auto: none
show-sql: true
logging:
level:
ROOT: ERROR
@@ -0,0 +1,31 @@
create table avatars(
id bigserial,
photo_url varchar(8000),
primary key (id)
);
create table courses(
id bigserial,
name varchar(255),
primary key (id)
);
create table otus_students(
id bigserial,
name varchar(255),
avatar_id bigint references avatars (id),
primary key (id)
);
create table emails(
id bigserial,
student_id bigint references otus_students(id) on delete cascade,
email varchar(255),
primary key (id)
);
create table student_courses(
student_id bigint references otus_students(id) on delete cascade,
course_id bigint references courses(id),
primary key (student_id, course_id)
);
@@ -0,0 +1,126 @@
package ru.otus.example.ormdemo.repositories;
import lombok.val;
import org.hibernate.SessionFactory;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.context.annotation.Import;
import ru.otus.example.ormdemo.models.OtusStudent;
import ru.otus.example.ormdemo.models.Avatar;
import ru.otus.example.ormdemo.models.Course;
import ru.otus.example.ormdemo.models.EMail;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
@DataJpaTest
@Import(JpaOtusStudentRepository.class)
class JpaOtusStudentRepositoryTest {
private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
private static final long FIRST_STUDENT_ID = 1L;
private static final String FIRST_STUDENT_NAME = "student_01";
private static final int EXPECTED_QUERIES_COUNT = 31;
private static final String STUDENT_AVATAR_URL = "где-то там";
private static final String STUDENT_EMAIL = "any@mail.com";
private static final String COURSE_NAME = "Spring";
private static final String STUDENT_NAME = "Вася";
@Autowired
private JpaOtusStudentRepository repositoryJpa;
@Autowired
private TestEntityManager em;
@DisplayName(" должен загружать информацию о нужном студенте по его id")
@Test
void shouldFindExpectedStudentById() {
val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(optionalActualStudent).isPresent().get()
.usingRecursiveComparison().isEqualTo(expectedStudent);
}
@DisplayName("должен загружать список всех студентов с полной информацией о них")
@Test
void shouldReturnCorrectStudentsListWithAllInfo() {
SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
.unwrap(SessionFactory.class);
sessionFactory.getStatistics().setStatisticsEnabled(true);
System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
val students = repositoryJpa.findAll();
assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
.allMatch(s -> !s.getName().equals(""))
.allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
.allMatch(s -> s.getAvatar().getPhotoUrl() != null)
.allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
}
@DisplayName(" должен корректно сохранять всю информацию о студенте")
@Test
void shouldSaveAllStudentInfo() {
val avatar = new Avatar(0, STUDENT_AVATAR_URL);
val email = new EMail(0, STUDENT_EMAIL);
val emails = Collections.singletonList(email);
val course = new Course(0, COURSE_NAME);
val courses = Collections.singletonList(course);
val vasya = new OtusStudent(0, STUDENT_NAME, avatar, emails, courses);
repositoryJpa.save(vasya);
assertThat(vasya.getId()).isGreaterThan(0);
val actualStudent = em.find(OtusStudent.class, vasya.getId());
assertThat(actualStudent).isNotNull().matches(s -> !s.getName().equals(""))
.matches(s -> s.getCourses() != null && s.getCourses().size() > 0 && s.getCourses().get(0).getId() > 0)
.matches(s -> s.getAvatar() != null)
.matches(s -> s.getEmails() != null && s.getEmails().size() > 0);
}
@DisplayName(" должен загружать информацию о нужном студенте по его имени")
@Test
void shouldFindExpectedStudentByName() {
val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
List<OtusStudent> students = repositoryJpa.findByName(FIRST_STUDENT_NAME);
assertThat(students).containsOnlyOnce(firstStudent);
}
@DisplayName(" должен изменять имя заданного студента по его id")
@Test
void shouldUpdateStudentNameById() {
val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
String oldName = firstStudent.getName();
em.detach(firstStudent);
repositoryJpa.updateNameById(FIRST_STUDENT_ID, STUDENT_NAME);
val updatedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(updatedStudent.getName()).isNotEqualTo(oldName).isEqualTo(STUDENT_NAME);
}
@DisplayName(" должен удалять заданного студента по его id")
@Test
void shouldDeleteStudentNameById() {
val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(firstStudent).isNotNull();
em.detach(firstStudent);
repositoryJpa.deleteById(FIRST_STUDENT_ID);
val deletedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(deletedStudent).isNull();
}
}
@@ -0,0 +1,20 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
sql:
init:
mode: always
jpa:
generate-ddl: false
#generate-ddl: true
hibernate:
ddl-auto: none
#ddl-auto: create-drop
#show-sql: true
logging:
level:
ROOT: ERROR
@@ -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-01</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.1</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package ru.otus.example.ormdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrmDemoApplication {
public static void main(String[] args) {
SpringApplication.run(OrmDemoApplication.class, args);
}
}
@@ -0,0 +1,25 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "avatars")
public class Avatar {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "photo_url", nullable = false, unique = true)
private String photoUrl;
}
@@ -0,0 +1,26 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name", nullable = false, unique = true)
private String name;
}
@@ -0,0 +1,26 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "emails")
public class EMail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "email", nullable = false, unique = true)
private String email;
}
@@ -0,0 +1,53 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity // Указывает, что данный класс является сущностью
@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
public class OtusStudent {
@Id // Позволяет указать какое поле является идентификатором
@GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
private long id;
// Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
@Column(name = "name", nullable = false, unique = true)
private String name;
// Указывает на связь между таблицами "один к одному"
@OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
// Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
@JoinColumn(name = "avatar_id")
private Avatar avatar;
// Указывает на связь между таблицами "один ко многим"
@OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "student_id")
private List<EMail> emails;
// Указывает на связь между таблицами "многие ко многим"
@ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
// Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
@JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@@ -0,0 +1,64 @@
package ru.otus.example.ormdemo.repositories;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import ru.otus.example.ormdemo.models.OtusStudent;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
// @Transactional должна стоять на методе сервиса.
// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
// выставить у аннотации параметр readOnly в true.
// Но это только упражнение и транзакции мы пока не проходили.
// Поэтому, для упрощения, пока вешаем над классом репозитория
@Transactional
@Repository
public class JpaOtusStudentRepository implements OtusStudentRepository {
@PersistenceContext
private final EntityManager em;
public JpaOtusStudentRepository(EntityManager em) {
this.em = em;
}
@Override
public OtusStudent save(OtusStudent student) {
if (student.getId() == 0) {
em.persist(student);
return student;
}
return em.merge(student);
}
@Override
public Optional<OtusStudent> findById(long id) {
return Optional.ofNullable(em.find(OtusStudent.class, id));
}
@Override
public List<OtusStudent> findAll() {
return Collections.emptyList();
}
@Override
public List<OtusStudent> findByName(String name) {
return Collections.emptyList();
}
@Override
public void updateNameById(long id, String name) {
}
@Override
public void deleteById(long id) {
}
}
@@ -0,0 +1,18 @@
package ru.otus.example.ormdemo.repositories;
import ru.otus.example.ormdemo.models.OtusStudent;
import java.util.List;
import java.util.Optional;
public interface OtusStudentRepository {
OtusStudent save(OtusStudent student);
Optional<OtusStudent> findById(long id);
List<OtusStudent> findAll();
List<OtusStudent> findByName(String name);
void updateNameById(long id, String name);
void deleteById(long id);
}
@@ -0,0 +1,18 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
sql:
init:
mode: always
jpa:
generate-ddl: false
hibernate:
ddl-auto: none
show-sql: true
logging:
level:
ROOT: ERROR
@@ -0,0 +1,31 @@
create table avatars(
id bigserial,
photo_url varchar(8000),
primary key (id)
);
create table courses(
id bigserial,
name varchar(255),
primary key (id)
);
create table otus_students(
id bigserial,
name varchar(255),
avatar_id bigint references avatars (id),
primary key (id)
);
create table emails(
id bigserial,
student_id bigint references otus_students(id) on delete cascade,
email varchar(255),
primary key (id)
);
create table student_courses(
student_id bigint references otus_students(id) on delete cascade,
course_id bigint references courses(id),
primary key (student_id, course_id)
);
@@ -0,0 +1,126 @@
package ru.otus.example.ormdemo.repositories;
import lombok.val;
import org.hibernate.SessionFactory;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.context.annotation.Import;
import ru.otus.example.ormdemo.models.OtusStudent;
import ru.otus.example.ormdemo.models.Avatar;
import ru.otus.example.ormdemo.models.Course;
import ru.otus.example.ormdemo.models.EMail;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
@DataJpaTest
@Import(JpaOtusStudentRepository.class)
class JpaOtusStudentRepositoryTest {
private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
private static final long FIRST_STUDENT_ID = 1L;
private static final String FIRST_STUDENT_NAME = "student_01";
private static final int EXPECTED_QUERIES_COUNT = 31;
private static final String STUDENT_AVATAR_URL = "где-то там";
private static final String STUDENT_EMAIL = "any@mail.com";
private static final String COURSE_NAME = "Spring";
private static final String STUDENT_NAME = "Вася";
@Autowired
private JpaOtusStudentRepository repositoryJpa;
@Autowired
private TestEntityManager em;
@DisplayName(" должен загружать информацию о нужном студенте по его id")
@Test
void shouldFindExpectedStudentById() {
val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(optionalActualStudent).isPresent().get()
.usingRecursiveComparison().isEqualTo(expectedStudent);
}
@DisplayName("должен загружать список всех студентов с полной информацией о них")
@Test
void shouldReturnCorrectStudentsListWithAllInfo() {
SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
.unwrap(SessionFactory.class);
sessionFactory.getStatistics().setStatisticsEnabled(true);
System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
val students = repositoryJpa.findAll();
assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
.allMatch(s -> !s.getName().equals(""))
.allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
.allMatch(s -> s.getAvatar().getPhotoUrl() != null)
.allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
}
@DisplayName(" должен корректно сохранять всю информацию о студенте")
@Test
void shouldSaveAllStudentInfo() {
val avatar = new Avatar(0, STUDENT_AVATAR_URL);
val email = new EMail(0, STUDENT_EMAIL);
val emails = Collections.singletonList(email);
val course = new Course(0, COURSE_NAME);
val courses = Collections.singletonList(course);
val vasya = new OtusStudent(0, STUDENT_NAME, avatar, emails, courses);
repositoryJpa.save(vasya);
assertThat(vasya.getId()).isGreaterThan(0);
val actualStudent = em.find(OtusStudent.class, vasya.getId());
assertThat(actualStudent).isNotNull().matches(s -> !s.getName().equals(""))
.matches(s -> s.getCourses() != null && s.getCourses().size() > 0 && s.getCourses().get(0).getId() > 0)
.matches(s -> s.getAvatar() != null)
.matches(s -> s.getEmails() != null && s.getEmails().size() > 0);
}
@DisplayName(" должен загружать информацию о нужном студенте по его имени")
@Test
void shouldFindExpectedStudentByName() {
val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
List<OtusStudent> students = repositoryJpa.findByName(FIRST_STUDENT_NAME);
assertThat(students).containsOnlyOnce(firstStudent);
}
@DisplayName(" должен изменять имя заданного студента по его id")
@Test
void shouldUpdateStudentNameById() {
val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
String oldName = firstStudent.getName();
em.detach(firstStudent);
repositoryJpa.updateNameById(FIRST_STUDENT_ID, STUDENT_NAME);
val updatedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(updatedStudent.getName()).isNotEqualTo(oldName).isEqualTo(STUDENT_NAME);
}
@DisplayName(" должен удалять заданного студента по его id")
@Test
void shouldDeleteStudentNameById() {
val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(firstStudent).isNotNull();
em.detach(firstStudent);
repositoryJpa.deleteById(FIRST_STUDENT_ID);
val deletedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(deletedStudent).isNull();
}
}
@@ -0,0 +1,20 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
sql:
init:
mode: always
jpa:
generate-ddl: false
#generate-ddl: true
hibernate:
ddl-auto: none
#ddl-auto: create-drop
#show-sql: true
logging:
level:
ROOT: ERROR
@@ -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-02</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.1</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package ru.otus.example.ormdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrmDemoApplication {
public static void main(String[] args) {
SpringApplication.run(OrmDemoApplication.class, args);
}
}
@@ -0,0 +1,25 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "avatars")
public class Avatar {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "photo_url", nullable = false, unique = true)
private String photoUrl;
}
@@ -0,0 +1,25 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name", nullable = false, unique = true)
private String name;
}
@@ -0,0 +1,26 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "emails")
public class EMail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "email", nullable = false, unique = true)
private String email;
}
@@ -0,0 +1,53 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity // Указывает, что данный класс является сущностью
@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
public class OtusStudent {
@Id // Позволяет указать какое поле является идентификатором
@GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
private long id;
// Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
@Column(name = "name", nullable = false, unique = true)
private String name;
// Указывает на связь между таблицами "один к одному"
@OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
// Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
@JoinColumn(name = "avatar_id")
private Avatar avatar;
// Указывает на связь между таблицами "один ко многим"
@OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "student_id")
private List<EMail> emails;
// Указывает на связь между таблицами "многие ко многим"
@ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
// Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
@JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@@ -0,0 +1,69 @@
package ru.otus.example.ormdemo.repositories;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import ru.otus.example.ormdemo.models.OtusStudent;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import java.util.List;
import java.util.Optional;
// @Transactional должна стоять на методе сервиса.
// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
// выставить у аннотации параметр readOnly в true.
// Но это только упражнение и транзакции мы пока не проходили.
// Поэтому, для упрощения, пока вешаем над классом репозитория
@Transactional
@Repository
public class JpaOtusStudentRepository implements OtusStudentRepository {
@PersistenceContext
private final EntityManager em;
public JpaOtusStudentRepository(EntityManager em) {
this.em = em;
}
@Override
public OtusStudent save(OtusStudent student) {
if (student.getId() == 0) {
em.persist(student);
return student;
}
return em.merge(student);
}
@Override
public Optional<OtusStudent> findById(long id) {
return Optional.ofNullable(em.find(OtusStudent.class, id));
}
@Override
public List<OtusStudent> findAll() {
return em.createQuery("select s from OtusStudent s", OtusStudent.class)
.getResultList();
}
@Override
public List<OtusStudent> findByName(String name) {
TypedQuery<OtusStudent> query = em.createQuery("select s " +
"from OtusStudent s " +
"where s.name = :name",
OtusStudent.class);
query.setParameter("name", name);
return query.getResultList();
}
@Override
public void updateNameById(long id, String name) {
}
@Override
public void deleteById(long id) {
}
}
@@ -0,0 +1,18 @@
package ru.otus.example.ormdemo.repositories;
import ru.otus.example.ormdemo.models.OtusStudent;
import java.util.List;
import java.util.Optional;
public interface OtusStudentRepository {
OtusStudent save(OtusStudent student);
Optional<OtusStudent> findById(long id);
List<OtusStudent> findAll();
List<OtusStudent> findByName(String name);
void updateNameById(long id, String name);
void deleteById(long id);
}
@@ -0,0 +1,18 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
sql:
init:
mode: always
jpa:
generate-ddl: false
hibernate:
ddl-auto: none
show-sql: true
logging:
level:
ROOT: ERROR
@@ -0,0 +1,31 @@
create table avatars(
id bigserial,
photo_url varchar(8000),
primary key (id)
);
create table courses(
id bigserial,
name varchar(255),
primary key (id)
);
create table otus_students(
id bigserial,
name varchar(255),
avatar_id bigint references avatars (id),
primary key (id)
);
create table emails(
id bigserial,
student_id bigint references otus_students(id) on delete cascade,
email varchar(255),
primary key (id)
);
create table student_courses(
student_id bigint references otus_students(id) on delete cascade,
course_id bigint references courses(id),
primary key (student_id, course_id)
);
@@ -0,0 +1,126 @@
package ru.otus.example.ormdemo.repositories;
import lombok.val;
import org.hibernate.SessionFactory;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.context.annotation.Import;
import ru.otus.example.ormdemo.models.OtusStudent;
import ru.otus.example.ormdemo.models.Avatar;
import ru.otus.example.ormdemo.models.Course;
import ru.otus.example.ormdemo.models.EMail;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
@DataJpaTest
@Import(JpaOtusStudentRepository.class)
class JpaOtusStudentRepositoryTest {
private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
private static final long FIRST_STUDENT_ID = 1L;
private static final String FIRST_STUDENT_NAME = "student_01";
private static final int EXPECTED_QUERIES_COUNT = 31;
private static final String STUDENT_AVATAR_URL = "где-то там";
private static final String STUDENT_EMAIL = "any@mail.com";
private static final String COURSE_NAME = "Spring";
private static final String STUDENT_NAME = "Вася";
@Autowired
private JpaOtusStudentRepository repositoryJpa;
@Autowired
private TestEntityManager em;
@DisplayName(" должен загружать информацию о нужном студенте по его id")
@Test
void shouldFindExpectedStudentById() {
val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(optionalActualStudent).isPresent().get()
.usingRecursiveComparison().isEqualTo(expectedStudent);
}
@DisplayName("должен загружать список всех студентов с полной информацией о них")
@Test
void shouldReturnCorrectStudentsListWithAllInfo() {
SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
.unwrap(SessionFactory.class);
sessionFactory.getStatistics().setStatisticsEnabled(true);
System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
val students = repositoryJpa.findAll();
assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
.allMatch(s -> !s.getName().equals(""))
.allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
.allMatch(s -> s.getAvatar().getPhotoUrl() != null)
.allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
}
@DisplayName(" должен корректно сохранять всю информацию о студенте")
@Test
void shouldSaveAllStudentInfo() {
val avatar = new Avatar(0, STUDENT_AVATAR_URL);
val email = new EMail(0, STUDENT_EMAIL);
val emails = Collections.singletonList(email);
val course = new Course(0, COURSE_NAME);
val courses = Collections.singletonList(course);
val vasya = new OtusStudent(0, STUDENT_NAME, avatar, emails, courses);
repositoryJpa.save(vasya);
assertThat(vasya.getId()).isGreaterThan(0);
val actualStudent = em.find(OtusStudent.class, vasya.getId());
assertThat(actualStudent).isNotNull().matches(s -> !s.getName().equals(""))
.matches(s -> s.getCourses() != null && s.getCourses().size() > 0 && s.getCourses().get(0).getId() > 0)
.matches(s -> s.getAvatar() != null)
.matches(s -> s.getEmails() != null && s.getEmails().size() > 0);
}
@DisplayName(" должен загружать информацию о нужном студенте по его имени")
@Test
void shouldFindExpectedStudentByName() {
val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
List<OtusStudent> students = repositoryJpa.findByName(FIRST_STUDENT_NAME);
assertThat(students).containsOnlyOnce(firstStudent);
}
@DisplayName(" должен изменять имя заданного студента по его id")
@Test
void shouldUpdateStudentNameById() {
val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
String oldName = firstStudent.getName();
em.detach(firstStudent);
repositoryJpa.updateNameById(FIRST_STUDENT_ID, STUDENT_NAME);
val updatedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(updatedStudent.getName()).isNotEqualTo(oldName).isEqualTo(STUDENT_NAME);
}
@DisplayName(" должен удалять заданного студента по его id")
@Test
void shouldDeleteStudentNameById() {
val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(firstStudent).isNotNull();
em.detach(firstStudent);
repositoryJpa.deleteById(FIRST_STUDENT_ID);
val deletedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(deletedStudent).isNull();
}
}
@@ -0,0 +1,20 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
sql:
init:
mode: always
jpa:
generate-ddl: false
#generate-ddl: true
hibernate:
ddl-auto: none
#ddl-auto: create-drop
#show-sql: true
logging:
level:
ROOT: ERROR
@@ -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,57 @@
<?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>3.4.1</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package ru.otus.example.ormdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrmDemoApplication {
public static void main(String[] args) {
SpringApplication.run(OrmDemoApplication.class, args);
}
}
@@ -0,0 +1,25 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "avatars")
public class Avatar {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "photo_url", nullable = false, unique = true)
private String photoUrl;
}
@@ -0,0 +1,25 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name", nullable = false, unique = true)
private String name;
}
@@ -0,0 +1,26 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "emails")
public class EMail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "email", nullable = false, unique = true)
private String email;
}
@@ -0,0 +1,53 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity // Указывает, что данный класс является сущностью
@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
public class OtusStudent {
@Id // Позволяет указать какое поле является идентификатором
@GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
private long id;
// Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
@Column(name = "name", nullable = false, unique = true)
private String name;
// Указывает на связь между таблицами "один к одному"
@OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
// Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
@JoinColumn(name = "avatar_id")
private Avatar avatar;
// Указывает на связь между таблицами "один ко многим"
@OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "student_id")
private List<EMail> emails;
// Указывает на связь между таблицами "многие ко многим"
@ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
// Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
@JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@@ -0,0 +1,77 @@
package ru.otus.example.ormdemo.repositories;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import ru.otus.example.ormdemo.models.OtusStudent;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import java.util.List;
import java.util.Optional;
// @Transactional должна стоять на методе сервиса.
// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
// выставить у аннотации параметр readOnly в true.
// Но это только упражнение и транзакции мы пока не проходили.
// Поэтому, для упрощения, пока вешаем над классом репозитория
@Transactional
@Repository
public class JpaOtusStudentRepository implements OtusStudentRepository {
@PersistenceContext
private EntityManager em;
@Override
public OtusStudent save(OtusStudent student) {
if (student.getId() == 0) {
em.persist(student);
return student;
}
return em.merge(student);
}
@Override
public Optional<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();
}
// Только для примера, в реальности JPQL лучше использовать только для массовых операций
@Override
public void updateNameById(long id, String name) {
Query query = em.createQuery("update OtusStudent s " +
"set s.name = :name " +
"where s.id = :id");
query.setParameter("name", name);
query.setParameter("id", id);
query.executeUpdate();
}
// Только для примера, в реальности JPQL лучше использовать только для массовых операций
@Override
public void deleteById(long id) {
Query query = em.createQuery("delete " +
"from OtusStudent s " +
"where s.id = :id");
query.setParameter("id", id);
query.executeUpdate();
}
}
@@ -0,0 +1,18 @@
package ru.otus.example.ormdemo.repositories;
import ru.otus.example.ormdemo.models.OtusStudent;
import java.util.List;
import java.util.Optional;
public interface OtusStudentRepository {
OtusStudent save(OtusStudent student);
Optional<OtusStudent> findById(long id);
List<OtusStudent> findAll();
List<OtusStudent> findByName(String name);
void updateNameById(long id, String name);
void deleteById(long id);
}
@@ -0,0 +1,18 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
sql:
init:
mode: always
jpa:
generate-ddl: false
hibernate:
ddl-auto: none
show-sql: true
logging:
level:
ROOT: ERROR
@@ -0,0 +1,31 @@
create table avatars(
id bigserial,
photo_url varchar(8000),
primary key (id)
);
create table courses(
id bigserial,
name varchar(255),
primary key (id)
);
create table otus_students(
id bigserial,
name varchar(255),
avatar_id bigint references avatars (id),
primary key (id)
);
create table emails(
id bigserial,
student_id bigint references otus_students(id) on delete cascade,
email varchar(255),
primary key (id)
);
create table student_courses(
student_id bigint references otus_students(id) on delete cascade,
course_id bigint references courses(id),
primary key (student_id, course_id)
);
@@ -0,0 +1,127 @@
package ru.otus.example.ormdemo.repositories;
import lombok.val;
import org.hibernate.SessionFactory;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.context.annotation.Import;
import ru.otus.example.ormdemo.models.OtusStudent;
import ru.otus.example.ormdemo.models.Avatar;
import ru.otus.example.ormdemo.models.Course;
import ru.otus.example.ormdemo.models.EMail;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
@DataJpaTest
@Import(JpaOtusStudentRepository.class)
class JpaOtusStudentRepositoryTest {
private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
private static final long FIRST_STUDENT_ID = 1L;
private static final String FIRST_STUDENT_NAME = "student_01";
private static final int EXPECTED_QUERIES_COUNT = 31;
private static final String STUDENT_AVATAR_URL = "где-то там";
private static final String STUDENT_EMAIL = "any@mail.com";
private static final String COURSE_NAME = "Spring";
private static final String STUDENT_NAME = "Вася";
@Autowired
private JpaOtusStudentRepository repositoryJpa;
@Autowired
private TestEntityManager em;
@DisplayName(" должен загружать информацию о нужном студенте по его id")
@Test
void shouldFindExpectedStudentById() {
val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(optionalActualStudent).isPresent().get()
.usingRecursiveComparison().isEqualTo(expectedStudent);
}
@DisplayName("должен загружать список всех студентов с полной информацией о них")
@Test
void shouldReturnCorrectStudentsListWithAllInfo() {
SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
.unwrap(SessionFactory.class);
sessionFactory.getStatistics().setStatisticsEnabled(true);
System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
val students = repositoryJpa.findAll();
assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
.allMatch(s -> !s.getName().equals(""))
.allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
.allMatch(s -> s.getAvatar().getPhotoUrl() != null)
.allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
}
@DisplayName(" должен корректно сохранять всю информацию о студенте")
@Test
void shouldSaveAllStudentInfo() {
val avatar = new Avatar(0, STUDENT_AVATAR_URL);
val email = new EMail(0, STUDENT_EMAIL);
val emails = Collections.singletonList(email);
val course = new Course(0, COURSE_NAME);
val courses = Collections.singletonList(course);
val vasya = new OtusStudent(0, STUDENT_NAME, avatar, emails, courses);
repositoryJpa.save(vasya);
assertThat(vasya.getId()).isGreaterThan(0);
val actualStudent = em.find(OtusStudent.class, vasya.getId());
assertThat(actualStudent).isNotNull().matches(s -> !s.getName().equals(""))
.matches(s -> s.getCourses() != null && s.getCourses().size() > 0 && s.getCourses().get(0).getId() > 0)
.matches(s -> s.getAvatar() != null)
.matches(s -> s.getEmails() != null && s.getEmails().size() > 0);
}
@DisplayName(" должен загружать информацию о нужном студенте по его имени")
@Test
void shouldFindExpectedStudentByName() {
val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
List<OtusStudent> students = repositoryJpa.findByName(FIRST_STUDENT_NAME);
assertThat(students).containsOnlyOnce(firstStudent);
}
@DisplayName(" должен изменять имя заданного студента по его id")
@Test
void shouldUpdateStudentNameById() {
val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
String oldName = firstStudent.getName();
em.detach(firstStudent);
repositoryJpa.updateNameById(FIRST_STUDENT_ID, STUDENT_NAME);
val updatedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(updatedStudent.getName()).isNotEqualTo(oldName).isEqualTo(STUDENT_NAME);
}
@DisplayName(" должен удалять заданного студента по его id")
@Test
void shouldDeleteStudentNameById() {
val firstStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(firstStudent).isNotNull();
em.detach(firstStudent);
repositoryJpa.deleteById(FIRST_STUDENT_ID);
val deletedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(deletedStudent).isNull();
}
}
@@ -0,0 +1,20 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
sql:
init:
mode: always
jpa:
generate-ddl: false
#generate-ddl: true
hibernate:
ddl-auto: none
#ddl-auto: create-drop
#show-sql: true
logging:
level:
ROOT: ERROR
@@ -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-04</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.1</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package ru.otus.example.ormdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrmDemoApplication {
public static void main(String[] args) {
SpringApplication.run(OrmDemoApplication.class, args);
}
}
@@ -0,0 +1,25 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "avatars")
public class Avatar {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "photo_url", nullable = false, unique = true)
private String photoUrl;
}
@@ -0,0 +1,25 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name", nullable = false, unique = true)
private String name;
}
@@ -0,0 +1,26 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "emails")
public class EMail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "email", nullable = false, unique = true)
private String email;
}
@@ -0,0 +1,58 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.NamedAttributeNode;
import jakarta.persistence.NamedEntityGraph;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity // Указывает, что данный класс является сущностью
@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
// Позволяет указать какие связи родительской сущности загружать в одном с ней запросе
@NamedEntityGraph(name = "otus-student-avatars-entity-graph",
attributeNodes = {@NamedAttributeNode("avatar")})
public class OtusStudent {
@Id // Позволяет указать какое поле является идентификатором
@GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
private long id;
// Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
@Column(name = "name", nullable = false, unique = true)
private String name;
// Указывает на связь между таблицами "один к одному"
@OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
// Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
@JoinColumn(name = "avatar_id")
private Avatar avatar;
// Указывает на связь между таблицами "один ко многим"
@OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "student_id")
private List<EMail> emails;
// Указывает на связь между таблицами "многие ко многим"
@ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
// Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
@JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@@ -0,0 +1,86 @@
package ru.otus.example.ormdemo.repositories;
import jakarta.persistence.EntityGraph;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import ru.otus.example.ormdemo.models.OtusStudent;
import java.util.List;
import java.util.Optional;
import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH;
// @Transactional должна стоять на методе сервиса.
// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
// выставить у аннотации параметр readOnly в true.
// Но это только упражнение и транзакции мы пока не проходили.
// Поэтому, для упрощения, пока вешаем над классом репозитория
@Transactional
@Repository
public class JpaOtusStudentRepository implements OtusStudentRepository {
@PersistenceContext
private final EntityManager em;
public JpaOtusStudentRepository(EntityManager em) {
this.em = em;
}
@Override
public OtusStudent save(OtusStudent student) {
if (student.getId() == 0) {
em.persist(student);
return student;
}
return em.merge(student);
}
@Override
public Optional<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(FETCH.getKey(), 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();
}
// Только для примера, в реальности JPQL лучше использовать только для массовых операций
@Override
public void updateNameById(long id, String name) {
Query query = em.createQuery("update OtusStudent s " +
"set s.name = :name " +
"where s.id = :id");
query.setParameter("name", name);
query.setParameter("id", id);
query.executeUpdate();
}
// Только для примера, в реальности JPQL лучше использовать только для массовых операций
@Override
public void deleteById(long id) {
Query query = em.createQuery("delete " +
"from OtusStudent s " +
"where s.id = :id");
query.setParameter("id", id);
query.executeUpdate();
}
}
@@ -0,0 +1,18 @@
package ru.otus.example.ormdemo.repositories;
import ru.otus.example.ormdemo.models.OtusStudent;
import java.util.List;
import java.util.Optional;
public interface OtusStudentRepository {
OtusStudent save(OtusStudent student);
Optional<OtusStudent> findById(long id);
List<OtusStudent> findAll();
List<OtusStudent> findByName(String name);
void updateNameById(long id, String name);
void deleteById(long id);
}
@@ -0,0 +1,18 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
sql:
init:
mode: always
jpa:
generate-ddl: false
hibernate:
ddl-auto: none
show-sql: true
logging:
level:
ROOT: ERROR
@@ -0,0 +1,31 @@
create table avatars(
id bigserial,
photo_url varchar(8000),
primary key (id)
);
create table courses(
id bigserial,
name varchar(255),
primary key (id)
);
create table otus_students(
id bigserial,
name varchar(255),
avatar_id bigint references avatars (id),
primary key (id)
);
create table emails(
id bigserial,
student_id bigint references otus_students(id) on delete cascade,
email varchar(255),
primary key (id)
);
create table student_courses(
student_id bigint references otus_students(id) on delete cascade,
course_id bigint references courses(id),
primary key (student_id, course_id)
);
@@ -0,0 +1,58 @@
package ru.otus.example.ormdemo.repositories;
import lombok.val;
import org.hibernate.SessionFactory;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.context.annotation.Import;
import ru.otus.example.ormdemo.models.OtusStudent;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
@DataJpaTest
@Import(JpaOtusStudentRepository.class)
class JpaOtusStudentRepositoryTest {
private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
private static final long FIRST_STUDENT_ID = 1L;
private static final int EXPECTED_QUERIES_COUNT = 21;
@Autowired
private JpaOtusStudentRepository repositoryJpa;
@Autowired
private TestEntityManager em;
@DisplayName(" должен загружать информацию о нужном студенте по его id")
@Test
void shouldFindExpectedStudentById() {
val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(optionalActualStudent).isPresent().get()
.usingRecursiveComparison().isEqualTo(expectedStudent);
}
@DisplayName("должен загружать список всех студентов с полной информацией о них")
@Test
void shouldReturnCorrectStudentsListWithAllInfo() {
SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
.unwrap(SessionFactory.class);
sessionFactory.getStatistics().setStatisticsEnabled(true);
System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
val students = repositoryJpa.findAll();
assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
.allMatch(s -> !s.getName().equals(""))
.allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
.allMatch(s -> s.getAvatar().getPhotoUrl() != null)
.allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
}
}
@@ -0,0 +1,20 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
sql:
init:
mode: always
jpa:
generate-ddl: false
#generate-ddl: true
hibernate:
ddl-auto: none
#ddl-auto: create-drop
#show-sql: true
logging:
level:
ROOT: ERROR
@@ -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,57 @@
<?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>3.4.1</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package ru.otus.example.ormdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrmDemoApplication {
public static void main(String[] args) {
SpringApplication.run(OrmDemoApplication.class, args);
}
}
@@ -0,0 +1,25 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "avatars")
public class Avatar {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "photo_url", nullable = false, unique = true)
private String photoUrl;
}
@@ -0,0 +1,25 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name", nullable = false, unique = true)
private String name;
}
@@ -0,0 +1,26 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "emails")
public class EMail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "email", nullable = false, unique = true)
private String email;
}
@@ -0,0 +1,58 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.NamedAttributeNode;
import jakarta.persistence.NamedEntityGraph;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity // Указывает, что данный класс является сущностью
@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
// Позволяет указать какие связи родительской сущности загружать в одном с ней запросе
@NamedEntityGraph(name = "otus-student-avatars-entity-graph",
attributeNodes = {@NamedAttributeNode("avatar")})
public class OtusStudent {
@Id // Позволяет указать какое поле является идентификатором
@GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
private long id;
// Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
@Column(name = "name", nullable = false, unique = true)
private String name;
// Указывает на связь между таблицами "один к одному"
@OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
// Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
@JoinColumn(name = "avatar_id")
private Avatar avatar;
// Указывает на связь между таблицами "один ко многим"
@OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "student_id")
private List<EMail> emails;
// Указывает на связь между таблицами "многие ко многим"
@ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
// Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
@JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@@ -0,0 +1,87 @@
package ru.otus.example.ormdemo.repositories;
import jakarta.persistence.EntityGraph;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import ru.otus.example.ormdemo.models.OtusStudent;
import java.util.List;
import java.util.Optional;
import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH;
// @Transactional должна стоять на методе сервиса.
// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
// выставить у аннотации параметр readOnly в true.
// Но это только упражнение и транзакции мы пока не проходили.
// Поэтому, для упрощения, пока вешаем над классом репозитория
@Transactional
@Repository
public class JpaOtusStudentRepository implements OtusStudentRepository {
@PersistenceContext
private final EntityManager em;
public JpaOtusStudentRepository(EntityManager em) {
this.em = em;
}
@Override
public OtusStudent save(OtusStudent student) {
if (student.getId() == 0) {
em.persist(student);
return student;
}
return em.merge(student);
}
@Override
public Optional<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 distinct s from OtusStudent s " +
"left join fetch s.emails", OtusStudent.class);
query.setHint(FETCH.getKey(), 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();
}
// Только для примера, в реальности JPQL лучше использовать только для массовых операций
@Override
public void updateNameById(long id, String name) {
Query query = em.createQuery("update OtusStudent s " +
"set s.name = :name " +
"where s.id = :id");
query.setParameter("name", name);
query.setParameter("id", id);
query.executeUpdate();
}
// Только для примера, в реальности JPQL лучше использовать только для массовых операций
@Override
public void deleteById(long id) {
Query query = em.createQuery("delete " +
"from OtusStudent s " +
"where s.id = :id");
query.setParameter("id", id);
query.executeUpdate();
}
}
@@ -0,0 +1,18 @@
package ru.otus.example.ormdemo.repositories;
import ru.otus.example.ormdemo.models.OtusStudent;
import java.util.List;
import java.util.Optional;
public interface OtusStudentRepository {
OtusStudent save(OtusStudent student);
Optional<OtusStudent> findById(long id);
List<OtusStudent> findAll();
List<OtusStudent> findByName(String name);
void updateNameById(long id, String name);
void deleteById(long id);
}
@@ -0,0 +1,15 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
initialization-mode: always
jpa:
generate-ddl: false
hibernate:
ddl-auto: none
show-sql: true
logging:
level:
ROOT: ERROR
@@ -0,0 +1,31 @@
create table avatars(
id bigserial,
photo_url varchar(8000),
primary key (id)
);
create table courses(
id bigserial,
name varchar(255),
primary key (id)
);
create table otus_students(
id bigserial,
name varchar(255),
avatar_id bigint references avatars (id),
primary key (id)
);
create table emails(
id bigserial,
student_id bigint references otus_students(id) on delete cascade,
email varchar(255),
primary key (id)
);
create table student_courses(
student_id bigint references otus_students(id) on delete cascade,
course_id bigint references courses(id),
primary key (student_id, course_id)
);
@@ -0,0 +1,58 @@
package ru.otus.example.ormdemo.repositories;
import lombok.val;
import org.hibernate.SessionFactory;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.context.annotation.Import;
import ru.otus.example.ormdemo.models.OtusStudent;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
@DataJpaTest
@Import(JpaOtusStudentRepository.class)
class JpaOtusStudentRepositoryTest {
private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
private static final long FIRST_STUDENT_ID = 1L;
private static final int EXPECTED_QUERIES_COUNT = 11;
@Autowired
private JpaOtusStudentRepository repositoryJpa;
@Autowired
private TestEntityManager em;
@DisplayName(" должен загружать информацию о нужном студенте по его id")
@Test
void shouldFindExpectedStudentById() {
val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(optionalActualStudent).isPresent().get()
.usingRecursiveComparison().isEqualTo(expectedStudent);
}
@DisplayName("должен загружать список всех студентов с полной информацией о них")
@Test
void shouldReturnCorrectStudentsListWithAllInfo() {
SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
.unwrap(SessionFactory.class);
sessionFactory.getStatistics().setStatisticsEnabled(true);
System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
val students = repositoryJpa.findAll();
assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
.allMatch(s -> !s.getName().equals(""))
.allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
.allMatch(s -> s.getAvatar().getPhotoUrl() != null)
.allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
}
}
@@ -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,57 @@
<?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>3.4.1</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package ru.otus.example.ormdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrmDemoApplication {
public static void main(String[] args) {
SpringApplication.run(OrmDemoApplication.class, args);
}
}
@@ -0,0 +1,25 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "avatars")
public class Avatar {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "photo_url", nullable = false, unique = true)
private String photoUrl;
}
@@ -0,0 +1,25 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name", nullable = false, unique = true)
private String name;
}
@@ -0,0 +1,26 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "emails")
public class EMail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "email", nullable = false, unique = true)
private String email;
}
@@ -0,0 +1,62 @@
package ru.otus.example.ormdemo.models;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.NamedAttributeNode;
import jakarta.persistence.NamedEntityGraph;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity // Указывает, что данный класс является сущностью
@Table(name = "otus_students") // Задает имя таблицы, на которую будет отображаться сущность
// Позволяет указать какие связи родительской сущности загружать в одном с ней запросе
@NamedEntityGraph(name = "otus-student-avatars-entity-graph",
attributeNodes = {@NamedAttributeNode("avatar")})
public class OtusStudent {
@Id // Позволяет указать какое поле является идентификатором
@GeneratedValue(strategy = GenerationType.IDENTITY) // Стратегия генерации идентификаторов
private long id;
// Задает имя и некоторые свойства поля таблицы, на которое будет отображаться поле сущности
@Column(name = "name", nullable = false, unique = true)
private String name;
// Указывает на связь между таблицами "один к одному"
@OneToOne(targetEntity = Avatar.class, cascade = CascadeType.ALL)
// Задает поле, по которому происходит объединение с таблицей для хранения связанной сущности
@JoinColumn(name = "avatar_id")
private Avatar avatar;
// Указывает на связь между таблицами "один ко многим"
@OneToMany(targetEntity = EMail.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "student_id")
private List<EMail> emails;
// Все данные талицы будут загружены в память отдельным запросом и соединены с родительской сущностью
@Fetch(FetchMode.SUBSELECT)
// Указывает на связь между таблицами "многие ко многим"
@ManyToMany(targetEntity = Course.class, fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
// Задает таблицу связей между таблицами для хранения родительской и связанной сущностью
@JoinTable(name = "student_courses", joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@@ -0,0 +1,87 @@
package ru.otus.example.ormdemo.repositories;
import jakarta.persistence.EntityGraph;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import ru.otus.example.ormdemo.models.OtusStudent;
import java.util.List;
import java.util.Optional;
import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.FETCH;
// @Transactional должна стоять на методе сервиса.
// Причем, если метод не подразумевает изменения данных в БД то категорически желательно
// выставить у аннотации параметр readOnly в true.
// Но это только упражнение и транзакции мы пока не проходили.
// Поэтому, для упрощения, пока вешаем над классом репозитория
@Transactional
@Repository
public class JpaOtusStudentRepository implements OtusStudentRepository {
@PersistenceContext
private final EntityManager em;
public JpaOtusStudentRepository(EntityManager em) {
this.em = em;
}
@Override
public OtusStudent save(OtusStudent student) {
if (student.getId() == 0) {
em.persist(student);
return student;
}
return em.merge(student);
}
@Override
public Optional<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 distinct s from OtusStudent s " +
"left join fetch s.emails", OtusStudent.class);
query.setHint(FETCH.getKey(), 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();
}
// Только для примера, в реальности JPQL лучше использовать только для массовых операций
@Override
public void updateNameById(long id, String name) {
Query query = em.createQuery("update OtusStudent s " +
"set s.name = :name " +
"where s.id = :id");
query.setParameter("name", name);
query.setParameter("id", id);
query.executeUpdate();
}
// Только для примера, в реальности JPQL лучше использовать только для массовых операций
@Override
public void deleteById(long id) {
Query query = em.createQuery("delete " +
"from OtusStudent s " +
"where s.id = :id");
query.setParameter("id", id);
query.executeUpdate();
}
}
@@ -0,0 +1,18 @@
package ru.otus.example.ormdemo.repositories;
import ru.otus.example.ormdemo.models.OtusStudent;
import java.util.List;
import java.util.Optional;
public interface OtusStudentRepository {
OtusStudent save(OtusStudent student);
Optional<OtusStudent> findById(long id);
List<OtusStudent> findAll();
List<OtusStudent> findByName(String name);
void updateNameById(long id, String name);
void deleteById(long id);
}
@@ -0,0 +1,18 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
sql:
init:
mode: always
jpa:
generate-ddl: false
hibernate:
ddl-auto: none
show-sql: true
logging:
level:
ROOT: ERROR
@@ -0,0 +1,31 @@
create table avatars(
id bigserial,
photo_url varchar(8000),
primary key (id)
);
create table courses(
id bigserial,
name varchar(255),
primary key (id)
);
create table otus_students(
id bigserial,
name varchar(255),
avatar_id bigint references avatars (id),
primary key (id)
);
create table emails(
id bigserial,
student_id bigint references otus_students(id) on delete cascade,
email varchar(255),
primary key (id)
);
create table student_courses(
student_id bigint references otus_students(id) on delete cascade,
course_id bigint references courses(id),
primary key (student_id, course_id)
);
@@ -0,0 +1,58 @@
package ru.otus.example.ormdemo.repositories;
import lombok.val;
import org.hibernate.SessionFactory;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.context.annotation.Import;
import ru.otus.example.ormdemo.models.OtusStudent;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Репозиторий на основе Jpa для работы со студентами ")
@DataJpaTest
@Import(JpaOtusStudentRepository.class)
class JpaOtusStudentRepositoryTest {
private static final int EXPECTED_NUMBER_OF_STUDENTS = 10;
private static final long FIRST_STUDENT_ID = 1L;
private static final int EXPECTED_QUERIES_COUNT = 2;
@Autowired
private JpaOtusStudentRepository repositoryJpa;
@Autowired
private TestEntityManager em;
@DisplayName(" должен загружать информацию о нужном студенте по его id")
@Test
void shouldFindExpectedStudentById() {
val optionalActualStudent = repositoryJpa.findById(FIRST_STUDENT_ID);
val expectedStudent = em.find(OtusStudent.class, FIRST_STUDENT_ID);
assertThat(optionalActualStudent).isPresent().get()
.usingRecursiveComparison().isEqualTo(expectedStudent);
}
@DisplayName("должен загружать список всех студентов с полной информацией о них")
@Test
void shouldReturnCorrectStudentsListWithAllInfo() {
SessionFactory sessionFactory = em.getEntityManager().getEntityManagerFactory()
.unwrap(SessionFactory.class);
sessionFactory.getStatistics().setStatisticsEnabled(true);
System.out.println("\n\n\n\n----------------------------------------------------------------------------------------------------------");
val students = repositoryJpa.findAll();
assertThat(students).isNotNull().hasSize(EXPECTED_NUMBER_OF_STUDENTS)
.allMatch(s -> !s.getName().equals(""))
.allMatch(s -> s.getCourses() != null && s.getCourses().size() > 0)
.allMatch(s -> s.getAvatar().getPhotoUrl() != null)
.allMatch(s -> s.getEmails() != null && s.getEmails().size() > 0);
System.out.println("----------------------------------------------------------------------------------------------------------\n\n\n\n");
assertThat(sessionFactory.getStatistics().getPrepareStatementCount()).isEqualTo(EXPECTED_QUERIES_COUNT);
}
}
@@ -0,0 +1,24 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
sql:
init:
mode: always
jpa:
generate-ddl: false
#generate-ddl: true
hibernate:
ddl-auto: none
#ddl-auto: create-drop
#show-sql: true
properties:
hibernate:
format_sql: true
logging:
level:
ROOT: ERROR
@@ -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,57 @@
<?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>3.4.1</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</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