Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.sap.cds.impl.AssociationLoader Maven / Gradle / Ivy
/************************************************************************
* © 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
************************************************************************/
package com.sap.cds.impl;
import static com.sap.cds.impl.LazyRowImpl.lazyRow;
import static com.sap.cds.impl.builder.model.ExpressionImpl.matching;
import static com.sap.cds.impl.builder.model.StructuredTypeRefImpl.typeRef;
import static com.sap.cds.impl.parser.token.RefSegmentImpl.refSegment;
import static com.sap.cds.ql.cqn.CqnComparisonPredicate.Operator.EQ;
import static com.sap.cds.util.CdsModelUtils.concreteKeyNames;
import static com.sap.cds.util.CdsModelUtils.isSingleValued;
import static java.util.stream.Collectors.groupingBy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sap.cds.CdsDataStore;
import com.sap.cds.CdsDataStoreException;
import com.sap.cds.CdsException;
import com.sap.cds.Result;
import com.sap.cds.Row;
import com.sap.cds.impl.builder.model.ExpandBuilder;
import com.sap.cds.impl.qat.QatBuilder;
import com.sap.cds.jdbc.spi.DbContext;
import com.sap.cds.jdbc.spi.SqlMapping;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnExpand;
import com.sap.cds.ql.cqn.CqnParameter;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnReference;
import com.sap.cds.ql.cqn.CqnReference.Segment;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnSelectListItem;
import com.sap.cds.ql.cqn.CqnSelectListValue;
import com.sap.cds.ql.cqn.CqnSortSpecification;
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
import com.sap.cds.ql.cqn.CqnValue;
import com.sap.cds.ql.impl.ExpandProcessor;
import com.sap.cds.ql.impl.SelectBuilder;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.util.CdsModelUtils;
import com.sap.cds.util.DataUtils;
import com.sap.cds.util.OnConditionAnalyzer;
public class AssociationLoader {
private static final Logger logger = LoggerFactory.getLogger(AssociationLoader.class);
private static final String PK_PREFIX = "@@";
private final CdsDataStore dataStore;
private final CdsStructuredType root;
private final List keyNames;
private final Map pk2Element = new HashMap<>();
private final SqlMapping sqlMapping;
public AssociationLoader(CdsDataStore dataStore, DbContext dbCtx, CdsStructuredType root) {
this.dataStore = dataStore;
this.root = root;
keyNames = new ArrayList<>(CdsModelUtils.concreteKeyNames(root));
sqlMapping = dbCtx.getSqlMapping(root);
}
public void expand(ExpandProcessor expandProcessor, List> rows) {
ExpandBuilder> expand = expandProcessor.getExpand();
if (dataStore == null || rows.isEmpty()) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Expand {} using parent-keys", expand.ref());
}
boolean lazy = expand.lazy();
boolean addCount = expand.hasInlineCount() && !expand.hasLimit();
CqnStructuredTypeRef ref = expand.ref();
Map mappingAliases = expandProcessor.getMappingAliases();
if (lazy) {
expandLazy(expand, ref, mappingAliases, rows, expandProcessor.getQueryHints());
} else {
expandEager(expand, ref, mappingAliases, addCount, rows, expandProcessor.isLoadSingle(), expandProcessor.getQueryHints());
}
}
private void expandEager(CqnExpand expand, CqnStructuredTypeRef ref, Map mappingAliases, boolean addCount,
List> rows, boolean enforceLoadSingle, Map queryHints) {
boolean singleValued = singleValued((CdsEntity) root, expand.ref());
CqnSelect query = queryByParams(ref, expand.items(), expand.orderBy(), expand.top(), expand.skip(), mappingAliases, queryHints);
String path = expand.alias().orElse(ref.lastSegment());
if (rows.size() == 1 || enforceLoadSingle) {
rows.forEach(row -> loadSingle(row, query, path, singleValued, addCount));
} else if (singleValued) {
loadBulk(rows, query, mappingAliases, (row, list) -> putOne(row, path, list));
} else if (!expand.hasLimit()) {
loadBulk(rows, query, mappingAliases, (row, list) -> putMany(row, path, list, addCount));
} else {
rows.forEach(row -> loadSingle(row, query, path, singleValued, addCount));
}
}
private void loadSingle(Map row, CqnSelect query, String path, boolean singleValued,
boolean addCount) {
Result result = dataStore.execute(query, row);
if (singleValued) {
Row map = result.first().orElse(null);
DataUtils.putPath(row, path, map, map != null);
} else {
List list = result.list();
putMany(row, path, list, addCount);
}
}
private static interface Mapper {
void map(Map row, List list);
}
private void loadBulk(List> rows, CqnSelect query, Map mappingAliases, Mapper mapper) {
Map, List> keyToData = execAndGroupByParentKeys(rows, query);
for (Map row : rows) {
List key = keyValuesInResultData(row, mappingAliases);
List list = keyToData.getOrDefault(key, Collections.emptyList());
list.forEach(r -> clean(r));
mapper.map(row, list);
}
}
private void putOne(Map row, String path, List list) {
Map map = switch (list.size()) {
case 0 -> null;
case 1 -> clean(list.get(0));
default -> throw new CdsDataStoreException("Failed to map result of expand " + path);
};
DataUtils.putPath(row, path, map, map != null);
}
private void putMany(Map row, String path, List extends Map> list,
boolean addCount) {
DataUtils.putPath(row, path, list, !list.isEmpty());
if (addCount) {
DataUtils.putPath(row, DataUtils.countName(path), Long.valueOf(list.size()));
}
}
private Map, List> execAndGroupByParentKeys(List> rows, CqnSelect query) {
keyNames.forEach( // TODO: Adds the parent keys: T0.ID as '@@ID'
k -> {
String el = pk2Element.get(k); // root PK in path mode (JOIN), FK in direct mode
String columnName = sqlMapping.columnName(el);
CqnSelectListValue slv = CQL.plain(QatBuilder.ROOT_ALIAS + "." + columnName).as(pkAlias(k));
((SelectBuilder>) query).addItem(slv);
});
int maxBatchSize = query.orderBy().isEmpty() ? 100 : rows.size();
Result result = dataStore.execute(query, rows, maxBatchSize);
return result.stream().collect(groupingBy(this::keyValuesInResultRow));
}
private static String pkAlias(String k) {
return PK_PREFIX + k;
}
private List keyValuesInResultRow(Map row) {
return keyNames.stream().map(k -> row.get(pkAlias(k))).toList();
}
private List keyValuesInResultData(Map row, Map aliases) {
return keyNames.stream().map(k -> row.get(aliases.getOrDefault(k, k))).toList();
}
private static Map clean(Map r) {
r.keySet().removeIf(k -> k.startsWith(PK_PREFIX));
return r;
}
private void expandLazy(CqnExpand expand, CqnStructuredTypeRef ref, Map mappingAliases,
List> rows, Map queryHints) {
for (Map row : rows) {
injector(row, mappingAliases, queryHints).injectInto(row, ref, expand.items(), expand.orderBy(), expand.top(),
expand.skip(), expand.alias());
}
}
private LazyAssociationLoaderInjector injector(Map row, Map mappingAliases, Map queryHints) {
Map pkValues = new HashMap<>();
root.keyElements().forEach(k -> {
String keyName = k.getName();
String displayName = mappingAliases.getOrDefault(keyName, keyName);
pkValues.put(keyName, row.get(displayName));
});
return new LazyAssociationLoaderInjector((CdsEntity) root, pkValues, queryHints);
}
private class LazyAssociationLoaderInjector {
private CdsEntity entity;
private Map keyValues;
private Map queryHints;
LazyAssociationLoaderInjector(CdsEntity entity, Map keyValues, Map queryHints) {
this.entity = entity;
this.keyValues = keyValues;
this.queryHints = queryHints;
}
private void injectInto(Map row, CqnStructuredTypeRef path, List slis,
List orderBy, long top, long skip, Optional alias) {
CqnSelect query = queryByValues(path, slis, orderBy, top, skip, keyValues);
Lazy loader = singleValued(entity, path) ? lazyRow(dataStore, query) : new LazyResultImpl(dataStore, query);
String displayName = alias.orElse(path.lastSegment());
DataUtils.putPath(row, displayName, loader);
}
private CqnSelect queryByValues(CqnStructuredTypeRef path, List slis,
List orderBy, long top, long skip, Map keyValues) {
if (!keyValues.keySet().containsAll(concreteKeyNames(root))) {
throw new CdsException("Missing key values for entity " + root.getQualifiedName()
+ ". Please add all keys to the projection.");
}
List segments = new ArrayList<>();
segments.add(refSegment(root.getQualifiedName(), matching(keyValues)));
segments.addAll(path.segments());
return SelectBuilder.from(typeRef(segments, null)).columns(slis).orderBy(orderBy).limit(top, skip).hints(queryHints);
}
}
private boolean singleValued(CdsEntity entity, CqnStructuredTypeRef path) {
CdsEntity e = entity;
CdsElement association = null;
for (Segment seg : path.segments()) {
String assocName = seg.id();
association = e.getAssociation(assocName);
e = e.getTargetOf(assocName);
}
if (association == null) {
throw new CdsException(
"Missing association for Entity " + e.getName() + ", under Path " + path.toJson() + ".");
}
return isSingleValued(association.getType());
}
private CqnSelect queryByParams(CqnStructuredTypeRef path, List items,
List orderBy, long top, long skip, Map aliases, Map queryHints) {
var target = fkMapping(path)
.map(m -> filteredTarget(m, path, aliases)) // no inner join
.orElseGet(() -> pathFromRoot(path, aliases)); // inner join
return Select.from(target).columns(items).orderBy(orderBy).limit(top, skip).hints(queryHints);
}
private Optional> fkMapping(CqnStructuredTypeRef path) {
if (path.size() != 1) {
return Optional.empty();
}
CdsElement assoc = CdsModelUtils.element(root, path.segments());
if (!CdsModelUtils.isReverseAssociation(assoc)) {
return Optional.empty();
}
OnConditionAnalyzer analyzer = new OnConditionAnalyzer(assoc, true);
Map fkMapping;
try {
fkMapping = analyzer.getFkMapping();
} catch (UnsupportedOperationException ex) {
return Optional.empty(); // on-condition contains OR or NOT
}
Map fk2Pk = new HashMap<>(fkMapping.size());
Set referencedPks = new HashSet<>();
fkMapping.forEach((fk, pkVal) -> {
if (pkVal.isRef() && pkVal.asRef().size() == 1) {
String pk = pkVal.asRef().firstSegment();
fk2Pk.put(fk, pk);
referencedPks.add(pk);
}
});
if (referencedPks.size() != keyNames.size()) {
return Optional.empty();
}
if (keyNames.stream().anyMatch(k -> !referencedPks.contains(k))) {
return Optional.empty();
}
return Optional.of(fk2Pk);
}
private CqnStructuredTypeRef pathFromRoot(CqnStructuredTypeRef path, Map aliases) {
List segments = new ArrayList<>(path.segments().size() + 1);
pk2Element.clear();
keyNames.forEach(k -> pk2Element.put(k, k));
segments.add(refSegment(root.getQualifiedName(), pkFilter(aliases)));
segments.addAll(path.segments());
return typeRef(segments);
}
private CqnStructuredTypeRef filteredTarget(Map fkMapping, CqnStructuredTypeRef ref, Map aliases) {
Map fkAliases = new HashMap<>();
pk2Element.clear();
fkMapping.forEach((fk, pk) -> {
String paramName = aliases.get(pk);
fkAliases.put(fk, paramName);
pk2Element.put(pk, fk);
});
CdsStructuredType targetEntity = CdsModelUtils.target(root, ref.segments());
CqnPredicate filter = CQL.and(pkFilter(fkAliases), ref.rootSegment().filter().orElse(CQL.TRUE));
return typeRef(refSegment(targetEntity.getQualifiedName(), filter));
}
private CqnPredicate pkFilter(Map aliases) {
return switch (keyNames.size()) {
case 0 -> CQL.TRUE;
case 1 -> pkFilterSingle(aliases);
default -> pkFilterList(aliases);
};
}
private CqnPredicate pkFilterSingle(Map pkAliases) {
String pk = keyNames.get(0);
String el = pk2Element.get(pk);
return CQL.comparison(CQL.get(el), EQ, CQL.param(pkAliases.getOrDefault(el, el)));
}
private CqnPredicate pkFilterList(Map aliases) {
int n = keyNames.size();
List fkRefs = new ArrayList<>(n);
List pkParams = new ArrayList<>(n);
keyNames.forEach(pk -> {
String el = pk2Element.get(pk);
fkRefs.add(CQL.get(el));
pkParams.add(CQL.param(aliases.getOrDefault(el, el)));
});
return CQL.comparison(CQL.list(fkRefs), EQ, CQL.list(pkParams));
}
}