Skip to content

Commit

Permalink
Improve cycles with one-to-many relationships
Browse files Browse the repository at this point in the history
  • Loading branch information
JoseLion committed Mar 5, 2024
1 parent 6ed5c21 commit 4859270
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import java.lang.reflect.Field;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;

import org.eclipse.jdt.annotation.Nullable;
import org.springframework.context.ApplicationContext;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.sql.SqlIdentifier;
Expand All @@ -20,6 +22,7 @@
import io.github.joselion.springr2dbcrelationships.helpers.Reflect;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;

/**
* The {@link ManyToOne} annotation processor.
Expand All @@ -43,6 +46,7 @@ public Mono<Object> populate(final ManyToOne annotation, final Field field) {
final var fieldType = this.domainFor(fieldProjection);
final var byTable = this.tableNameOf(fieldType).concat("_id");
final var byField = Commons.toSnakeCase(field.getName()).concat("_id");
final var parentId = this.idColumnOf(fieldType);
final var foreignField = Optional.of(annotation)
.map(ManyToOne::foreignKey)
.map(Commons::toCamelCase)
Expand All @@ -57,33 +61,17 @@ public Mono<Object> populate(final ManyToOne annotation, final Field field) {
.formatted(entityType.getSimpleName(), byTable, byField);
return RelationshipException.of(message);
});
final var parentId = this.idColumnOf(fieldType);

return Mono.just(this.entity)
.mapNotNull(Reflect.getter(foreignField))
.flatMap(fkValue ->
Mono.deferContextual(ctx -> {
final var store = ctx.<List<Object>>getOrDefault(OneToMany.class, List.of());

return Flux.fromIterable(store)
.filter(fkValue::equals)
.collectList()
.filter(List::isEmpty)
.map(x -> fkValue);
})
)
.flatMap(this::breakingCycles)
.flatMap(fkValue ->
this.template
.select(fieldType)
.as(fieldProjection)
.matching(query(where(parentId).is(fkValue)))
.one()
.contextWrite(ctx -> {
final var store = ctx.<List<Object>>getOrDefault(ManyToOne.class, List.of());
final var next = Stream.concat(store.stream(), Stream.of(fkValue)).toList();

return ctx.put(ManyToOne.class, next);
})
.contextWrite(this.storeWith(fkValue))
);
}

Expand All @@ -94,16 +82,57 @@ public Mono<Object> persist(final ManyToOne annotation, final Field field) {
.map(ManyToOne::foreignKey)
.filter(not(String::isBlank))
.orElseGet(() -> this.tableNameOf(fieldType).concat("_id"));
final var foreignField = Commons.toCamelCase(foreignKey);
final var fkFieldName = Commons.toCamelCase(foreignKey);
final var fkValue = Reflect.getter(this.entity, fkFieldName);

return Mono.just(this.entity)
.mapNotNull(Reflect.getter(field))
.flatMap(this.breakingCyclesWith(fkValue))
.flatMap(this::save)
.map(saved -> {
final var savedId = this.idValueOf(saved);
final var newEntity = Reflect.update(this.entity, field, saved);
return Reflect.update(newEntity, foreignField, savedId);
return Reflect.update(newEntity, fkFieldName, savedId);
})
.defaultIfEmpty(Reflect.update(this.entity, foreignField, null));
.switchIfEmpty(
Mono.just(this.entity)
.flatMap(this.breakingCyclesWith(fkValue))
.map(Reflect.update(fkFieldName, null))
.map(Reflect.update(field, null))
)
.contextWrite(this.storeWith(fkValue));
}

private <S, T> Function<S, Mono<S>> breakingCyclesWith(final @Nullable T fkValue) {
return value -> Mono.deferContextual(ctx -> {
if (fkValue != null) {
final var store = ctx.<List<Object>>getOrDefault(OneToMany.class, List.of());

return Flux.fromIterable(store)
.filter(fkValue::equals)
.collectList()
.filter(List::isEmpty)
.map(x -> value);
}

return Mono.just(value);
});
}

private <T> Mono<T> breakingCycles(final T fkValue) {
return this.<T, T>breakingCyclesWith(fkValue).apply(fkValue);
}

private <T> Function<Context, Context> storeWith(final @Nullable T fkValue) {
return ctx -> {
if (fkValue != null) {
final var store = ctx.<List<Object>>getOrDefault(ManyToOne.class, List.of());
final var next = Stream.concat(store.stream(), Stream.of(fkValue)).toList();

return ctx.put(ManyToOne.class, next);
}

return ctx;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
import static java.util.function.Predicate.not;
import static org.springframework.data.relational.core.query.Criteria.where;
import static org.springframework.data.relational.core.query.Query.query;
import static org.springframework.data.relational.core.query.Update.update;

import java.lang.reflect.Field;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;

import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.query.Update;
import org.springframework.data.relational.core.sql.SqlIdentifier;

import io.github.joselion.maybe.Maybe;
Expand All @@ -22,6 +23,7 @@
import io.github.joselion.springr2dbcrelationships.helpers.Reflect;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;

/**
* The {@link OneToMany} annotation processor.
Expand Down Expand Up @@ -55,30 +57,15 @@ public Mono<List<?>> populate(final OneToMany annotation, final Field field) {

return Mono.just(this.entity)
.mapNotNull(this::idValueOf)
.flatMap(entityId ->
Mono.deferContextual(ctx -> {
final var store = ctx.<List<Object>>getOrDefault(OneToMany.class, List.of());

return Flux.fromIterable(store)
.filter(entityId::equals)
.collectList()
.filter(List::isEmpty)
.map(x -> entityId);
})
)
.flatMap(this::breackingCycles)
.flatMap(entityId ->
this.template
.select(innerType)
.as(innerProjection)
.matching(query(where(mappedBy).is(entityId)).sort(byColumn))
.all()
.collectList()
.contextWrite(ctx -> {
final var store = ctx.<List<Object>>getOrDefault(OneToMany.class, List.of());
final var next = Stream.concat(store.stream(), Stream.of(entityId)).toList();

return ctx.put(OneToMany.class, next);
})
.contextWrite(this.storeWith(entityId))
);
}

Expand All @@ -92,6 +79,7 @@ public Mono<List<?>> persist(final OneToMany annotation, final Field field) {

return Mono.just(this.entity)
.mapNotNull(this::idValueOf)
.flatMap(this::breackingCycles)
.flatMap(entityId -> {
final var innerType = this.domainFor(Reflect.innerTypeOf(field));
final var mappedBy = Optional.of(annotation)
Expand Down Expand Up @@ -142,14 +130,36 @@ public Mono<List<?>> persist(final OneToMany annotation, final Field field) {
return this.template
.update(innerType)
.matching(allOrphans)
.apply(Update.update(mappedBy, null));
.apply(update(mappedBy, null));
}

return this.template
.delete(innerType)
.matching(allOrphans)
.all();
});
})
.contextWrite(this.storeWith(entityId));
});
}

private <T> Mono<T> breackingCycles(final T entityId) {
return Mono.deferContextual(ctx -> {
final var store = ctx.<List<Object>>getOrDefault(OneToMany.class, List.of());

return Flux.fromIterable(store)
.filter(entityId::equals)
.collectList()
.filter(List::isEmpty)
.map(x -> entityId);
});
}

private Function<Context, Context> storeWith(final Object entityId) {
return ctx -> {
final var store = ctx.<List<Object>>getOrDefault(OneToMany.class, List.of());
final var next = Stream.concat(store.stream(), Stream.of(entityId)).toList();

return ctx.put(OneToMany.class, next);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,27 @@ public Mono<Object> populate(final OneToOne annotation, final Field field) {

return Mono.just(this.entity)
.mapNotNull(Reflect.getter(mappedField))
.flatMap(this::breakOnCycle)
.flatMap(this::breakingCycles)
.flatMap(fkValue ->
this.template
.select(fieldType)
.as(fieldProjection)
.matching(query(where(parentId).is(fkValue)))
.one()
.contextWrite(this.storeOf(fkValue))
.contextWrite(this.storeWith(fkValue))
);
}

return Mono.just(this.entity)
.mapNotNull(this::idValueOf)
.flatMap(this::breakOnCycle)
.flatMap(this::breakingCycles)
.flatMap(entityId ->
this.template
.select(fieldType)
.as(fieldProjection)
.matching(query(where(mappedBy).is(entityId)))
.one()
.contextWrite(this.storeOf(entityId))
.contextWrite(this.storeWith(entityId))
);
}

Expand All @@ -90,20 +90,20 @@ public Mono<Object> persist(final OneToOne annotation, final Field field) {

return Mono.just(this.entity)
.mapNotNull(Reflect.getter(field))
.flatMap(this::breakOnCycle)
.flatMap(this::breakingCycles)
.flatMap(this::save)
.flatMap(saved -> {
final var savedId = this.idValueOf(saved);

return Mono.just(this.entity)
.map(Reflect.update(mappedField, savedId))
.map(Reflect.update(field, saved))
.contextWrite(this.storeOf(savedId));
.contextWrite(this.storeWith(savedId));
})
.switchIfEmpty(
Mono.just(this.entity)
.map(Reflect.update(mappedField, null))
.flatMap(this::breakOnCycle)
.flatMap(this::breakingCycles)
.delayUntil(x -> {
if (!annotation.keepOrphan() && mappedId != null) {
final var parentId = this.idColumnOf(fieldType);
Expand All @@ -118,12 +118,12 @@ public Mono<Object> persist(final OneToOne annotation, final Field field) {
})
)
.defaultIfEmpty(this.entity)
.contextWrite(this.storeOf(mappedId));
.contextWrite(this.storeWith(mappedId));
}

return Mono.just(this.entity)
.mapNotNull(this::idValueOf)
.flatMap(this::breakOnCycle)
.flatMap(this::breakingCycles)
.flatMap(entityId ->
Mono.just(this.entity)
.mapNotNull(Reflect.getter(field))
Expand All @@ -146,7 +146,7 @@ public Mono<Object> persist(final OneToOne annotation, final Field field) {
)
.then(Mono.empty())
)
.contextWrite(this.storeOf(entityId))
.contextWrite(this.storeWith(entityId))
);
}

Expand Down Expand Up @@ -213,7 +213,7 @@ private String inferMappedBy(final OneToOne annotation, final Field field) {
});
}

private Mono<Object> breakOnCycle(final Object entityId) {
private Mono<Object> breakingCycles(final Object entityId) {
return Mono.deferContextual(ctx -> {
final var store = ctx.<List<Object>>getOrDefault(OneToOne.class, List.of());
final var distinct = store.stream().distinct().toList();
Expand All @@ -230,7 +230,7 @@ private Mono<Object> breakOnCycle(final Object entityId) {
});
}

private Function<Context, Context> storeOf(final @Nullable Object entityId) {
private Function<Context, Context> storeWith(final @Nullable Object entityId) {
return ctx -> {
if (entityId != null) {
final var store = ctx.getOrDefault(OneToOne.class, List.<Object>of());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@
.then(cityRepo.findByName(boston))
)
.as(TxStepVerifier::withRollback)
.assertNext(consumer((countryId, found) -> {
assertThat(found.countryId()).isEqualTo(countryId);
assertThat(found.country()).isNotNull();
assertThat(found.country().id()).isEqualTo(countryId);
assertThat(found.country().cities())
.allSatisfy(city -> assertThat(city.country()).isNull())
.assertNext(consumer((countryId, city) -> {
assertThat(city.countryId()).isEqualTo(countryId);
assertThat(city.country()).isNotNull();
assertThat(city.country().id()).isEqualTo(countryId);
System.err.println("*********** " + city.country());
assertThat(city.country().cities())
.allSatisfy(c -> assertThat(c.country()).isNull())
.extracting(City::name)
.containsExactly(chicago, boston, newYork);
}))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,5 +313,31 @@
}
}
}

@Nested class when_the_children_persist_option_is_true {
@Test void creates_the_children_entities_breaking_cycles() {
Flux.just(manhattan, albuquerque, springfield)
.map(Town::of)
.delayElements(Duration.ofMillis(1))
.collectList()
.map(usa::withTowns)
.flatMap(countryRepo::save)
.map(Country::id)
.flatMap(countryRepo::findById)
.zipWhen(x -> countryRepo.count())
.as(TxStepVerifier::withRollback)
.assertNext(consumer((country, countryCount) -> {
assertThat(countryCount).isOne();
assertThat(country.towns())
.allSatisfy(town -> {
assertThat(town.countryId()).isEqualTo(country.id());
assertThat(town.country()).isNull();
})
.extracting(Town::name)
.containsExactly(springfield, albuquerque, manhattan);
}))
.verifyComplete();
}
}
}
}

0 comments on commit 4859270

Please sign in to comment.