Skip to content

Commit

Permalink
fix(core): Unlink many-to-one when null parent (#26)
Browse files Browse the repository at this point in the history
* fix(core): Unlink many-to-one when null parent

* Improve cycles with one-to-many relationships
  • Loading branch information
JoseLion committed Mar 5, 2024
1 parent 11927b2 commit 61d56d1
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 78 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
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,15 +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);
})
.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
Loading

0 comments on commit 61d56d1

Please sign in to comment.