MongoDB enhancement of Cascade and Event brought by spring-boot-up.
Entity relationship model:
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
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
// 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
// Only need to focus on setting relationships between documents
car.setGasTank(gasTank);
car.setEngine(engine);
engine.setMotor(motor);
car.setWheels(wheels);
carRepository.save(car);
<dependency>
<groupId>com.github.wnameless.spring.boot.up</groupId>
<artifactId>spring-boot-up-data-mongodb</artifactId>
<version>${newestVersion}</version>
<!-- Newest version shows in the maven-central badge above -->
</dependency>
This lib uses Semantic Versioning: {MAJOR.MINOR.PATCH}
.
However, the MAJOR version is always matched the Spring Boot MAJOR version.
! Maven dependency spring-boot-starter-data-mongodb is required
@EnableSpringBootUpMongo(allowAnnotationDrivenEvent = true) // Default value is false
@Configuration
public class MyConfiguration {}
@Repository
public interface CarRepository extends MongoRepository<Car, String>, MongoProjectionRepository<Car> {}
// With projection feature
Repository without projection feature
@Repository
public interface CarRepository extends MongoRepository<Car, String> {}
Name | Option | Description | Since |
---|---|---|---|
Cascade(@CascadeRef) | --- | Cascade feature for Spring Data MongoDB entities | v3.0.0 |
CascadeType.CREATE | Cascade CREATE | v3.0.0 | |
CascadeType.UPDATE | Cascade UPDATE | v3.0.0 | |
CascadeType.DELETE | Cascade DELETE | v3.0.0 | |
CascadeType.ALL | A combining of CREATE, UPDATE and DELETE | v3.0.0 | |
@ParentRef | --- | Automatically set the cascade event publisher object into @ParentRef annotated fields of the cascade event receiver |
v3.0.0 |
Default Usage | No additional configuration | v3.0.0 | |
Advanced Usage | Providing a field name of parent object | v3.0.0 | |
Annotation Driven Event | --- | Annotation Driven Event feature for MongoEvent |
v3.0.0 |
No arguments | Annotated methods with no arguments | v3.0.0 | |
SourceAndDocument | Annotated methods with single SourceAndDocument argument |
v3.0.0 | |
Projection | --- | Projection feature for Spring Data MongoDB entities | v3.0.0 |
Dot notation | String path with dot operator(.) | v3.0.0 | |
Path | QueryDSL Path | v3.0.0 | |
Projection Class | Java Class | v3.0.0 | |
Custom Conversions | --- | A collection of MongoCustomConversions | v3.0.0 |
JavaTime | MongoCustomConversions for Java 8 Date/Time | v3.0.0 |
🔝 Cascade(@CascadeRef)
+ @CascadeRef must annotate alongside @DBRef
Entity classes:
Car
@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<Wheel> wheels = new ArrayList<>();
@CascadeRef({CascadeType.UPDATE, CascadeType.DELETE})
@DBRef
GasTank subGasTank;
}
GasTank
@EqualsAndHashCode(of = "id")
@Data
@Document
public class GasTank {
@Id
String id;
@ParentRef
@DBRef
Car car;
double capacity = 100;
}
Engine
@EqualsAndHashCode(of = "id")
@Data
@Document
public class Engine {
@Id
String id;
@ParentRef
@DBRef
Car car;
double horsePower = 500;
@CascadeRef
@DBRef
Motor motor;
}
Motor
@EqualsAndHashCode(of = "id")
@Data
@Document
public class Motor {
@Id
String id;
@ParentRef
@DBRef
Engine engine;
@ParentRef("car")
@DBRef
Car car;
double rpm = 60000;
}
Wheel
@EqualsAndHashCode(of = "id")
@Data
@Document
public class Wheel {
@Id
String id;
@ParentRef
@DBRef
Car car;
String tireBrand = "MAXXIS";
}
JUnit BeaforeEach
mongoTemplate.getDb().drop(); // reset DB before each test
car.setGasTank(gasTank);
car.setEngine(engine);
engine.setMotor(motor);
car.setWheels(Arrays.asList(frontRightWheel, frontLeftWheel, rareRightWheel, rareLeftWheel));
carRepository.save(car);
🔝 CascadeType.CREATE
// JUnit
assertEquals(1, carRepository.count());
assertEquals(1, gasTankRepository.count());
assertEquals(1, engineRepository.count());
assertEquals(1, motorRepository.count());
assertEquals(4, wheelRepository.count());
🔝 CascadeType.UPDATE
// 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.
@@ Once @DBRef has been established, CascadeType.UPDATE won't change anything in @DBRef's nature @@
🔝 CascadeType.DELETE
// JUnit
carRepository.deleteAll();
assertEquals(0, carRepository.count());
assertEquals(1, engineRepository.count());
assertEquals(1, motorRepository.count());
assertEquals(1, gasTankRepository.count());
assertEquals(4, wheelRepository.count());
- Cascade is NOT working on bulk operations(ex: CrudRepository#deleteAll)
// 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());
+ Using CrudRepository#deleteAll(Iterable) instead of CrudRepository#deleteAll can perform cascade normally in most circumstances
🔝 @ParentRef
🔝 Default Usage
Car is treated as a parent of GasTank, because it is an event publisher to GasTank.
Car
@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<Wheel> 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
@EqualsAndHashCode(of = "id")
@Data
@Document
public class GasTank {
@Id
String id;
@ParentRef
@DBRef
Car car;
double capacity = 100;
}
🔝 Advanced Usage
Engine is treated as a parent of Motor, because it is an event publisher to Motor.
Engine
@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
@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
// Default usage
assertSame(car, gasTank.getCar());
// Advanced usage
assertSame(car, engine.getCar());
assertSame(engine, motor.getEngine());
assertSame(car, motor.getCar());
🔝 Annotation Driven Event
6 types of annotation driven events are supported:
- BeforeConvertToMongo
- BeforeSaveToMongo
- AfterSaveToMongo
- AfterConvertFromMongo
- BeforeDeleteFromMongo
- AfterDeleteFromMongo
All annotated methods will be triggered in corresponding MongoDB lifecycle events.
Annotated methods can accept only empty or single SourceAndDocument
as argument.
SourceAndDocument
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> T getSource(Class<T> type) {
return (T) source;
}
// #hashCode, #equals, #toString
}
SourceAndDocument
stores both event source object and event BSON Document at that point.
- Annotation Driven Event won't be triggered under Mongo bulk operations
🔝 No arguments
@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");
}
}
🔝 SourceAndDocument
@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);
}
}
🔝 Projection
Entity classes:
@EqualsAndHashCode(of = "id")
@Data
@Document
public class ComplexModel {
@Id
String id;
String str;
Integer i;
Double d;
Boolean b;
NestedModel nested;
}
@Data
public class NestedModel {
Float f;
Short s;
}
@Data
public class ProjectModel {
String str;
}
Init:
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);
🔝 Approach 1: Dot notation
var projected = complexModelRepository.findProjectedBy("str");
// Use dot operator(.) to represent nested projection object
var nestedProjected = complexModelRepository.findProjectedBy("nested.f");
Result
// 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());
🔝 Approach 2: QueryDSL Path
// QueryDSL PathBuilder
PathBuilder<Car> entityPath = new PathBuilder<>(ComplexModel.class, "entity");
var projected = carRepository.findProjectedBy(entityPath.getString("str"));
Result
// JUnit
assertEquals("str", projected.getStr());
assertNull(projected.getI());
assertNull(projected.getD());
assertNull(projected.getB());
assertNull(projected.getNested());
🔝 Approach 3: Java Class
// By projection Class
var projected = carRepository.findProjectedBy(ProjectModel.class);
Result
// JUnit
assertEquals("str", projected.getStr());
assertNull(projected.getI());
assertNull(projected.getD());
assertNull(projected.getB());
assertNull(projected.getNested());
🔝 Custom Conversions
🔝 JavaTime
MongoDB doesn't natively support Java 8 Date/Time(Ex: LocalDateTime
), so here is a convenient solution.
@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.
Note | Since |
---|---|
Java 17 required. | v3.0.0 |
Spring Boot 3.0.0+ required. | v3.0.0 |