Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): Unlink many-to-one when null parent #26

Merged
merged 2 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading