tech.ydb.yoj.repository.db.projection.ProjectionMappings Maven / Gradle / Ivy
Show all versions of yoj-repository Show documentation
package tech.ydb.yoj.repository.db.projection;
import com.google.common.base.Preconditions;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import tech.ydb.yoj.databind.schema.Schema;
import tech.ydb.yoj.repository.db.Entity;
import tech.ydb.yoj.repository.db.EntityIdSchema;
import tech.ydb.yoj.repository.db.EntitySchema;
import tech.ydb.yoj.repository.db.list.ListRequest;
import tech.ydb.yoj.repository.db.list.ListResult;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import static java.util.stream.Collectors.toList;
import static lombok.AccessLevel.PRIVATE;
public final class ProjectionMappings {
private ProjectionMappings() {
}
/**
* Creates a lenient one-to-one mapping from entity fields to projection fields, which contains all
* {@link #strictFieldMapping(Class, Class) strict mappings}, plus mappings of the form
* {@code <-> } (if both fields exist).
*
* @param projectionType projection class
* @param entityType entity class
* @param projection type
* @param entity type
* @return Map: Entity field path -> Projection field path
* @see #strictFieldMapping(Class, Class)
*/
@NonNull
public static , T extends Entity> Map lenientFieldMapping(
@NonNull Class projectionType, @NonNull Class entityType) {
EntitySchema entitySchema = EntitySchema.of(entityType);
Class> entityIdType = entitySchema.getIdSchema().getType();
BiMap mapping = HashBiMap.create(strictFieldMapping(projectionType, entityType));
EntitySchema.of(projectionType).flattenFields()
.stream()
.filter(pf -> !isEntityId(pf, entityIdType) && !mapping.inverse().containsKey(pf.getPath()))
.forEach(pf -> findMatchingNonIdField(pf, entitySchema)
.ifPresent(ef -> mapping.put(ef.getPath(), pf.getPath()))
);
return mapping;
}
/**
* Creates a one-to-one mapping from entity fields to entity projection ID fields, assuming that the projection ID
* contains fields with the same name as in the main entity and at most one field for the main entity ID
* (with any name).
*
*
* E.g., the following entity-projection pair qualifies:
* @Value class MyEntity implements Entity<MyEntity> {
* Id id;
* String field;
*
* @Value static class Id implements Entity.Id<MyEntity> { String value; }
* }
*
* @Value class MyIndex implements Entity<MyIndex> {
* Id id;
*
* @Value
* static class Id implements Entity.Id<MyIndex> {
* // MUST have the same Java field name as in the entity class
* // (DB name specified in @Column annotation does not matter.)
* String field;
*
* // OPTIONAL. If present, this field MAY have any name
* MyEntity.Id entityId;
* }
* }
*
*
* @param projectionType projection class
* @param entityType entity class
* @param projection type
* @param entity type
* @return Bidirectional mapping: Entity field path -> Projection field path
*/
@NonNull
public static , T extends Entity> Map strictFieldMapping(
@NonNull Class projectionType, @NonNull Class entityType) {
EntitySchema entitySchema = EntitySchema.of(entityType);
Class> entityIdType = entitySchema.getIdSchema().getType();
List projectionIdFields = EntityIdSchema.ofEntity(projectionType).getFields();
Map mapping = new HashMap<>();
projectionIdFields
.stream()
.filter(f -> !isEntityId(f, entityIdType))
.flatMap(Schema.JavaField::flatten)
.forEach(f -> mapping.put(getMatchingNonIdField(f, entitySchema).getPath(), f.getPath()));
List idTypeFields = projectionIdFields.stream()
.filter(f -> isEntityId(f, entityIdType))
.collect(toList());
if (idTypeFields.size() > 1) {
throw new IllegalStateException("Projection ID cannot have more than 1 field with type: " + entityIdType);
}
if (idTypeFields.size() == 0) {
return mapping;
}
idTypeFields.get(0).flatten().forEach(idPart ->
mapping.put(getMatchingIdField(idPart, entitySchema).getPath(), idPart.getPath()));
return mapping;
}
private static boolean isEntityId(@NonNull Schema.JavaField field, @NonNull Class> entityIdType) {
return field.getType().equals(entityIdType);
}
@NonNull
private static Optional findMatchingNonIdField(@NonNull Schema.JavaField projectionField,
@NonNull EntitySchema> realEntity) {
// Entity. <-> Projection.id.
return realEntity.findField(projectionField.getRawPath());
}
@NonNull
private static Schema.JavaField getMatchingNonIdField(@NonNull Schema.JavaField projectionField,
@NonNull EntitySchema> realEntity) {
// Entity. <-> Projection.id.
return realEntity.getField(projectionField.getRawPath());
}
@NonNull
private static Schema.JavaField getMatchingIdField(@NonNull Schema.JavaField projectionIdField,
@NonNull EntitySchema> realEntity) {
// Entity.id[.] <-> Projection.id.[.]
return realEntity.getIdSchema().getField(projectionIdField.getRawSubPath(1));
}
@NonNull
public static > ListViaProjection
listViaProjection(@NonNull Class
projectionType) {
return new ListViaProjection<>(projectionType);
}
@RequiredArgsConstructor(access = PRIVATE)
public static final class ListViaProjection
> {
private final Class
projectionType;
@NonNull
public > Listing entities(@NonNull ListRequest request) {
return entities(request, lenientFieldMapping(projectionType, request.getSchema().getType()));
}
@NonNull
public > Listing entities(@NonNull ListRequest request,
@NonNull Map fieldMapping) {
return entities(request, f -> getFieldMapping(fieldMapping, f));
}
@NonNull
private String getFieldMapping(@NonNull Map fieldMapping, String f) {
String mapping = fieldMapping.get(f);
Preconditions.checkState(mapping != null,
"No mapping for entity field \"%s\" in projection %s", f, projectionType);
return mapping;
}
@NonNull
public > Listing entities(@NonNull ListRequest request,
@NonNull UnaryOperator fieldMapping) {
return new Listing<>(request, request.forEntity(projectionType, fieldMapping));
}
@RequiredArgsConstructor(access = PRIVATE)
public static final class Listing, P extends Entity> {
private final ListRequest request;
private final ListRequest projRequest;
@NonNull
public TransformedListing transforming(@NonNull Function unproject) {
return new TransformedListing<>(this, unproject);
}
@NonNull
public ListResult
run(@NonNull Function, ListResult> listFunc) {
return listFunc.apply(projRequest);
}
}
@RequiredArgsConstructor(access = PRIVATE)
public static final class TransformedListing, P extends Entity> {
private final Listing listing;
private final Function unproject;
@NonNull
public ListResult run(@NonNull Function, ListResult> listFunc) {
ListResult
projResult = listing.run(listFunc);
return ListResult.builder(listing.request)
.entries(projResult.stream().map(unproject).collect(toList()))
.lastPage(projResult.isLastPage())
.build();
}
}
}
}