wnameless / spring-boot-up-data-mongodb

MongoDB enhancement of Cascade and Event brought by spring-boot-up

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Maven Central codecov

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:

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;
Loading
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);

Maven Repo

<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

Quick Start

@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> {}

Feature List

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);

Projection can be performed in 3 ways:

πŸ” 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.

MISC

Note Since
Java 17 required. v3.0.0
Spring Boot 3.0.0+ required. v3.0.0

About

MongoDB enhancement of Cascade and Event brought by spring-boot-up

License:Apache License 2.0


Languages

Language:Java 100.0%