mongo-db-demo added

This commit is contained in:
stvort
2019-04-01 23:05:36 +04:00
parent 6620521317
commit cd6f9757df
28 changed files with 675 additions and 1 deletions
+2 -1
View File
@@ -4,4 +4,5 @@
* *unit-testing-plain-spring* - пример тестирования в проектах на чистом Spring
* *unit-testing-spring-boot* - то же самое, только на Spring Boot
* *hibernate-fetch-mode-demo* - демонстрация настроек Hibernate, в частности для решения проблемы N+1
* *hibernate-fetch-mode-demo* - демонстрация настроек Hibernate, в частности для решения проблемы N+1
* *mongo-db-demo* - демонстрация подходов к хранению вложенных сущностенй в MongoDB, работы с MongoEventListener, агрегациями и инструментом миграций Mongock
+29
View File
@@ -0,0 +1,29 @@
HELP.md
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
### VS Code ###
.vscode/
+7
View File
@@ -0,0 +1,7 @@
## Пример работы с MongoDB
В примере демонстрируется:
* *два подхода к хранению вложенных сущностей*
* *инициализация базы данными с помощью инструмента миграций Mongock*
* *использование AbstractMongoEventListener для выполнения каскадных операций*
* *работа с массивами с помощью агрегаций и Criteria api*
+123
View File
@@ -0,0 +1,123 @@
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>ru.otus.example</groupId>
<artifactId>mongo-db-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mongo-db-demo</name>
<description>Demo project for MongoDB</description>
<properties>
<java.version>1.8</java.version>
<maven.compiler.sourcre>1.8.</maven.compiler.sourcre>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.cloudyrock.mongock</groupId>
<artifactId>mongock-spring</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.assertj</groupId>
<artifactId>org.assertj.core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--Тестирование-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.21.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>2.23.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.12.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>
@@ -0,0 +1,15 @@
package ru.otus.example.mongoDbDemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties
public class MongoDbDemoApplication {
public static void main(String[] args) {
SpringApplication.run(MongoDbDemoApplication.class, args);
}
}
@@ -0,0 +1,42 @@
package ru.otus.example.mongoDbDemo.changelogs;
import com.github.cloudyrock.mongock.ChangeLog;
import com.github.cloudyrock.mongock.ChangeSet;
import com.mongodb.client.MongoDatabase;
import lombok.val;
import org.springframework.data.mongodb.core.MongoTemplate;
import ru.otus.example.mongoDbDemo.model.Knowledge;
import ru.otus.example.mongoDbDemo.model.Student;
import ru.otus.example.mongoDbDemo.model.Teacher;
@ChangeLog(order = "001")
public class InitMongoDBDataChangeLog {
private Knowledge springDataKnowledge;
private Knowledge mongockKnowledge;
private Knowledge aggregationApiKnowledge;
@ChangeSet(order = "000", id = "dropDB", author = "stvort", runAlways = true)
public void dropDB(MongoDatabase database){
database.drop();
}
@ChangeSet(order = "001", id = "initKnowledges", author = "stvort", runAlways = true)
public void initKnowledges(MongoTemplate template){
springDataKnowledge = template.save(new Knowledge("Spring Data"));
mongockKnowledge = template.save(new Knowledge("Mongock"));
aggregationApiKnowledge = template.save(new Knowledge("AggregationApi"));
}
@ChangeSet(order = "002", id = "initStudents", author = "stvort", runAlways = true)
public void initStudents(MongoTemplate template){
val student = new Student("Student #1", springDataKnowledge, mongockKnowledge);
template.save(student);
}
@ChangeSet(order = "003", id = "Teacher", author = "stvort", runAlways = true)
public void initTeachers(MongoTemplate template){
val teacher = new Teacher("Teacher #1", springDataKnowledge, mongockKnowledge, aggregationApiKnowledge);
template.save(teacher);
}
}
@@ -0,0 +1,20 @@
package ru.otus.example.mongoDbDemo.config;
import com.github.cloudyrock.mongock.Mongock;
import com.github.cloudyrock.mongock.SpringMongockBuilder;
import com.mongodb.MongoClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ApplicationConfig {
private static final String CHANGELOGS_PACKAGE = "ru.otus.example.mongoDbDemo.changelogs";
@Bean
public Mongock mongock(MongoProps mongoProps, MongoClient mongoClient) {
return new SpringMongockBuilder(mongoClient, mongoProps.getDatabase(), CHANGELOGS_PACKAGE)
.setLockQuickConfig()
.build();
}
}
@@ -0,0 +1,14 @@
package ru.otus.example.mongoDbDemo.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties("spring.data.mongodb")
public class MongoProps {
private int port;
private String database;
private String uri;
}
@@ -0,0 +1,24 @@
package ru.otus.example.mongoDbDemo.events;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;
import org.springframework.data.mongodb.core.mapping.event.AfterDeleteEvent;
import org.springframework.stereotype.Component;
import ru.otus.example.mongoDbDemo.model.Knowledge;
import ru.otus.example.mongoDbDemo.repositories.StudentRepository;
@Component
@RequiredArgsConstructor
public class MongoKnowledgeCascadeDeleteEventsListener extends AbstractMongoEventListener<Knowledge> {
private final StudentRepository studentRepository;
@Override
public void onAfterDelete(AfterDeleteEvent<Knowledge> event) {
super.onAfterDelete(event);
val source = event.getSource();
val id = source.get("_id").toString();
studentRepository.removeExperienceArrayElementsById(id);
}
}
@@ -0,0 +1,27 @@
package ru.otus.example.mongoDbDemo.events;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;
import org.springframework.stereotype.Component;
import ru.otus.example.mongoDbDemo.model.Student;
import ru.otus.example.mongoDbDemo.repositories.KnowledgeRepository;
import java.util.Objects;
@Component
@RequiredArgsConstructor
public class MongoStudentCascadeSaveEventsListener extends AbstractMongoEventListener<Student> {
private final KnowledgeRepository knowledgeRepository;
@Override
public void onBeforeConvert(BeforeConvertEvent<Student> event) {
super.onBeforeConvert(event);
val student = event.getSource();
if (student.getExperience() != null) {
student.getExperience().stream().filter(e -> Objects.isNull(e.getId())).forEach(knowledgeRepository::save);
}
}
}
@@ -0,0 +1,21 @@
package ru.otus.example.mongoDbDemo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document
public class Knowledge {
@Id
private String id;
private String name;
public Knowledge(String name) {
this.name = name;
}
}
@@ -0,0 +1,29 @@
package ru.otus.example.mongoDbDemo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Arrays;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document
public class Student {
@Id
private String id;
private String name;
@DBRef
private List<Knowledge> experience;
public Student(String name, Knowledge... experience) {
this.name = name;
this.experience = Arrays.asList(experience);
}
}
@@ -0,0 +1,28 @@
package ru.otus.example.mongoDbDemo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Arrays;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document
public class Teacher {
@Id
private String id;
private String name;
private List<Knowledge> experience;
public Teacher(String name, Knowledge... experience) {
this.name = name;
this.experience = Arrays.asList(experience);
}
}
@@ -0,0 +1,7 @@
package ru.otus.example.mongoDbDemo.repositories;
import org.springframework.data.mongodb.repository.MongoRepository;
import ru.otus.example.mongoDbDemo.model.Knowledge;
public interface KnowledgeRepository extends MongoRepository<Knowledge, String> {
}
@@ -0,0 +1,8 @@
package ru.otus.example.mongoDbDemo.repositories;
import org.springframework.data.mongodb.repository.MongoRepository;
import ru.otus.example.mongoDbDemo.model.Student;
public interface StudentRepository extends MongoRepository<Student, String>, StudentRepositoryCustom {
}
@@ -0,0 +1,6 @@
package ru.otus.example.mongoDbDemo.repositories;
public interface StudentRepositoryCustom {
long getExperienceArrayLengthByStudentId(String id);
void removeExperienceArrayElementsById(String id);
}
@@ -0,0 +1,42 @@
package ru.otus.example.mongoDbDemo.repositories;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import ru.otus.example.mongoDbDemo.model.Student;
import static org.springframework.data.mongodb.core.query.Criteria.where;
@RequiredArgsConstructor
public class StudentRepositoryCustomImpl implements StudentRepositoryCustom {
@Data
private class ArraySizeProjection {
private int size;
}
private final MongoTemplate mongoTemplate;
public long getExperienceArrayLengthByStudentId(String id) {
val aggregation = Aggregation.newAggregation(
Aggregation.match(where("id").is(id)),
Aggregation.project().andExclude("_id").and("experience").size().as("size"));
val arraySizeProjection = mongoTemplate.aggregate(aggregation, Student.class, ArraySizeProjection.class).getUniqueMappedResult();
return arraySizeProjection == null ? 0 : arraySizeProjection.getSize();
}
public void removeExperienceArrayElementsById(String id) {
val query = Query.query(Criteria.where("$id").is(new ObjectId(id)));
val update = new Update().pull("experience", query);
mongoTemplate.updateMulti(new Query(), update, Student.class);
}
}
@@ -0,0 +1,7 @@
package ru.otus.example.mongoDbDemo.repositories;
import org.springframework.data.mongodb.repository.MongoRepository;
import ru.otus.example.mongoDbDemo.model.Teacher;
public interface TeacherRepository extends MongoRepository<Teacher, String>, TeacherRepositoryCustom {
}
@@ -0,0 +1,9 @@
package ru.otus.example.mongoDbDemo.repositories;
import ru.otus.example.mongoDbDemo.model.Knowledge;
import java.util.List;
public interface TeacherRepositoryCustom {
List<Knowledge> getTeacherExperienceById(String teacherId);
}
@@ -0,0 +1,28 @@
package ru.otus.example.mongoDbDemo.repositories;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import ru.otus.example.mongoDbDemo.model.Knowledge;
import ru.otus.example.mongoDbDemo.model.Teacher;
import java.util.List;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
@RequiredArgsConstructor
public class TeacherRepositoryCustomImpl implements TeacherRepositoryCustom {
private final MongoTemplate mongoTemplate;
@Override
public List<Knowledge> getTeacherExperienceById(String teacherId) {
val aggregation = newAggregation(
match(Criteria.where("id").is(teacherId))
, unwind("experience")
, project().andExclude("_id").and("experience.id").as("_id").and("experience.name").as("name")
);
return mongoTemplate.aggregate(aggregation, Teacher.class, Knowledge.class).getMappedResults();
}
}
@@ -0,0 +1,6 @@
spring:
data:
mongodb:
uri: mongodb://localhost
port: 27017
database: test
@@ -0,0 +1,13 @@
package ru.otus.example.mongoDbDemo.repositories;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.annotation.DirtiesContext;
@DataMongoTest
@EnableConfigurationProperties
@ComponentScan({"ru.otus.example.mongoDbDemo.config", "ru.otus.example.mongoDbDemo.repositories"})
@DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD)
abstract class AbstractRepositoryTest {
}
@@ -0,0 +1,41 @@
package ru.otus.example.mongoDbDemo.repositories;
import lombok.val;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
@DisplayName("KnowledgeRepository при наличии listener-ов в контексте ")
@ComponentScan("ru.otus.example.mongoDbDemo.events")
class KnowledgeRepositoryWithListenerTest extends AbstractRepositoryTest {
@Autowired
private KnowledgeRepository knowledgeRepository;
@Autowired
private StudentRepository studentRepository;
@DisplayName("при удалении Knowledge должен удалить его из опыта студента")
@Test
void shouldRemoveKnowledgeFromStudentExperienceWhenRemovingKnowledge() {
// Загрузка студента и его пе
val students = studentRepository.findAll();
val student = students.get(0);
val experience = student.getExperience();
val firstKnowledge = experience.get(0);
knowledgeRepository.delete(firstKnowledge);
val expectedExperienceArrayLength = experience.size() - 1;
val actualExperienceArrayLength = studentRepository.getExperienceArrayLengthByStudentId(student.getId());
assertThat(actualExperienceArrayLength).isEqualTo(expectedExperienceArrayLength);
val actualStudentOptional = studentRepository.findById(student.getId());
assertThat(actualStudentOptional.get().getExperience().size()).isEqualTo(expectedExperienceArrayLength);
}
}
@@ -0,0 +1,39 @@
package ru.otus.example.mongoDbDemo.repositories;
import lombok.val;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
@DisplayName("KnowledgeRepository при отсутствии listener-ов в контексте ")
class KnowledgeRepositoryWithoutListenerTest extends AbstractRepositoryTest {
@Autowired
private KnowledgeRepository knowledgeRepository;
@Autowired
private StudentRepository studentRepository;
@DisplayName("при удалении Knowledge не должен удалять его из опыта студента")
@Test
void shouldLeaveKnowledgeInStudentExperienceWhenRemovingKnowledge() {
// Загрузка студента и его пе
val students = studentRepository.findAll();
val student = students.get(0);
val experience = student.getExperience();
val firstKnowledge = experience.get(0);
knowledgeRepository.delete(firstKnowledge);
val expectedExperienceArrayLength = experience.size();
val actualExperienceArrayLength = studentRepository.getExperienceArrayLengthByStudentId(student.getId());
assertThat(actualExperienceArrayLength).isEqualTo(expectedExperienceArrayLength);
val actualStudentOptional = studentRepository.findById(student.getId());
assertThat(actualStudentOptional.get().getExperience().size()).isNotEqualTo(expectedExperienceArrayLength);
}
}
@@ -0,0 +1,29 @@
package ru.otus.example.mongoDbDemo.repositories;
import lombok.val;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import ru.otus.example.mongoDbDemo.model.Knowledge;
import ru.otus.example.mongoDbDemo.model.Student;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("StudentRepository при наличии listener-ов в контексте ")
@ComponentScan("ru.otus.example.mongoDbDemo.events")
class StudentRepositoryWithListenersTest extends AbstractRepositoryTest {
@Autowired
private StudentRepository studentRepository;
@DisplayName("должен корректно сохранять студента с отсутствующими в БД знаниями")
@Test
void shouldCorrectSaveStudentWithNewKnowledge(){
val student = new Student("Student #2", new Knowledge("Knowledge #3"));
val actual = studentRepository.save(student);
assertThat(actual.getId()).isNotNull();
assertThat(actual.getName()).isEqualTo(student.getName());
}
}
@@ -0,0 +1,25 @@
package ru.otus.example.mongoDbDemo.repositories;
import lombok.val;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mapping.MappingException;
import ru.otus.example.mongoDbDemo.model.Knowledge;
import ru.otus.example.mongoDbDemo.model.Student;
import static org.assertj.core.api.Assertions.*;
@DisplayName("StudentRepository при отсутствии listener-ов в контексте ")
class StudentRepositoryWithoutListenerTest extends AbstractRepositoryTest {
@Autowired
private StudentRepository studentRepository;
@DisplayName("должен кидать MappingException во время сохранения студента с отсутствующими в БД знаниями")
@Test
void shouldThrowMappingExceptionWhenSaveStudentWithNewKnowledge(){
val student = new Student("Student #2", new Knowledge("Knowledge #3"));
assertThatThrownBy(() -> studentRepository.save(student)).isInstanceOf(MappingException.class);
}
}
@@ -0,0 +1,29 @@
package ru.otus.example.mongoDbDemo.repositories;
import lombok.val;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import ru.otus.example.mongoDbDemo.model.Knowledge;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("TeacherRepository должен ")
class TeacherRepositoryTest extends AbstractRepositoryTest {
@Autowired
private TeacherRepository teacherRepository;
@DisplayName(" возвращать корректный список знаний преподавателя")
@Test
void shouldReturnCorrectKnowledgeList(){
val teachers = teacherRepository.findAll();
val teacher = teachers.get(0);
val experience = teacher.getExperience();
assertThat(experience).isNotNull().hasSize(3);
val actualExperience = teacherRepository.getTeacherExperienceById(teacher.getId());
assertThat(actualExperience).containsExactlyInAnyOrder(experience.toArray(new Knowledge[experience.size()]));
}
}
@@ -0,0 +1,5 @@
spring:
data:
mongodb:
port: 0
database: test