All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.kenshoo.pl.entity.internal.fetch.OldEntityFetcher Maven / Gradle / Ivy

package com.kenshoo.pl.entity.internal.fetch;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.kenshoo.jooq.*;
import com.kenshoo.pl.data.ImpersonatorTable;
import com.kenshoo.pl.entity.UniqueKey;
import com.kenshoo.pl.entity.*;
import com.kenshoo.pl.entity.internal.EntityImpl;
import com.kenshoo.pl.entity.internal.EntityTypeReflectionUtil;
import com.kenshoo.pl.entity.internal.PartialEntityInvocationHandler;
import org.jooq.*;
import org.jooq.lambda.Seq;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.function.Predicate;

import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.*;
import static org.apache.commons.lang3.Validate.notEmpty;
import static org.jooq.lambda.Seq.seq;
import static org.jooq.lambda.function.Functions.not;


public class OldEntityFetcher {

    private final DSLContext dslContext;

    public OldEntityFetcher(DSLContext dslContext) {
        this.dslContext = dslContext;
    }

    public > Map, Entity> fetchEntitiesByKeys(final E entityType,
                                                                                    final UniqueKey uniqueKey,
                                                                                    final Collection> keys,
                                                                                    final Collection> fieldsToFetch) {
        return fetchEntitiesByIds(keys, fieldsToFetch);

    }

    public > Map, Entity> fetchEntitiesByIds(final Collection> ids,
                                                                                   final EntityField... fieldsToFetchArgs) {
        return fetchEntitiesByIds(ids, ImmutableList.copyOf(fieldsToFetchArgs));
    }

    public > Map, Entity> fetchEntitiesByIds(final Collection> ids,
                                                                                   final Collection> fieldsToFetch) {
        if (ids.isEmpty()) {
            return Collections.emptyMap();
        }
        final UniqueKey uniqueKey = ids.iterator().next().getUniqueKey();
        final EntityType entityType = uniqueKey.getEntityType();
        final AliasedKey aliasedKey = new AliasedKey<>(uniqueKey);

        final SelectJoinStep query = buildFetchQuery(entityType.getPrimaryTable(), aliasedKey.aliasedFields(), fieldsToFetch);
        try (QueryExtension> queryExtender = new QueryBuilder(dslContext).addIdsCondition(query, entityType.getPrimaryTable(), uniqueKey, ids)) {
            return fetchEntitiesMap(queryExtender.getQuery(), aliasedKey, fieldsToFetch);
        }
    }

    public List fetch(final EntityType entityType,
                              final PLCondition plCondition,
                              final EntityField... fieldsToFetch) {
        requireNonNull(entityType, "An entity type must be provided");
        requireNonNull(plCondition, "A condition must be provided");
        notEmpty(fieldsToFetch, "There must be at least one field to fetch");

        final Set> requestedFieldsToFetch = ImmutableSet.copyOf(fieldsToFetch);
        final Set> allFieldsToFetch = Sets.union(requestedFieldsToFetch, plCondition.getFields());

        final SelectJoinStep query = buildFetchQuery(entityType.getPrimaryTable(),
                emptyList(),
                allFieldsToFetch);
        final Condition completeJooqCondition = addVirtualPartitionConditions(entityType, plCondition.getJooqCondition());

        return query.where(completeJooqCondition)
                .fetch(record -> mapRecordToEntity(record, requestedFieldsToFetch));
    }

    public > Map, Entity> fetchEntitiesByForeignKeys(E entityType, UniqueKey foreignUniqueKey, Collection> keys, Collection> fieldsToFetch) {
        try (final TempTableResource foreignKeysTable = createForeignKeysTable(entityType.getPrimaryTable(), foreignUniqueKey, keys)) {
            final AliasedKey aliasedKey = new AliasedKey<>(foreignUniqueKey, foreignKeysTable);
            final SelectJoinStep query = buildFetchQuery(foreignKeysTable.getTable(), aliasedKey.aliasedFields(), fieldsToFetch);

            return fetchEntitiesMap(query, aliasedKey, fieldsToFetch);
        }
    }

    public , PE extends PartialEntity, ID extends Identifier> Map fetchPartialEntities(E entityType, Collection keys, final Class entityIface) {
        if (keys.isEmpty()) {
            return Collections.emptyMap();
        }
        UniqueKey uniqueKey = keys.iterator().next().getUniqueKey();
        final Map> entityMethodsMap = EntityTypeReflectionUtil.getMethodsMap(entityType, entityIface);
        Map, Entity> entityMap = fetchEntitiesByIds(keys, entityMethodsMap.values());
        ClassLoader classLoader = entityIface.getClassLoader();
        Class[] interfaces = {entityIface};
        //noinspection unchecked
        return keys.stream().filter(entityMap::containsKey)
                .collect(toMap(identity(), key -> (PE) Proxy.newProxyInstance(classLoader, interfaces, new PartialEntityInvocationHandler<>(entityMethodsMap, entityMap.get(key)))));
    }

    public , PE extends PartialEntity> List fetchByCondition(E entityType, Condition condition, final Class entityIface) {
        final Map> entityMethodsMap = EntityTypeReflectionUtil.getMethodsMap(entityType, entityIface);
        final Collection> fieldsToFetch = entityMethodsMap.values();
        SelectJoinStep query = buildFetchQuery(entityType.getPrimaryTable(), Collections.>emptyList(), fieldsToFetch);
        for (FieldAndValue fieldAndValue : entityType.getPrimaryTable().getVirtualPartition()) {
            //noinspection unchecked
            condition = condition.and(fieldAndValue.getField().eq(fieldAndValue.getValue()));
        }
        List entities = query.where(condition).fetch(record -> {
            EntityImpl entity = new EntityImpl();
            Iterator valuesIterator = record.intoList().iterator();
            for (EntityField field : fieldsToFetch) {
                fieldFromRecordToEntity(entity, field, valuesIterator);
            }
            return entity;
        });
        //noinspection unchecked
        ClassLoader classLoader = entityIface.getClassLoader();
        Class[] interfaces = {entityIface};
        //noinspection unchecked
        return entities.stream()
                .map(entity -> (PE) Proxy.newProxyInstance(classLoader, interfaces, new PartialEntityInvocationHandler<>(entityMethodsMap, entity)))
                .collect(toList());
    }

    /*
     This method generates a query that joins the starting table with one or more foreign keys, with the tables
     necessary to get to all fieldsToFetch. This is done by traversing the tree that starts at the starting table and
     follows all possible foreign keys, in the BFS manner. As soon as a table containing one of the fieldsToFetch is found,
     the necessary joins are made and the participating tables are marked as already joined. The target table is removed
     from the set of tables to fetch. The traversal continues until there are tables in this set.
     */
    private SelectJoinStep buildFetchQuery(DataTable startingTable, Collection> aliasedKeyFields, Collection> fieldsToFetch) {
        // The set of tables to reach with joins. This set is mutable, the tables are removed from it as they are reached
        Set targetPrimaryTables = fieldsToFetch.stream()
                .map(field -> field.getEntityType().getPrimaryTable())
                .filter(tb -> !tb.equals(startingTable))
                .collect(toSet());

        final Set targetOneToOneRelations = fieldsToFetch.stream()
                .filter(not(isOfPrimaryTable()))
                .map(field -> OneToOneTableRelation.builder()
                        .secondary(field.getDbAdapter().getTable())
                        .primary(field.getEntityType().getPrimaryTable())
                        .build())
                .collect(toSet());

        List> selectFields = dbFieldsOf(fieldsToFetch).concat(seq(aliasedKeyFields)).toList();

        final SelectJoinStep query = dslContext.select(selectFields).from(startingTable);
        final Set joinedTables = Sets.newHashSet(startingTable);
        final TreeEdge startingEdge = new TreeEdge(null, startingTable);

        BFS.visit(startingEdge, this::edgesComingOutOf)
                .limitUntil(__ -> targetPrimaryTables.isEmpty())
                .forEach(edge -> {
                    final DataTable table = edge.target.table;

                    if (edge != startingEdge && targetPrimaryTables.contains(table)) {
                        targetPrimaryTables.remove(table);
                        QueryBuilder.joinTables(query, joinedTables, edge);
                    }
                });

        if (!targetPrimaryTables.isEmpty()) {
            throw new IllegalStateException("Tables " + targetPrimaryTables + " could not be reached via joins");
        }

        QueryBuilder.joinSecondaryTables(query, joinedTables, targetOneToOneRelations);

        return query;
    }

    private Predicate> isOfPrimaryTable() {
        return field -> field.getDbAdapter().getTable().equals(field.getEntityType().getPrimaryTable());
    }

    private Seq edgesComingOutOf(TreeEdge edge) {
        return seq(edge.target.table.getReferences()).map(new ToEdgesOf(edge.target));
    }

    private > Map, Entity> fetchEntitiesMap(ResultQuery query, AliasedKey aliasedKey, Collection> fields) {
        return query.fetchMap(record -> RecordReader.createKey(record, aliasedKey), record -> RecordReader.createEntity(record, fields));
    }

    private > TempTableResource createForeignKeysTable(final DataTable primaryTable, final UniqueKey foreignUniqueKey, final Collection> keys) {
        ImpersonatorTable impersonatorTable = new ImpersonatorTable(primaryTable);
        foreignUniqueKey.getTableFields().forEach(impersonatorTable::createField);

        return TempTableHelper.tempInMemoryTable(dslContext, impersonatorTable, batchBindStep -> {
                    for (Identifier key : keys) {
                        EntityField[] keyFields = foreignUniqueKey.getFields();
                        List values = new ArrayList<>();
                        for (EntityField field : keyFields) {
                            addToValues(key, field, values);
                        }
                        batchBindStep.bind(values.toArray());
                    }
                }
        );
    }

    private  void fieldFromRecordToEntity(EntityImpl entity, EntityField field, Iterator valuesIterator) {
        entity.set(field, field.getDbAdapter().getFromRecord(valuesIterator));
    }

    private , T> void addToValues(Identifier key, EntityField field, List values) {
        field.getDbAdapter().getDbValues(key.get(field)).forEach(values::add);
    }

    private Seq> dbFieldsOf(Collection> fieldsToFetch) {
        return seq(fieldsToFetch).flatMap(field -> field.getDbAdapter().getTableFields());
    }

    private Condition addVirtualPartitionConditions(final EntityType entityType, final Condition inputJooqCondition) {
        return entityType.getPrimaryTable().getVirtualPartition().stream()
                         .map(this::asTypedFieldAndValue)
                         .map(fieldAndValue -> fieldAndValue.getField().eq(fieldAndValue.getValue()))
                         .reduce(inputJooqCondition, Condition::and);
    }

    private Entity mapRecordToEntity(final Record record, final Collection> fieldsToFetch) {
        final EntityImpl entity = new EntityImpl();
        final Iterator valuesIterator = record.intoList().iterator();
        fieldsToFetch.forEach( field -> fieldFromRecordToEntity(entity, field, valuesIterator));
        return entity;
    }

    @SuppressWarnings("unchecked")
    private FieldAndValue asTypedFieldAndValue(final FieldAndValue fv) {
        return (FieldAndValue)fv;
    }
}