https://github.com/wnameless/spring-boot-up-data-mongodb
MongoDB enhancement of Cascade and Event brought by spring-boot-up
https://github.com/wnameless/spring-boot-up-data-mongodb
java spring-boot-data-mongodb
Last synced: 3 months ago
JSON representation
MongoDB enhancement of Cascade and Event brought by spring-boot-up
- Host: GitHub
- URL: https://github.com/wnameless/spring-boot-up-data-mongodb
- Owner: wnameless
- License: apache-2.0
- Created: 2023-08-29T04:51:27.000Z (almost 2 years ago)
- Default Branch: main
- Last Pushed: 2023-09-16T05:47:40.000Z (almost 2 years ago)
- Last Synced: 2025-01-21T06:11:48.991Z (5 months ago)
- Topics: java, spring-boot-data-mongodb
- Language: Java
- Homepage:
- Size: 80.1 KB
- Stars: 3
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
[](https://maven-badges.herokuapp.com/maven-central/com.github.wnameless.spring.boot.up/spring-boot-up-data-mongodb)
[](https://codecov.io/gh/wnameless/spring-boot-up-data-mongodb)spring-boot-up-data-mongodb
=============
MongoDB enhancement of Cascade and Event brought by spring-boot-up.## _Goal_ - introducing `Cascade` and more features into Spring MongoDB
## _Purpose_ - reducing boilerplate codes followed by Spring `@DBRef`($ref)
Entity relationship model:
```mermaid
graph TD;
Car-- "$ref: gasTank" -->GasTank;
GasTank-. "$ref: car" .->Car;
Car-- "$ref: engine" -->Engine;
Engine-. "$ref: car" .->Car;
Engine-->Motor;
Motor-. "$ref: engine" .->Engine;
Motor-. "$ref: car" .->Car;
Car-- "$ref: wheels" -->Wheel;
Wheel-. "$ref: car" .->Car;
Car-- "$ref: subGasTank" -->SubGasTank
SubGasTank-. "$ref: car" .->Car;
```Entity Initialization
```java
var car = new Car();
var gasTank = new GasTank();
var engine = new Engine();
var motor = new Motor();
var frontRightWheel = new Wheel();
var frontLeftWheel = new Wheel();
var rareRightWheel = new Wheel();
var rareLeftWheel = new Wheel();
```Boilerplate codes before spring-boot-up-data-mongodb was introduced
```java
// Must save all documents before assigning @DBRef fields// Create Car
carRepository.save(car);// Create GasTank with Car ref
gasTank.set(car);
gasTankRepository.save(gasTank);// Create Engine with Car ref
engine.setCar(car);
engineRepository.save(engine);// Create Motor with Engine and Car ref
motor.setEngine(engine);
motor.setCar(car);
motorRepository.save(motor);// Update Engine with Motor ref
engine.setMotor(motor);
engineRepository.save(engine);// Create Wheel(s) with Car ref
frontRightWheel.setCar(car);
frontLeftWheel.setCar(car);
rareRightWheel.setCar(car);
rareLeftWheel.setCar(car);
wheelRepository.save(wheels);// Update Car with GasTank, Engine and Wheel(s) ref
car.setGasTank(gasTank);
car.setEngine(engine);
car.setWheels(Arrays.asList(frontRightWheel, frontLeftWheel, rareRightWheel, rareLeftWheel));
carRepository.save(car);
```Compact codes after utilizing spring-boot-up-data-mongodb
```java
// Only need to focus on setting relationships between documents
car.setGasTank(gasTank);
car.setEngine(engine);
engine.setMotor(motor);
car.setWheels(wheels);carRepository.save(car);
```# Maven Repo
```xmlcom.github.wnameless.spring.boot.up
spring-boot-up-data-mongodb
${newestVersion}
```
This lib uses Semantic Versioning: `{MAJOR.MINOR.PATCH}`.
However, the MAJOR version is always matched the Spring Boot MAJOR version.
```diff
! Maven dependency spring-boot-starter-data-mongodb is required
```# Quick Start
```java
@EnableSpringBootUpMongo(allowAnnotationDrivenEvent = true) // Default value is false
@Configuration
public class MyConfiguration {}
``````java
@Repository
public interface CarRepository extends MongoRepository, MongoProjectionRepository {}
// With projection feature
```Repository without projection feature
```java
@Repository
public interface CarRepository extends MongoRepository {}
```# Feature List
| Name | Option | Description | Since |
| --- | --- | --- | --- |
| [Cascade(@CascadeRef)](#3.0.0-1) | --- | Cascade feature for Spring Data MongoDB entities | v3.0.0 |
| | [CascadeType.CREATE](#3.0.0-1.1) | Cascade CREATE | v3.0.0 |
| | [CascadeType.UPDATE](#3.0.0-1.2) | Cascade UPDATE | v3.0.0 |
| | [CascadeType.DELETE](#3.0.0-1.3) | Cascade DELETE | v3.0.0 |
| | CascadeType.ALL | A combining of CREATE, UPDATE and DELETE | v3.0.0 |
| [@ParentRef](#3.0.0-2) | --- | Automatically set the cascade event publisher object into `@ParentRef` annotated fields of the cascade event receiver | v3.0.0 |
| | [Default Usage](#3.0.0-2.1) | No additional configuration | v3.0.0 |
| | [Advanced Usage](#3.0.0-2.2) | Providing a field name of parent object | v3.0.0 |
| [Annotation Driven Event](#3.0.0-3) | --- | Annotation Driven Event feature for `MongoEvent` | v3.0.0 |
| | [No arguments](#3.0.0-3.1) | Annotated methods with no arguments | v3.0.0 |
| | [SourceAndDocument](#3.0.0-3.2) | Annotated methods with single `SourceAndDocument` argument | v3.0.0 |
| [Projection](#3.0.0-4) | --- | Projection feature for Spring Data MongoDB entities | v3.0.0 |
| | [Dot notation](#3.0.0-4.1) | String path with dot operator(.) | v3.0.0 |
| | [Path](#3.0.0-4.2) | QueryDSL Path | v3.0.0 |
| | [Projection Class](#3.0.0-4.3) | Java Class | v3.0.0 |
| [Custom Conversions](#3.0.0-5) | --- | A collection of MongoCustomConversions | v3.0.0 |
| | [JavaTime](#3.0.0-5.1) | MongoCustomConversions for Java 8 Date/Time | v3.0.0 |### [:top:](#top) Cascade(@CascadeRef)
```diff
+ @CascadeRef must annotate alongside @DBRef
```
Entity classes:Car
```java
@EqualsAndHashCode(of = "id")
@Data
@Document
public class Car {
@Id
String id;@CascadeRef({CascadeType.CREATE, CascadeType.DELETE})
@DBRef
Engine engine;@CascadeRef(CascadeType.CREATE)
@DBRef
GasTank gasTank;@CascadeRef // Equivalent to @CascadeRef(CascadeType.ALL)
@DBRef
List wheels = new ArrayList<>();@CascadeRef({CascadeType.UPDATE, CascadeType.DELETE})
@DBRef
GasTank subGasTank;
}
```GasTank
```java
@EqualsAndHashCode(of = "id")
@Data
@Document
public class GasTank {
@Id
String id;@ParentRef
@DBRef
Car car;double capacity = 100;
}
```Engine
```java
@EqualsAndHashCode(of = "id")
@Data
@Document
public class Engine {
@Id
String id;@ParentRef
@DBRef
Car car;double horsePower = 500;
@CascadeRef
@DBRef
Motor motor;
}
```Motor
```java
@EqualsAndHashCode(of = "id")
@Data
@Document
public class Motor {
@Id
String id;@ParentRef
@DBRef
Engine engine;@ParentRef("car")
@DBRef
Car car;double rpm = 60000;
}
```Wheel
```java
@EqualsAndHashCode(of = "id")
@Data
@Document
public class Wheel {
@Id
String id;@ParentRef
@DBRef
Car car;String tireBrand = "MAXXIS";
}
```---
JUnit `BeaforeEach`
```java
mongoTemplate.getDb().drop(); // reset DB before each testcar.setGasTank(gasTank);
car.setEngine(engine);
engine.setMotor(motor);
car.setWheels(Arrays.asList(frontRightWheel, frontLeftWheel, rareRightWheel, rareLeftWheel));
carRepository.save(car);
```#### [:top:](#top) CascadeType.CREATE
```java
// JUnit
assertEquals(1, carRepository.count());
assertEquals(1, gasTankRepository.count());
assertEquals(1, engineRepository.count());
assertEquals(1, motorRepository.count());
assertEquals(4, wheelRepository.count());
```#### [:top:](#top) CascadeType.UPDATE
```java
// JUnit
car = new Car();
var subGasTank = new GasTank();
car.setSubGasTank(subGasTank);
// Because this car object hasn't been saved, so the CascadeType.UPDATE about the subGasTank object won't be performed
assertThrows(RuntimeException.class, () -> {
carRepository.save(car);
});car = new Car();
carRepository.save(car);
var subGasTank = new GasTank();
car.setSubGasTank(subGasTank);
carRepository.save(car);
// Because this car object has been saved, so the CascadeType.UPDATE is performed
assertSame(subGasTank, car.getSubGasTank());
```
The main diffrence between `CascadeType.UPDATE` and plain `@DBREf` is that
`CascadeType.UPDATE` allows unsaved documents to be set in `@DBREf` fields but plain `@DBREf` won't.
```diff
@@ Once @DBRef has been established, CascadeType.UPDATE won't change anything in @DBRef's nature @@
```#### [:top:](#top) CascadeType.DELETE
```java
// JUnit
carRepository.deleteAll();
assertEquals(0, carRepository.count());
assertEquals(1, engineRepository.count());
assertEquals(1, motorRepository.count());
assertEquals(1, gasTankRepository.count());
assertEquals(4, wheelRepository.count());
```
```diff
- Cascade is NOT working on bulk operations(ex: CrudRepository#deleteAll)
```
```java
// JUnit
carRepository.deleteAll(carRepository.findAll());
assertEquals(0, carRepository.count());
assertEquals(0, engineRepository.count());
assertEquals(0, motorRepository.count());
assertEquals(1, gasTankRepository.count());
// gasTank won't be deleted because it's only annotated with @CascadeRef(CascadeType.CREATE)
assertEquals(0, wheelRepository.count());
```
```diff
+ Using CrudRepository#deleteAll(Iterable) instead of CrudRepository#deleteAll can perform cascade normally in most circumstances
```### [:top:](#top) @ParentRef
#### [:top:](#top) Default Usage
Car is treated as a _parent_ of GasTank, because it is an event publisher to GasTank.Car
```java
@EqualsAndHashCode(of = "id")
@Data
@Document
public class Car {
@Id
String id;@CascadeRef({CascadeType.CREATE, CascadeType.DELETE})
@DBRef
Engine engine;@CascadeRef(CascadeType.CREATE)
@DBRef
GasTank gasTank;@CascadeRef // Equivalent to @CascadeRef(CascadeType.ALL)
@DBRef
List wheels = new ArrayList<>();@CascadeRef({CascadeType.UPDATE, CascadeType.DELETE})
@DBRef
GasTank subGasTank;
}
```Therefore, the `@ParentRef` annotated field of a GasTank will be set by Car automatically.
GasTank
```java
@EqualsAndHashCode(of = "id")
@Data
@Document
public class GasTank {
@Id
String id;@ParentRef
@DBRef
Car car;double capacity = 100;
}
```#### [:top:](#top) Advanced Usage
Engine is treated as a _parent_ of Motor, because it is an event publisher to Motor.Engine
```java
@EqualsAndHashCode(of = "id")
@Data
@Document
public class Engine {
@Id
String id;@ParentRef
@DBRef
Car car;double horsePower = 500;
@CascadeRef
@DBRef
Motor motor;
}
```Therefore, the `@ParentRef("car")` field of Motor is set by the _car_ field of Engine automatically.
Motor
```java
@EqualsAndHashCode(of = "id")
@Data
@Document
public class Motor {
@Id
String id;@ParentRef
@DBRef
Engine engine;@ParentRef("car")
@DBRef
Car car;double rpm = 60000;
}
```Test `@ParentRef`
```java
// Default usage
assertSame(car, gasTank.getCar());
// Advanced usage
assertSame(car, engine.getCar());
assertSame(engine, motor.getEngine());
assertSame(car, motor.getCar());
```### [:top:](#top) Annotation Driven Event
6 types of annotation driven events are supported:* BeforeConvertToMongo
* BeforeSaveToMongo
* AfterSaveToMongo
* AfterConvertFromMongo
* BeforeDeleteFromMongo
* AfterDeleteFromMongoAll annotated methods will be triggered in corresponding MongoDB lifecycle events.
Annotated methods can accept only empty or single `SourceAndDocument` as argument.
SourceAndDocument
```java
public final class SourceAndDocument {private final Object source;
private final Document document;public SourceAndDocument(Object source, Document document) {
this.source = source;
this.document = document;
}public Object getSource() {
return source;
}public Document getDocument() {
return document;
}public boolean hasSource(Class> type) {
return type.isAssignableFrom(source.getClass());
}@SuppressWarnings("unchecked")
public T getSource(Class type) {
return (T) source;
}// #hashCode, #equals, #toString
}
````SourceAndDocument` stores both event source object and event BSON Document at that point.
```diff
- Annotation Driven Event won't be triggered under Mongo bulk operations
```#### [:top:](#top) No arguments
```java
@Document
public class Car {
@Id
String id;@BeforeConvertToMongo
void beforeConvert() {
System.out.println("beforeConvertToMongo");
}@BeforeSaveToMongo
void beforeSave() {
System.out.println("beforeSaveToMongo");
}@AfterSaveToMongo
void afterSave() {
System.out.println("afterSaveToMongo");
}@AfterConvertFromMongo
void afterConvert() {
System.out.println("afterConvertFromMongo");
}@BeforeDeleteFromMongo
void beforeDeleteFromMongo() {
System.out.println("beforeDeleteFromMongo");
}@AfterDeleteFromMongo
void afterDeleteFromMongo() {
System.out.println("afterDeleteFromMongo");
}
}
```#### [:top:](#top) SourceAndDocument
```java
@Document
public class Car {
@Id
String id;@BeforeConvertToMongo
void beforeConvertArg(SourceAndDocument sad) {
var car = sad.getSource(Car.class);
}@BeforeSaveToMongo
void beforeSaveArg(SourceAndDocument sad) {
var car = sad.getSource(Car.class);
}@AfterSaveToMongo
void afterSaveArg(SourceAndDocument sad) {
var car = sad.getSource(Car.class);
}@AfterConvertFromMongo
void afterConvertArg(SourceAndDocument sad) {
var car = sad.getSource(Car.class);
}@BeforeDeleteFromMongo
void beforeDeleteFromMongoArg(SourceAndDocument sad) {
var car = sad.getSource(Car.class);
}@AfterDeleteFromMongo
void afterDeleteFromMongoArg(SourceAndDocument sad) {
var car = sad.getSource(Car.class);
}
}
```### [:top:](#top) Projection
Entity classes:
```java
@EqualsAndHashCode(of = "id")
@Data
@Document
public class ComplexModel {
@Id
String id;String str;
Integer i;
Double d;
Boolean b;
NestedModel nested;
}
```
```java
@Data
public class NestedModel {
Float f;Short s;
}
```
```java
@Data
public class ProjectModel {
String str;
}
```Init:
```java
var model = new ComplexModel();
model.setStr("str");
model.setI(123);
model.setD(45.6);
model.setB(true);
var nested = new NestedModel();
nested.setF(7.8f);
nested.setS((short) 9);
model.setNested(nested);
complexModelRepository.save(model);
```### Projection can be performed in 3 ways:
#### [:top:](#top) Approach 1: Dot notation
```java
var projected = complexModelRepository.findProjectedBy("str");
// Use dot operator(.) to represent nested projection object
var nestedProjected = complexModelRepository.findProjectedBy("nested.f");
```Result
```java
// JUnit
assertEquals("str", projected.getStr());
assertNull(projected.getI());
assertNull(projected.getD());
assertNull(projected.getB());
assertNull(projected.getNested());assertNull(nestedProjected.getStr());
assertNull(nestedProjected.getI());
assertNull(nestedProjected.getD());
assertNull(nestedProjected.getB());
assertEquals(7.8f, nestedProjected.getNested().getF());
```#### [:top:](#top) Approach 2: QueryDSL Path
```java
// QueryDSL PathBuilder
PathBuilder entityPath = new PathBuilder<>(ComplexModel.class, "entity");
var projected = carRepository.findProjectedBy(entityPath.getString("str"));
```Result
```java
// JUnit
assertEquals("str", projected.getStr());
assertNull(projected.getI());
assertNull(projected.getD());
assertNull(projected.getB());
assertNull(projected.getNested());
```#### [:top:](#top) Approach 3: Java Class
```java
// By projection Class
var projected = carRepository.findProjectedBy(ProjectModel.class);
```Result
```java
// JUnit
assertEquals("str", projected.getStr());
assertNull(projected.getI());
assertNull(projected.getD());
assertNull(projected.getB());
assertNull(projected.getNested());
```### [:top:](#top) Custom Conversions
#### [:top:](#top) JavaTime
MongoDB doesn't natively support Java 8 Date/Time(Ex: `LocalDateTime`), so here is a convenient solution.
```java
@Configuration
public class MongoConfig extends AbstractMongoClientConfiguration {
@Override
public MongoCustomConversions customConversions() {
// MongoConverters.javaTimeConversions() includes all types of Java 8 Date/Time converters
return MongoConverters.javaTimeConversions();
}
}
```
All Java 8 Date/Time types(excluding DayOfWeek and Month Enums) are converted to `String`, and vice versa.## MISC
| Note| Since |
| --- | --- |
| Java 17 required. | v3.0.0 |
| Spring Boot 3.0.0+ required. | v3.0.0 |