diff --git a/README.md b/README.md index bb6ee5e..cb5a3cf 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,8 @@ public record City( > [!Note] > Notice that having the `countryId` field, which maps to the foreign key column, is required for the relationship to work properly. +If the annotation is `persist = true` and the field is `null` upon persistence, the annotation shall never delete the parent because it can still have other linked children. However, it will change the foreign key to `null` to unlink the children from the parent. + ### ManyToMany The `@ManyToMany` annotation lets you mark fields to have a many-to-many relationship. The default behavior of the annotation is to populate the field after mapping the entity object, create/update the associated entities, and link the relations on the join table. The annotation uses the join table transparently, meaning you **don't need** to create an entity type for the join table on your codebase. diff --git a/src/main/java/io/github/joselion/springr2dbcrelationships/processors/ManyToOneProcessor.java b/src/main/java/io/github/joselion/springr2dbcrelationships/processors/ManyToOneProcessor.java index 44147fa..14b9e06 100644 --- a/src/main/java/io/github/joselion/springr2dbcrelationships/processors/ManyToOneProcessor.java +++ b/src/main/java/io/github/joselion/springr2dbcrelationships/processors/ManyToOneProcessor.java @@ -103,6 +103,7 @@ public Mono persist(final ManyToOne annotation, final Field field) { final var savedId = this.idValueOf(saved); final var newEntity = Reflect.update(this.entity, field, saved); return Reflect.update(newEntity, foreignField, savedId); - }); + }) + .defaultIfEmpty(Reflect.update(this.entity, foreignField, null)); } } diff --git a/src/test/java/io/github/joselion/springr2dbcrelationships/processors/ManyToOneProcessorTest.java b/src/test/java/io/github/joselion/springr2dbcrelationships/processors/ManyToOneProcessorTest.java index f078a9b..01d8057 100644 --- a/src/test/java/io/github/joselion/springr2dbcrelationships/processors/ManyToOneProcessorTest.java +++ b/src/test/java/io/github/joselion/springr2dbcrelationships/processors/ManyToOneProcessorTest.java @@ -39,6 +39,8 @@ private final String chicago = "Chicago"; + private final String manhattan = "Manhattan"; + @Nested class populate { @Test void populates_the_field_with_the_parent_entity() { countryRepo.save(usa) @@ -66,7 +68,7 @@ } @Nested class persist { - @Nested class when_the_annotation_does_not_configure_persist { + @Nested class when_the_persist_option_is_false { @Test void does_not_persist_the_annotated_field_by_default() { countryRepo.save(usa) .map(saved -> saved.withName("USA")) @@ -90,25 +92,66 @@ } } - @Nested class when_the_annotation_sets_persist_to_true { - @Test void persists_the_annotated_field() { - final var manhattan = Town.of("Manhattan"); + @Nested class when_the_persist_option_is_true { + @Nested class and_the_parent_does_not_exist { + @Test void creates_the_parent_entity() { + Mono.just(usa) + .map(Town.of(manhattan)::withCountry) + .flatMap(townRepo::save) + .map(Town::id) + .flatMap(townRepo::findById) + .zipWhen(saved -> countryRepo.findById(saved.countryId())) + .as(TxStepVerifier::withRollback) + .assertNext(consumer((town, country) -> { + assertThat(town.countryId()).isEqualTo(country.id()); + assertThat(town.country().id()).isEqualTo(country.id()); + assertThat(country.towns()) + .extracting(Town::name) + .containsExactly(manhattan); + })) + .verifyComplete(); + } + } - Mono.just(usa) - .map(manhattan::withCountry) - .flatMap(townRepo::save) - .map(Town::id) - .flatMap(townRepo::findById) - .zipWhen(saved -> countryRepo.findById(saved.countryId())) - .as(TxStepVerifier::withRollback) - .assertNext(consumer((town, country) -> { - assertThat(town.countryId()).isEqualTo(country.id()); - assertThat(town.country().id()).isEqualTo(country.id()); - assertThat(country.towns()) - .extracting(Town::name) - .containsExactly(manhattan.name()); - })) - .verifyComplete(); + @Nested class and_the_parent_does_already_exists { + @Test void updates_the_parent_entity() { + Mono.just(usa) + .map(Town.of(manhattan)::withCountry) + .flatMap(townRepo::save) + .map(town -> town.withCountryBy(country -> country.withName("USA"))) + .flatMap(townRepo::save) + .map(Town::id) + .flatMap(townRepo::findById) + .zipWhen(saved -> countryRepo.findById(saved.countryId())) + .as(TxStepVerifier::withRollback) + .assertNext(consumer((town, country) -> { + assertThat(town.countryId()).isEqualTo(country.id()); + assertThat(town.country().id()).isEqualTo(country.id()); + assertThat(country.name()).isEqualTo("USA"); + assertThat(country.towns()) + .extracting(Town::name) + .containsExactly(manhattan); + })) + .verifyComplete(); + } + } + + @Nested class and_the_parent_is_null { + @Test void unlinks_the_entity_from_the_parent() { + Mono.just(usa) + .map(Town.of(manhattan)::withCountry) + .flatMap(townRepo::save) + .map(town -> town.withCountry(null)) + .flatMap(townRepo::save) + .map(Town::id) + .flatMap(townRepo::findById) + .as(TxStepVerifier::withRollback) + .assertNext(town -> { + assertThat(town.country()).isNull(); + assertThat(town.countryId()).isNull(); + }) + .verifyComplete(); + } } } }