com.sap.cds.services.impl.persistence.JdbcPersistenceService Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cds-feature-jdbc Show documentation
Show all versions of cds-feature-jdbc Show documentation
Consuming JDBC persistences using the CDS4j JDBC runtime
/**************************************************************************
* (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
**************************************************************************/
package com.sap.cds.services.impl.persistence;
import java.sql.Connection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import com.sap.cds.CdsDataStore;
import com.sap.cds.CdsDataStoreConnector;
import com.sap.cds.CdsException;
import com.sap.cds.CdsLockTimeoutException;
import com.sap.cds.NotNullConstraintException;
import com.sap.cds.Result;
import com.sap.cds.UniqueConstraintException;
import com.sap.cds.ql.CdsDataException;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnStatement;
import com.sap.cds.ql.cqn.CqnValidationException;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.services.EventContext;
import com.sap.cds.services.cds.CdsCreateEventContext;
import com.sap.cds.services.cds.CdsDeleteEventContext;
import com.sap.cds.services.cds.CdsReadEventContext;
import com.sap.cds.services.cds.CdsUpdateEventContext;
import com.sap.cds.services.cds.CdsUpsertEventContext;
import com.sap.cds.services.cds.CqnService;
import com.sap.cds.services.changeset.ChangeSetContext;
import com.sap.cds.services.changeset.ChangeSetContextSPI;
import com.sap.cds.services.changeset.ChangeSetListener;
import com.sap.cds.services.handler.annotations.Before;
import com.sap.cds.services.handler.annotations.HandlerOrder;
import com.sap.cds.services.handler.annotations.On;
import com.sap.cds.services.persistence.PersistenceService;
import com.sap.cds.services.request.RequestContext;
import com.sap.cds.services.runtime.CdsRuntime;
import com.sap.cds.services.transaction.ChangeSetMemberDelegate;
import com.sap.cds.services.transaction.TransactionManager;
import com.sap.cds.services.utils.CdsErrorStatuses;
import com.sap.cds.services.utils.ErrorStatusException;
import com.sap.cds.services.utils.OpenTelemetryUtils;
import com.sap.cds.services.utils.OrderConstants;
import com.sap.cds.services.utils.SessionContextUtils;
import com.sap.cds.services.utils.StringUtils;
import com.sap.cds.services.utils.TenantAwareCache;
import com.sap.cds.services.utils.model.CdsModelUtils;
import com.sap.cds.services.utils.model.CqnUtils;
import com.sap.cds.services.utils.services.AbstractCqnService;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
public class JdbcPersistenceService extends AbstractCqnService implements PersistenceService {
private static final Logger logger = LoggerFactory.getLogger(JdbcPersistenceService.class);
private final TenantAwareCache cachedConnectors;
private final Map cachedDataStores = new ConcurrentHashMap<>();
private final TransactionManager txMgr;
public JdbcPersistenceService(String name, Supplier connectionSupplier, TransactionManager txMgr, CdsRuntime runtime) {
super(name, runtime);
this.txMgr = txMgr;
com.sap.cds.transaction.TransactionManager cds4jTxMgr = new com.sap.cds.transaction.TransactionManager() {
@Override
public boolean isActive() {
ChangeSetContextSPI changeSetContext = (ChangeSetContextSPI) ChangeSetContext.getCurrent();
boolean activeChangeSet = changeSetContext != null
&& changeSetContext.hasChangeSetMember(txMgr.getName());
boolean activeTxMgr = txMgr.isActive();
return activeChangeSet || activeTxMgr;
}
@Override
public void setRollbackOnly() {
ChangeSetContext changeSetContext = ChangeSetContext.getCurrent();
if (changeSetContext != null) {
// this directly goes to the ChangeSet, instead of using the transaction manager
changeSetContext.markForCancel();
} else {
txMgr.setRollbackOnly();
}
}
};
this.cachedConnectors = TenantAwareCache.create(() -> {
return CdsDataStoreConnector.createJdbcConnector(RequestContext.getCurrent(runtime).getModel(), cds4jTxMgr)
.connection(connectionSupplier)
.config(new JdbcDataStoreConfiguration(runtime.getEnvironment().getCdsProperties())).build();
}, runtime);
}
@Before
@HandlerOrder(OrderConstants.Before.TRANSACTION_BEGIN)
protected void ensureTransaction(EventContext context) {
// register and begin transaction
ChangeSetContextSPI changeSetContext = (ChangeSetContextSPI) context.getChangeSetContext();
if (!changeSetContext.isMarkedTransactional() && requiresTransaction(context)) {
changeSetContext.markTransactional();
}
if (changeSetContext.isMarkedTransactional() && !changeSetContext.hasChangeSetMember(txMgr.getName())) {
changeSetContext.register(new ChangeSetMemberDelegate(txMgr));
try {
txMgr.begin();
} catch (Exception e) { // NOSONAR
throw new ErrorStatusException(CdsErrorStatuses.TRANSACTION_INITIALIZATION_FAILED, e);
}
}
try {
updateSessionContext(context);
} catch (CdsException e) {
logger.warn("Not supported to set the locale on the connection session", e);
}
}
private boolean requiresTransaction(EventContext context) {
// TODO media streams are currently not yet detected, but also require transactions
if (context.getEvent().equals(CqnService.EVENT_READ)) {
CqnSelect cqn = context.as(CdsReadEventContext.class).getCqn();
if (cqn != null && !cqn.getLock().isPresent()) {
// only READ events without locks don't require transactions
return false;
}
}
return true;
}
@On
@HandlerOrder(OrderConstants.On.DEFAULT_ON)
protected Result defaultRead(CdsReadEventContext context) {
return checkExceptionAndOtelSpan(() -> getCdsDataStore().execute(context.getCqn(), context.getCqnNamedValues()), "SELECT", context);
}
@On
@HandlerOrder(OrderConstants.On.DEFAULT_ON)
protected Result defaultCreate(CdsCreateEventContext context) {
return checkExceptionAndOtelSpan(() -> getCdsDataStore().execute(context.getCqn()), "INSERT", context);
}
@On
@HandlerOrder(OrderConstants.On.DEFAULT_ON)
protected Result defaultUpsert(CdsUpsertEventContext context) {
return checkExceptionAndOtelSpan(() -> getCdsDataStore().execute(context.getCqn()), "UPSERT", context);
}
@On
@HandlerOrder(OrderConstants.On.DEFAULT_ON)
protected Result defaultUpdate(CdsUpdateEventContext context) {
return checkExceptionAndOtelSpan(() -> getCdsDataStore().execute(context.getCqn(), context.getCqnValueSets()), "UPDATE", context);
}
@On
@HandlerOrder(OrderConstants.On.DEFAULT_ON)
protected Result defaultDelete(CdsDeleteEventContext context) {
return checkExceptionAndOtelSpan(() -> getCdsDataStore().execute(context.getCqn(), context.getCqnValueSets()), "DELETE", context);
}
public CdsDataStore getCdsDataStore() {
if(!ChangeSetContext.isActive()) {
return cachedConnectors.findOrCreate().connect();
}
ChangeSetContext context = ChangeSetContext.getCurrent();
String currentTenant = RequestContext.getCurrent(runtime).getUserInfo().getTenant();
CachedCdsDataStore cachedDataStore = cachedDataStores.get(context);
if(cachedDataStore == null) {
cachedDataStore = new CachedCdsDataStore(cachedConnectors.findOrCreate().connect(), currentTenant);
context.register(new ChangeSetListener(){
@Override
public void afterClose(boolean completed) {
cachedDataStores.remove(context);
}
});
cachedDataStores.put(context, cachedDataStore);
} else if(!Objects.equals(cachedDataStore.getTenant(), currentTenant)) {
// cached data stores and their cached (by means of transactions) JDBC connections are tenant-dependant
// we implement this check to ensure that we don't switch tenants on an existing transaction
// otherwise this could lead to data leaking from one tenant to another
throw new ErrorStatusException(CdsErrorStatuses.TRANSACTION_TENANT_MISMATCH, cachedDataStore.getTenant(), currentTenant);
}
return cachedDataStore.getCdsDataStore();
}
@VisibleForTesting
Result checkExceptionAndOtelSpan(Supplier supplier, String cqnOperation, EventContext context) {
CqnStatement cqnStatement = (CqnStatement) context.get("cqn");
updateSessionContext(context);
Optional span = OpenTelemetryUtils.createSpan(OpenTelemetryUtils.CdsSpanType.CQN);
try (Scope scope = span.map(Span::makeCurrent).orElse(null)) {
OpenTelemetryUtils.updateSpan(span, runtime, cqnOperation, context.getTarget(), cqnStatement, "sql");
return supplier.get();
} catch (UniqueConstraintException e) {
throw new ErrorStatusException(CdsErrorStatuses.UNIQUE_CONSTRAINT_VIOLATED, e.getEntityName(), e);
} catch (NotNullConstraintException e) {
throw new ErrorStatusException(CdsErrorStatuses.VALUE_REQUIRED,
StringUtils.stringifyList(e.getElementNames()), e.getEntityName(), e);
} catch (CdsLockTimeoutException e) {
throw new ErrorStatusException(CdsErrorStatuses.LOCK_TIMEOUT, e.getDefinition().getQualifiedName(), e);
} catch (CqnValidationException | CdsDataException e) {
throw new ErrorStatusException(CdsErrorStatuses.INVALID_CQN, e.getMessage(), e); // TODO CDS4J support required
}
finally {
OpenTelemetryUtils.endSpan(span);
}
}
private void updateSessionContext(EventContext context) {
getCdsDataStore().setSessionContext(SessionContextUtils.toSessionContext(context));
}
@Override
protected String getTargetEntity(CqnStatement statement) {
CdsModel model = RequestContext.getCurrent(runtime).getModel();
return CdsModelUtils.getEntityPath(
CqnUtils.getTargetRef(statement, true), model).target().type().getQualifiedName();
}
private static class CachedCdsDataStore {
private final CdsDataStore cdsDataStore;
private final String tenant;
public CachedCdsDataStore(CdsDataStore cdsDataStore, String tenant) {
this.cdsDataStore = cdsDataStore;
this.tenant = tenant;
}
public CdsDataStore getCdsDataStore() {
return cdsDataStore;
}
public String getTenant() {
return tenant;
}
}
}