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

org.babyfish.jimmer.sql.loader.AbstractDataLoader Maven / Gradle / Ivy

There is a newer version: 0.9.19
Show newest version
package org.babyfish.jimmer.sql.loader;

import org.babyfish.jimmer.impl.util.CollectionUtils;
import org.babyfish.jimmer.lang.Ref;
import org.babyfish.jimmer.meta.*;
import org.babyfish.jimmer.runtime.DraftSpi;
import org.babyfish.jimmer.runtime.ImmutableSpi;
import org.babyfish.jimmer.runtime.Internal;
import org.babyfish.jimmer.sql.TransientResolver;
import org.babyfish.jimmer.sql.association.meta.AssociationType;
import org.babyfish.jimmer.sql.ast.Expression;
import org.babyfish.jimmer.sql.ast.Selection;
import org.babyfish.jimmer.sql.ast.impl.EntitiesImpl;
import org.babyfish.jimmer.sql.ast.impl.query.AbstractMutableQueryImpl;
import org.babyfish.jimmer.sql.ast.impl.query.FilterLevel;
import org.babyfish.jimmer.sql.ast.impl.query.MergedTypedRootQueryImpl;
import org.babyfish.jimmer.sql.ast.impl.query.Queries;
import org.babyfish.jimmer.sql.ast.impl.table.FetcherSelectionImpl;
import org.babyfish.jimmer.sql.ast.impl.table.TableImplementor;
import org.babyfish.jimmer.sql.ast.query.MutableQuery;
import org.babyfish.jimmer.sql.ast.query.Sortable;
import org.babyfish.jimmer.sql.ast.query.TypedRootQuery;
import org.babyfish.jimmer.sql.ast.table.Props;
import org.babyfish.jimmer.sql.ast.table.Table;
import org.babyfish.jimmer.sql.ast.table.spi.TableProxy;
import org.babyfish.jimmer.sql.ast.tuple.Tuple2;
import org.babyfish.jimmer.sql.cache.Cache;
import org.babyfish.jimmer.sql.cache.CacheAbandonedCallback;
import org.babyfish.jimmer.sql.cache.CacheEnvironment;
import org.babyfish.jimmer.sql.fetcher.Fetcher;
import org.babyfish.jimmer.sql.fetcher.FieldFilter;
import org.babyfish.jimmer.sql.fetcher.impl.*;
import org.babyfish.jimmer.sql.filter.CacheableFilter;
import org.babyfish.jimmer.sql.filter.Filter;
import org.babyfish.jimmer.sql.meta.ColumnDefinition;
import org.babyfish.jimmer.sql.meta.Storage;
import org.babyfish.jimmer.sql.runtime.ExecutionException;
import org.babyfish.jimmer.sql.runtime.ExecutionPurpose;
import org.babyfish.jimmer.sql.runtime.JSqlClientImplementor;

import java.sql.Connection;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public abstract class AbstractDataLoader {

    /* For globalFilter is not null but not `Filter.Parameterized` */
    private static final SortedMap ILLEGAL_PARAMETERS =
            Collections.unmodifiableSortedMap(new TreeMap<>());

    private final JSqlClientImplementor sqlClient;

    private final Connection con;

    private final FetchPath path;

    private final ImmutableProp prop;

    private final Storage storage;

    private final boolean remote;

    private final ImmutableProp sourceIdProp;

    private final ImmutableProp targetIdProp;

    private final org.babyfish.jimmer.sql.filter.Filter globalFiler;

    private final FieldFilter> propFilter;

    private final int limit;

    private final long offset;

    private final boolean rawValue;

    private final TransientResolver resolver;

    private final FetcherImplementor fetcher;

    @SuppressWarnings("unchecked")
    protected AbstractDataLoader(
            JSqlClientImplementor sqlClient,
            Connection con,
            ImmutableType entityType,
            FetchPath path,
            ImmutableProp prop,
            Fetcher fetcher,
            FieldFilter propFilter,
            int limit,
            int offset,
            boolean rawValue
    ) {
        if (!prop.getDependencies().isEmpty()) {
            throw new IllegalArgumentException(
                    "\"" + prop + "\" is view(@IdView, @ManyToManyView, @Formula) based on other properties"
            );
        }
        if (!prop.isAssociation(TargetLevel.ENTITY) && !prop.hasTransientResolver()) {
            throw new IllegalArgumentException(
                    "\"" + prop + "\" is neither association nor transient with resolver"
            );
        }
        if ((limit != Integer.MAX_VALUE || offset != 0) && !prop.isReferenceList(TargetLevel.PERSISTENT)) {
            throw new IllegalArgumentException(
                    "Cannot specify association property \"" +
                            prop +
                            "\" because it not list association(one-to-many/many-to-many)"
            );
        }
        if (!prop.isAssociation(TargetLevel.ENTITY)) {
            if (fetcher != null) {
                throw new IllegalArgumentException(
                        "Cannot specify fetcher for scalar prop \"" +
                                prop +
                                "\""
                );
            }
            if (propFilter != null) {
                throw new IllegalArgumentException(
                        "Cannot specify filter for scalar prop \"" +
                                prop +
                                "\""
                );
            }
            if (limit != Integer.MAX_VALUE) {
                throw new IllegalArgumentException(
                        "Cannot specify limit for scalar prop \"" +
                                prop +
                                "\""
                );
            }
            if (offset != 0) {
                throw new IllegalArgumentException(
                        "Cannot specify limit for scalar prop \"" +
                                prop +
                                "\""
                );
            }
        }
        if (entityType != null) {
            if (!entityType.isEntity()) {
                throw new IllegalArgumentException(
                        "\"" + entityType + "\" is not entity"
                );
            }
        } else if (!prop.getDeclaringType().isEntity()) {
            throw new IllegalArgumentException(
                    "\"" + prop + "\" is not declared in entity"
            );
        }
        this.sqlClient = sqlClient;
        this.con = con;
        this.path = FetchPath.of(path, prop);
        this.prop = prop;
        this.storage = prop.getStorage(sqlClient.getMetadataStrategy());
        this.remote = prop.isRemote();
        this.sourceIdProp = prop.getDeclaringType().getIdProp();
        this.targetIdProp = prop.getTargetType() != null ? prop.getTargetType().getIdProp() : null;
        if (prop.isAssociation(TargetLevel.PERSISTENT)) {
            globalFiler = sqlClient.getFilters().getTargetFilter(prop);
        } else {
            globalFiler = null;
        }
        this.propFilter = (FieldFilter>) propFilter;
        if (propFilter != null && prop.isReference(TargetLevel.ENTITY) && !prop.isNullable()) {
            throw new ExecutionException(
                    "Cannot apply field filter of object fetcher for \"" +
                            prop +
                            "\" because that property is not nullable"
            );
        }
        this.limit = limit;
        this.offset = offset;
        this.rawValue = rawValue;
        if (prop.isAssociation(TargetLevel.PERSISTENT)) {
            this.resolver = null;
            this.fetcher = fetcher != null ?
                    (FetcherImplementor) fetcher :
                    new FetcherImpl<>((Class) prop.getTargetType().getJavaClass());
        } else {
            this.resolver = sqlClient.getResolver(prop);
            if (prop.isAssociation(TargetLevel.ENTITY)) {
                this.fetcher = fetcher != null ?
                        (FetcherImplementor) fetcher :
                        new FetcherImpl<>((Class) prop.getTargetType().getJavaClass());
            } else {
                this.fetcher = null;
            }
        }
    }

    @SuppressWarnings("unchecked")
    public Map load(Collection sources) {
        if (sources.isEmpty()) {
            return Collections.emptyMap();
        }
        if (resolver != null) {
            return loadTransients(sources);
        }
        if (storage instanceof ColumnDefinition) {
            return (Map)(Map) loadParents(sources);
        }
        if (prop.isReferenceList(TargetLevel.ENTITY)) {
            return (Map)(Map) loadTargetMultiMap(sources);
        }
        return (Map)(Map) loadTargetMap(sources);
    }

    @SuppressWarnings("unchecked")
    private Map loadTransients(Collection sources) {

        Set sourceIds = toSourceIds(sources);
        TransientResolver resolver =
                ((TransientResolver) this.resolver);
        Cache cache = sqlClient.getCaches().getPropertyCache(prop);
        Ref> parameterMapRef = resolver.getParameterMapRef();
        SortedMap parameterMap = parameterMapRef != null ?
                standardParameterMap(parameterMapRef.getValue()) :
                null;
        Cache.Parameterized parameterizedCache =
                cache instanceof Cache.Parameterized ?
                        (Cache.Parameterized)cache :
                        null;
        final boolean useCache;
        if (cache != null) {
            if (parameterMapRef == null) {
                useCache = false;
                CacheAbandonedCallback callback = sqlClient.getCaches().getAbandonedCallback();
                if (callback != null) {
                    callback.abandoned(prop, CacheAbandonedCallback.Reason.CACHEABLE_FILTER_REQUIRED);
                }
            } else {
                if (parameterMap != null && !parameterMap.isEmpty() && parameterizedCache == null) {
                    useCache = false;
                    CacheAbandonedCallback callback = sqlClient.getCaches().getAbandonedCallback();
                    if (callback != null) {
                        callback.abandoned(prop, CacheAbandonedCallback.Reason.PARAMETERIZED_CACHE_REQUIRED);
                    }
                } else {
                    useCache = true;
                }
            }
        } else {
            useCache = false;
        }

        if (!useCache) {
            Map resolvedMap;
            TransientResolverContext ctx = TransientResolverContext.push(con, resolver, sourceIds);
            try {
                resolvedMap = translateResolvedMap(resolver.resolve(sourceIds), sourceIds);
            } finally {
                TransientResolverContext.pop(ctx);
            }
            return Utils.joinCollectionAndMap(
                    sources,
                    this::toSourceId,
                    resolvedMap
            );
        }

        CacheEnvironment env = new CacheEnvironment<>(
                sqlClient,
                con,
                (ids) -> {
                    TransientResolverContext ctx = TransientResolverContext.push(con, resolver, ids);
                    try {
                        return resolver.resolve(ids);
                    } finally {
                        TransientResolverContext.pop(ctx);
                    }
                },
                false
        );
        Map valueMap =
                parameterMap != null && !parameterMap.isEmpty() && parameterizedCache != null ?
                        parameterizedCache.getAll(sourceIds, parameterMap, env) :
                        cache.getAll(sourceIds, env);
        return Utils.joinCollectionAndMap(
                sources,
                this::toSourceId,
                translateResolvedMap(valueMap, sourceIds)
        );
    }

    private Map loadParents(Collection sources) {
        Cache fkCache = sqlClient.getCaches().getPropertyCache(prop);
        SortedMap parameters = getParameters();
        if (!remote && !useCache(fkCache, parameters)) {
            return loadParentsDirectly(sources);
        }
        Map fkMap = new LinkedHashMap<>(
                (sources.size() * 4 + 2) / 3
        );
        Collection missedFkSourceIds;
        missedFkSourceIds = new ArrayList<>();
        if (isUnreliableParentId()) {
            missedFkSourceIds = toSourceIds(sources);
        } else {
            for (ImmutableSpi source : sources) {
                if (source.__isLoaded(prop.getId())) {
                    ImmutableSpi target = (ImmutableSpi) source.__get(prop.getId());
                    if (target != null) {
                        fkMap.put(toSourceId(source), toTargetId(target));
                    }
                } else {
                    missedFkSourceIds.add(toSourceId(source));
                }
            }
        }
        if (!missedFkSourceIds.isEmpty()) {
            Map missedFkMap;
            if (remote) {
                missedFkMap = queryForeignKeyMap(missedFkSourceIds);
            } else {
                CacheEnvironment env = new CacheEnvironment<>(
                        sqlClient,
                        con,
                        this::queryForeignKeyMap,
                        false
                );
                missedFkMap = parameters != null ?
                        ((Cache.Parameterized) fkCache).getAll(missedFkSourceIds, parameters, env) :
                        fkCache.getAll(missedFkSourceIds, env);
            }
            for (Object sourceId : missedFkSourceIds) {
                Object fk = missedFkMap.get(sourceId);
                if (fk != null) {
                    fkMap.put(sourceId, fk);
                }
            }
        }
        Map targetMap =
                Utils.joinMaps(
                        fkMap,
                        Utils.toMap(
                                this::toTargetId,
                                findTargets(new LinkedHashSet<>(fkMap.values()))
                        )
                );
        return Utils.joinCollectionAndMap(sources, this::toSourceId, targetMap);
    }

    private Map loadParentsDirectly(
            Collection sources
    ) {
        Map fkMap = new LinkedHashMap<>(
                (sources.size() * 4 + 2) / 3
        );
        Collection missedFkSourceIds = new ArrayList<>();
        for (ImmutableSpi source : sources) {
            if (source.__isLoaded(prop.getId())) {
                ImmutableSpi target = (ImmutableSpi) source.__get(prop.getId());
                if (target != null) {
                    fkMap.put(toSourceId(source), toTargetId(target));
                }
            } else {
                missedFkSourceIds.add(toSourceId(source));
            }
        }
        Map map1 = null;
        if (!fkMap.isEmpty()) {
            if (isUnreliableParentId()) {
                map1 = Utils.joinMaps(
                        fkMap,
                        Utils.toMap(
                                this::toTargetId,
                                queryTargets(fkMap.values())
                        )
                );
            } else {
                map1 = Utils.joinMaps(
                        fkMap,
                        Utils.toMap(
                                this::toTargetId,
                                findTargets(fkMap.values())
                        )
                );
            }
        }
        Map map2 = null;
        if (!missedFkSourceIds.isEmpty()) {
            if (isUnreliableParentId() || fetcher.getFieldMap().size() > 1) {
                map2 = Tuple2.toMap(
                        querySourceTargetPairs(missedFkSourceIds)
                );
            } else {
                Map loadedFkMap =
                        queryForeignKeyMap(missedFkSourceIds);
                map2 = new LinkedHashMap<>((missedFkSourceIds.size() * 4 + 2) / 3);
                for (Object sourceId : missedFkSourceIds) {
                    Object targetId = loadedFkMap.get(sourceId);
                    map2.put(sourceId, makeIdOnlyTarget(targetId));
                }
            }
        }
        return Utils.joinCollectionAndMap(
                sources,
                this::toSourceId,
                Utils.mergeMap(map1, map2)
        );
    }

    private Map loadTargetMap(Collection sources) {
        Cache cache = sqlClient.getCaches().getPropertyCache(prop);
        SortedMap parameters = getParameters();
        if (!remote && !useCache(cache, parameters)) {
            return loadTargetMapDirectly(sources);
        }
        if (remote && prop.getMappedBy() != null) {
            List> tuples;
            try {
                tuples = sqlClient
                        .getMicroServiceExchange()
                        .findByAssociatedIds(
                                prop.getTargetType().getMicroServiceName(),
                                prop.getMappedBy(),
                                toSourceIds(sources),
                                FetcherFactory.excludeMicroServiceNameExceptRoot(fetcher, prop.getDeclaringType().getMicroServiceName())
                        );
            } catch (Exception ex) {
                throw new ExecutionException(
                        "Cannot load the remote association \"" +
                                prop +
                                "\" because error raised",
                        ex
                );
            }
            return Utils.joinCollectionAndMap(
                    sources,
                    this::toSourceId,
                    Tuple2.toMap(tuples)
            );
        }
        Set sourceIds = toSourceIds(sources);
        Map idMap;
        if (remote) {
            idMap = Tuple2.toMap(querySourceTargetIdPairs(sourceIds));
        } else {
            CacheEnvironment env = new CacheEnvironment<>(
                    sqlClient,
                    con,
                    it -> Tuple2.toMap(
                            querySourceTargetIdPairs(it)
                    ),
                    false
            );
            idMap = parameters != null ?
                    ((Cache.Parameterized) cache).getAll(sourceIds, parameters, env) :
                    cache.getAll(sourceIds, env);
        }
        Map targetMap = Utils.toMap(
                this::toTargetId,
                findTargets(new LinkedHashSet<>(idMap.values()))
        );
        return Utils.joinCollectionAndMap(
                sources,
                this::toSourceId,
                Utils.joinMaps(idMap, targetMap)
        );
    }

    private Map loadTargetMapDirectly(Collection sources) {
        Set sourceIds = toSourceIds(sources);
        Map targetMap;
        if (globalFiler != null || propFilter != null || fetcher.getFieldMap().size() > 1) {
            targetMap = Tuple2.toMap(
                    querySourceTargetPairs(sourceIds)
            );
        } else {
            targetMap = Tuple2.toMap(
                    querySourceTargetIdPairs(sourceIds),
                    this::makeIdOnlyTarget
            );
        }
        return Utils.joinCollectionAndMap(
                sources,
                this::toSourceId,
                targetMap
        );
    }

    private Map> loadTargetMultiMap(Collection sources) {
        Cache> cache = sqlClient.getCaches().getPropertyCache(prop);
        SortedMap parameters = getParameters();
        if (!remote && !useCache(cache, parameters)) {
            return loadTargetMultiMapDirectly(sources);
        }
        if (remote && prop.getMappedBy() != null) {
            List> tuples;
            try {
                tuples = sqlClient
                        .getMicroServiceExchange()
                        .findByAssociatedIds(
                                prop.getTargetType().getMicroServiceName(),
                                prop.getMappedBy(),
                                toSourceIds(sources),
                                FetcherFactory.excludeMicroServiceNameExceptRoot(fetcher, prop.getDeclaringType().getMicroServiceName())
                        );
            } catch (Exception ex) {
                throw new ExecutionException(
                        "Cannot load the remote association \"" +
                                prop +
                                "\" because error raised",
                        ex
                );
            }
            return Utils.joinCollectionAndMap(
                    sources,
                    this::toSourceId,
                    Tuple2.toMultiMap(tuples)
            );
        }
        Set sourceIds = toSourceIds(sources);
        Map> idMultiMap;
        if (remote) {
            idMultiMap = Tuple2.toMultiMap(
                    querySourceTargetIdPairs(sourceIds)
            );
        } else {
            CacheEnvironment> env = new CacheEnvironment<>(
                    sqlClient,
                    con,
                    it -> Tuple2.toMultiMap(
                            querySourceTargetIdPairs(it)
                    ),
                    false
            );
            idMultiMap = parameters != null ?
                    ((Cache.Parameterized>) cache).getAll(sourceIds, parameters, env) :
                    cache.getAll(sourceIds, env);
        }
        Map targetMap = Utils.toMap(
                this::toTargetId,
                findTargets(
                        idMultiMap
                                .values()
                                .stream()
                                .filter(Objects::nonNull)
                                .flatMap(Collection::stream)
                                .distinct()
                                .collect(Collectors.toList())
                )
        );
        return Utils.joinCollectionAndMap(
                sources,
                this::toSourceId,
                Utils.joinMultiMapAndMap(idMultiMap, targetMap)
        );
    }

    private Map> loadTargetMultiMapDirectly(Collection sources) {
        Set sourceIds = toSourceIds(sources);
        Map> targetMap;
        if (globalFiler != null || propFilter != null || fetcher.getFieldMap().size() > 1) {
            targetMap = Tuple2.toMultiMap(
                    querySourceTargetPairs(sourceIds)
            );
        } else {
            targetMap = Tuple2.toMultiMap(
                    querySourceTargetIdPairs(sourceIds),
                    this::makeIdOnlyTarget
            );
        }
        return Utils.joinCollectionAndMap(
                sources,
                this::toSourceId,
                targetMap
        );
    }

    private Map queryForeignKeyMap(Collection sourceIds) {

        if (sourceIds.size() == 1) {
            Object sourceId = CollectionUtils.first(sourceIds);
            List targetIds = Queries.createQuery(sqlClient, prop.getDeclaringType(), ExecutionPurpose.LOAD, FilterLevel.IGNORE_ALL, (q, source) -> {
                Expression pkExpr = source.get(sourceIdProp);
                Table targetTable = source.join(prop);
                Expression fkExpr = source.getAssociatedId(prop);
                q.where(pkExpr.eq(sourceId));
                q.where(fkExpr.isNotNull());
                applyPropFilter(q, targetTable, sourceIds);
                applyGlobalFilter(q, targetTable);
                applyDefaultOrder(q, targetTable);
                return q.select(fkExpr);
            }).execute(con);
            return Utils.toMap(sourceId, targetIds);
        }
        List> tuples = Queries
                .createQuery(sqlClient, prop.getDeclaringType(), ExecutionPurpose.LOAD, FilterLevel.IGNORE_ALL, (q, source) -> {
                    Expression pkExpr = source.get(sourceIdProp);
                    Table targetTable = source.join(prop);
                    Expression fkExpr = source.getAssociatedId(prop);
                    q.where(pkExpr.in(sourceIds));
                    q.where(fkExpr.isNotNull());
                    applyPropFilter(q, targetTable, sourceIds);
                    applyGlobalFilter(q, targetTable);
                    applyDefaultOrder(q, targetTable);
                    return q.select(pkExpr, fkExpr);
                }).execute(con);
        return Tuple2.toMap(tuples);
    }

    private List> querySourceTargetIdPairs(Collection sourceIds) {
        if (propFilter == null && prop.getReal().isMiddleTableDefinition()) {
            if (sourceIds.size() == 1) {
                Object sourceId = CollectionUtils.first(sourceIds);
                List targetIds = Queries.createAssociationQuery(sqlClient, AssociationType.of(prop), ExecutionPurpose.LOAD, (q, association) -> {
                    Expression sourceIdExpr = association.sourceId();
                    Expression targetIdExpr = association.targetId();
                    q.where(sourceIdExpr.eq(sourceId));
                    applyPropFilter(q, association.target(), sourceIds);
                    applyGlobalFilter(q, association.target());
                    applyDefaultOrder(q, association.target());
                    return q.select(targetIdExpr);
                }).limit(limit, offset).execute(con);
                return Utils.toTuples(sourceId, targetIds);
            }
            if (limit != Integer.MAX_VALUE || offset != 0) {
                TypedRootQuery>[] queries = new TypedRootQuery[sourceIds.size()];
                int index = 0;
                for (Object sourceId : sourceIds) {
                    queries[index++] =
                            Queries.createAssociationQuery(sqlClient, AssociationType.of(prop), ExecutionPurpose.LOAD, (q, association) -> {
                                Expression sourceIdExpr = association.sourceId();
                                Expression targetIdExpr = association.targetId();
                                q.where(sourceIdExpr.eq(sourceId));
                                applyPropFilter(q, association.target(), Collections.singletonList(sourceId));
                                applyGlobalFilter(q, association.target());
                                applyDefaultOrder(q, association.target());
                                return q.select(sourceIdExpr, targetIdExpr);
                            }).limit(limit, offset);
                }
                return new MergedTypedRootQueryImpl<>(sqlClient, "union all", queries).execute(con);
            }
            return Queries.createAssociationQuery(sqlClient, AssociationType.of(prop), ExecutionPurpose.LOAD, (q, association) -> {
                Expression sourceIdExpr = association.sourceId();
                Expression targetIdExpr = association.targetId();
                q.where(sourceIdExpr.in(sourceIds));
                applyPropFilter(q, association.target(), sourceIds);
                applyGlobalFilter(q, association.target());
                applyDefaultOrder(q, association.target());
                return q.select(sourceIdExpr, targetIdExpr);
            }).execute(con);
        }
        return executeTupleQuery(sourceIds, target -> target.get(targetIdProp.getName()));
    }

    @SuppressWarnings("unchecked")
    private List> querySourceTargetPairs(
            Collection sourceIds
    ) {
        return executeTupleQuery(sourceIds, target -> new FetcherSelectionImpl<>(target, path, fetcher));
    }

    @SuppressWarnings("unchecked")
    private List queryTargets(Collection targetIds) {

        return Queries.createQuery(sqlClient, prop.getTargetType(), ExecutionPurpose.LOAD, FilterLevel.IGNORE_ALL, (q, target) -> {
            Expression idExpr = target.get(targetIdProp.getName());
            q.where(idExpr.in(targetIds));
            applyPropFilter(q, target, targetIds);
            applyGlobalFilter(q, target);
            applyDefaultOrder(q, target);
            return q.select(
                    new FetcherSelectionImpl<>((Table)target, path, fetcher)
            );
        }).execute(con);
    }

    @SuppressWarnings("unchecked")
    private  List> executeTupleQuery(
            Collection sourceIds,
            Function, Selection> valueExpressionGetter
    ) {
        if (sourceIds.size() == 1) {
            Object sourceId = CollectionUtils.first(sourceIds);
            List results = Queries.createQuery(sqlClient, prop.getTargetType(), ExecutionPurpose.LOAD, FilterLevel.IGNORE_ALL, (q, target) -> {
                Expression sourceIdExpr = target.inverseGetAssociatedId(prop);
                q.where(sourceIdExpr.eq(sourceId));
                applyPropFilter(q, target, sourceIds);
                applyGlobalFilter(q, target);
                applyDefaultOrder(q, target);
                return q.select((Selection) valueExpressionGetter.apply((Table) target));
            }).limit(limit, offset).execute(con);
            return Utils.toTuples(sourceId, results);
        }
        if (limit != Integer.MAX_VALUE || offset != 0) {
            TypedRootQuery>[] queries = new TypedRootQuery[sourceIds.size()];
            int index = 0;
            for (Object sourceId : sourceIds) {
                queries[index++] =
                        Queries.createQuery(sqlClient, prop.getTargetType(), ExecutionPurpose.LOAD, FilterLevel.IGNORE_ALL, (q, target) -> {
                            Expression sourceIdExpr = target.inverseGetAssociatedId(prop);
                            q.where(sourceIdExpr.eq(sourceId));
                            applyPropFilter(q, target, Collections.singletonList(sourceId));
                            applyGlobalFilter(q, target);
                            applyDefaultOrder(q, target);
                            return q.select(sourceIdExpr, (Selection) valueExpressionGetter.apply((Table) target));
                        }).limit(limit, offset);
            }
            return new MergedTypedRootQueryImpl<>(sqlClient, "union all", queries).execute(con);
        }
        return Queries.createQuery(sqlClient, prop.getTargetType(), ExecutionPurpose.LOAD, FilterLevel.IGNORE_ALL, (q, target) -> {
            Expression sourceIdExpr = target.inverseGetAssociatedId(prop);
            q.where(sourceIdExpr.in(sourceIds));
            applyPropFilter(q, target, sourceIds);
            applyGlobalFilter(q, target);
            applyDefaultOrder(q, target);
            return q.select(sourceIdExpr, (Selection) valueExpressionGetter.apply((Table) target));
        }).execute(con);
    }

    private void applyGlobalFilter(Sortable sortable, Table table) {
        AbstractMutableQueryImpl query = (AbstractMutableQueryImpl) sortable;
        query.setOrderByPriority(AbstractMutableQueryImpl.ORDER_BY_PRIORITY_GLOBAL_FILTER);
        TableImplementor tableImplementor = null;
        if (table instanceof TableImplementor) {
            tableImplementor = (TableImplementor) table;
        } else if (table instanceof TableProxy) {
            tableImplementor = ((TableProxy) table).__unwrap();
        }
        if (tableImplementor == null) {
            throw new AssertionError(
                    "The table create by data loader must be table implementation or table wrapper"
            );
        }
        query.applyDataLoaderGlobalFilters(tableImplementor);
    }

    @SuppressWarnings("unchecked")
    private void applyPropFilter(
            MutableQuery query,
            Table table,
            Collection keys
    ) {
        if (propFilter != null) {
            FieldFilterArgsImpl> args = FieldFilterArgsImpl.of(
                    (AbstractMutableQueryImpl) query,
                    (Table) table,
                    keys
            );
            ((AbstractMutableQueryImpl)query)
                    .setOrderByPriority(AbstractMutableQueryImpl.ORDER_BY_PRIORITY_PROP_FILTER);
            propFilter.apply(args);
        }
    }

    private void applyDefaultOrder(
            MutableQuery query,
            Table table
    ) {
        if (((AbstractMutableQueryImpl)query).getAcceptedOrderByPriority() >
                AbstractMutableQueryImpl.ORDER_BY_PRIORITY_STATEMENT) {
            return;
        }
        List orderedItems = prop.getOrderedItems();
        if (!orderedItems.isEmpty() && !remote) {
            for (OrderedItem orderedItem : orderedItems) {
                Expression expr = table.get(orderedItem.getProp().getName());
                if (orderedItem.isDesc()) {
                    query.orderBy(expr.desc());
                } else {
                    query.orderBy(expr);
                }
            }
        }
    }

    private Object toSourceId(ImmutableSpi source) {
        return source.__get(sourceIdProp.getId());
    }

    private Set toSourceIds(Collection sources) {
        Set sourceIds = new LinkedHashSet<>((sources.size() * 4 + 2) / 3);
        for (ImmutableSpi source : sources) {
            sourceIds.add(toSourceId(source));
        }
        return sourceIds;
    }

    private Object toTargetId(ImmutableSpi target) {
        if (target == null) {
            return null;
        }
        return target.__get(targetIdProp.getId());
    }


    @SuppressWarnings("unchecked")
    private List findTargets(Collection targetIds) {
        if (fetcher.getFieldMap().size() < 2 && !remote) {
            return makeIdOnlyTargets(targetIds);
        }
        if (remote) {
            try {
                return sqlClient.getMicroServiceExchange().findByIds(
                        prop.getTargetType().getMicroServiceName(),
                        targetIds,
                        FetcherFactory.excludeMicroServiceNameExceptRoot(fetcher, prop.getDeclaringType().getMicroServiceName())
                );
            } catch (Exception ex) {
                throw new ExecutionException(
                        "Cannot load the remote association \"" +
                                prop +
                                "\" because error raised",
                        ex
                );
            }
        }
        return ((EntitiesImpl)sqlClient.getEntities()).forLoader().forConnection(con).findByIds(
                fetcher,
                targetIds
        );
    }

    private List makeIdOnlyTargets(Collection targetIds) {
        return targetIds
                .stream()
                .map(this::makeIdOnlyTarget)
                .collect(
                        Collectors.toCollection(
                                () -> new ArrayList<>(targetIds.size())
                        )
                );
    }

    private ImmutableSpi makeIdOnlyTarget(Object id) {
        if (id == null) {
            return null;
        }
        return (ImmutableSpi) Internal.produce(prop.getTargetType(), null, draft -> {
            DraftSpi targetDraft = (DraftSpi) draft;
            targetDraft.__set(targetIdProp.getId(), id);
        });
    }

    private SortedMap getParameters() {
        Filter filter = globalFiler;
        if (filter instanceof CacheableFilter) {
            SortedMap parameters = standardParameterMap(((CacheableFilter) filter).getParameters());
            if (parameters.isEmpty()) {
                return null;
            }
            return parameters;
        }
        if (filter != null) {
            return ILLEGAL_PARAMETERS;
        }
        return null;
    }

    private boolean useCache(Cache cache, Map parameters) {
        if (cache == null) {
            return false;
        }
        if (remote) {
            return true;
        }
        if (propFilter != null) {
            CacheAbandonedCallback callback = sqlClient.getCaches().getAbandonedCallback();
            if (callback != null) {
                callback.abandoned(prop, CacheAbandonedCallback.Reason.FIELD_FILTER_USED);
            }
            return false;
        }
        if (parameters == ILLEGAL_PARAMETERS) {
            CacheAbandonedCallback callback = sqlClient.getCaches().getAbandonedCallback();
            if (callback != null) {
                callback.abandoned(prop, CacheAbandonedCallback.Reason.CACHEABLE_FILTER_REQUIRED);
            }
            return false;
        }
        if (parameters != null && !(cache instanceof Cache.Parameterized)) {
            CacheAbandonedCallback callback = sqlClient.getCaches().getAbandonedCallback();
            if (callback != null) {
                callback.abandoned(prop, CacheAbandonedCallback.Reason.PARAMETERIZED_CACHE_REQUIRED);
            }
            return false;
        }
        return true;
    }

    private Map translateResolvedMap(Map map, Collection keys) {
        map = fetchResolvedMap(map);
        Object defaultValue = resolver.getDefaultValue();
        if (defaultValue == null && prop.isReferenceList(TargetLevel.OBJECT)) {
            defaultValue = Collections.emptyList();
        }
        if (defaultValue != null) {
            for (Object key : keys) {
                if (map.get(key) == null) {
                    map.putIfAbsent(key, defaultValue);
                }
            }
        }
        return map;
    }

    @SuppressWarnings("unchecked")
    private Map fetchResolvedMap(Map map) {
        if (map.isEmpty() || !prop.isAssociation(TargetLevel.OBJECT) || !prop.getTargetType().isEntity()) {
            return map;
        }
        Collection targetIds = new LinkedHashSet<>();
        if (prop.isReferenceList(TargetLevel.OBJECT)) {
            for (Object mapValue : map.values()) {
                if (mapValue != null) {
                    for (Object targetId : (Collection) mapValue) {
                        if (targetId != null) {
                            targetIds.add(targetId);
                        }
                    }
                }
            }
        } else {
            for (Object targetId : map.values()) {
                if (targetId != null) {
                    targetIds.add(targetId);
                }
            }
        }

        boolean noFilter = propFilter == null && globalFiler == null;
        List targets;
        if (noFilter) {
            targets = findTargets(targetIds);
        } else {
            targets = Queries.createQuery(sqlClient, prop.getTargetType(), ExecutionPurpose.LOAD, FilterLevel.IGNORE_ALL, (q, target) -> {
                Expression pkExpr = target.get(targetIdProp.getName());
                q.where(pkExpr.in(targetIds));
                applyPropFilter(q, target, map.keySet());
                applyGlobalFilter(q, target);
                return q.select(
                        new FetcherSelectionImpl<>((Table)target, path, fetcher)
                );
            }).execute(con);
        }

        if (targets.isEmpty()) {
            return new LinkedHashMap<>();
        }

        Map targetMap = new HashMap<>((targets.size() * 4 + 2) / 3);
        PropId targetIdPropId = prop.getTargetType().getIdProp().getId();
        for (ImmutableSpi target : targets) {
            targetMap.put(target.__get(targetIdPropId), target);
        }

        Map fetchedMap = new LinkedHashMap<>((map.size() * 4 + 2) / 3);
        if (noFilter && prop.isReferenceList(TargetLevel.ENTITY)) {
            for (Map.Entry e : map.entrySet()) {
                Collection subCollection = (Collection) e.getValue();
                if (subCollection != null) {
                    List targetList = new ArrayList<>(subCollection.size());
                    for (Object targetId : subCollection) {
                        ImmutableSpi target = targetMap.get(targetId);
                        if (target != null) {
                            targetList.add(target);
                        }
                    }
                    fetchedMap.put(e.getKey(), targetList);
                } else {
                    fetchedMap.put(e.getKey(), Collections.emptyList());
                }
            }
        } else if (!noFilter && prop.isReferenceList(TargetLevel.ENTITY)) {
            IdentityHashMap identityMap = new IdentityHashMap<>();
            for (Map.Entry e : map.entrySet()) {
                Collection subCollection = (Collection) e.getValue();
                if (subCollection != null) {
                    for (Object targetId : subCollection) {
                        ImmutableSpi target = targetMap.get(targetId);
                        if (target != null) {
                            identityMap.put(target, e.getKey());
                        }
                    }
                }
            }
            for (ImmutableSpi target : targets) {
                Object key = identityMap.get(target);
                if (key != null) {
                    List targetList = (List) fetchedMap.get(key);
                    if (targetList == null) {
                        Collection ids = (Collection) map.get(key);
                        if (ids != null) {
                            targetList = new ArrayList<>(ids.size());
                        } else {
                            targetList = new ArrayList<>();
                        }
                        fetchedMap.put(key, targetList);
                    }
                    targetList.add(target);
                }
            }
        } else {
            for (Map.Entry e : map.entrySet()) {
                ImmutableSpi target = targetMap.get(e.getValue());
                if (target != null) {
                    fetchedMap.put(e.getKey(), target);
                }
            }
        }
        return fetchedMap;
    }

    private boolean isUnreliableParentId() {
        return !remote && (
                (!rawValue && (globalFiler != null || propFilter != null)) || !((ColumnDefinition)storage).isForeignKey()
        );
    }

    private static SortedMap standardParameterMap(SortedMap parameters) {
        if (parameters == null || parameters.isEmpty()) {
            return parameters;
        }
        boolean hasNullValue = false;
        for (Object o : parameters.values()) {
            if (o == null) {
                hasNullValue = true;
                break;
            }
        }
        if (!hasNullValue) {
            return parameters;
        }
        SortedMap withoutNullValueMap = new TreeMap<>();
        for (Map.Entry e : parameters.entrySet()) {
            Object value = e.getValue();
            if (value != null) {
                withoutNullValueMap.put(e.getKey(), value);
            }
        }
        return withoutNullValueMap;
    }
}