In the previous blog, we saw how tenant onboarding works, tenant-specific schema creation and dependency injection.
In this blog we are going to focus on the following topics:
- Accessing Tenant Specific Data and improving the performance of data access from the database.
- How to access destination data coming S/4HANA Cloud/on-premise system from the subscriber subaccount in a multitenant application.
Accessing Tenant Specific Data and improving the performance of data access from the database.
The recommendation is to use schema-based data isolation when building multitenant applications. Here’s what the schema-based data isolation looks like:
During the onboarding, we created HDI-Containers for each of the subscribing tenants of our application using Service Manager Service on BTP.
The Service Manager acts as a controller under which all the tenant-specific schema and service keys are created. Here’s a quick view of it:
To establish a connection with the database we need tenant-specific service keys.
Accessing Tenant-Specific HDI-Container Service keys and Credentials caching
To access the service keys we can either use the service manager’s APIs with client credentials which needs multiple roundtrips to the service manager before we get the service keys and repeated the same for each request or we can store the credentials in the application runtime for a given period of time which reduces the round trip to the service manager every time.
We are using a package @sap/instance-manager
@sap/instance-manager
Node.js package for creating and deleting service instances per tenant within an application at runtime.
This package provides a client for Instance Manager, Service Manager and Migration Manager – components that create and delete service instances (via REST API) for a specified key. These components can be used in the context of multitenant applications where the tenant id is the key an instance is associated with.
Multitenancy is a concept of sharing resources between several different and unrelated to each other groups of users called tenants. Example: subscriptions to a commercial cloud application can be sold to two different companies each of which should use the application in isolation from the other one. Customizations are also applied (e.g. different branding, identity providers, database schemas etc.).
Implementation
const createInstanceManager = require('@sap/instance-manager');
const xsenv = require('@sap/xsenv');
const hanaClient = require('@sap/hana-client');
const smCredentials = xsenv.getServices({
sm: {name: 'es-sm'},
});
let options = smCredentials.sm;
// to search for HDI-Containers created under service manager
options = Object.assign(options, {service: 'hana', plan: 'hdi-shared'});
/**
* Returns HDI connection Object
* @param {string} tenantId - tenant id
* @param {object} logger - logger object
* @returns {Object} - HDI connection Object
*/
const getHDICredentials = async (tenantId, logger) => {
console.log(tenantId);
return new Promise((resolve, reject) => {
try {
createInstanceManager.create(options, (err, instanceManager) => {
if (err) {
logger.error('Error in getting Connection Manager', err);
reject(err);
} else {
instanceManager.get(tenantId, (err1, credentials) => {
try {
if (err1) {
logger.error('Error in getting Connection Credentials', err1);
reject(err1);
} else {
if (credentials) {
const conn = hanaClient.createConnection();
conn.connect(credentials.credentials, (err2) => {
if (err2) {
logger.error('Error in connecting to HDI container', err2);
reject(err2);
} else {
logger.info(`HDI connection established for tenant ${tenantId}`);
console.timeEnd('connectionpool');
conn.exec(`SET SCHEMA ${credentials.credentials.schema}`, (err3, result)=>{
if (err3) {
logger.error('Error in setting schema', err3);
reject(err3);
} else {
resolve(conn);
}
});
}
});
} else {
reject(new Error(`No credentials found for tenant:: ${tenantId}`));
}
}
} catch (e) {
reject(e);
}
});
}
});
} catch (error) {
logger.error('Error in getting Connection Manager', error);
reject(error);
}
});
};
This creates and returns a connection object.
How to access destination data coming S/4HANA Cloud/on-premise system from the subscriber subaccount in a multitenant application
Here we will see how to use SAP Cloud SDK to access S/4HANA System from the subscriber subaccount. The connection uses a connectivity service and destination service to fetch data from the on-premise system and destination service in case it is a Cloud System.
Here the section highlighted in red is where we have multiple subaccounts and each subaccount is linked to its own S/4HANA system and we need to isolate the requests for requesting tenants.
Implementation
We are using SAP Cloud SDK
The request comes after the user has logged in, and the requested backend gets has a JWT token. Here’s how it looks:
Business Partner Service Implementation.
const {businessPartnerService} = require('@sap/cloud-sdk-op-vdm-business-partner-service');
const {businessPartnerApi} = businessPartnerService();
/**
* Get Business Partner
* @param {string} token JWT TOKEN with tenant ID to isolate tenant specific Destinations
* @return {Promise<any>}
*/
const getBusinessPartner = async (token) => {
// TODO: filter for identifying new business partners
const resultPromise = await businessPartnerApi.requestBuilder().getAll().filter(businessPartnerApi.schema.SEARCH_TERM_1.equals(`${process.env.searchTerm}`))
.execute({destinationName: 'bupa', jwt: token}, {useCache: true});
return resultPromise;
};
Here bupa is the destination name in the subscriber subaccount and the token is the JWT token generated using the subscriber subaccount subdomain and client-id and the secret of the destination service.
Destination Service Cache
To reduce the number of calls to the destination service, you can enable the destination cache. All the discussed options apply to the execute()
and getDestination()
methods of the SAP Cloud SDK. The destination caching is disabled by default and you switch it on by the useCache
flag:
.execute({ destinationName: 'myDestination', jwt: 'yourJWT', useCache: true })
A cached destination is stored for 5 minutes in the cache. The SAP Cloud SDK tries to set the proper isolation strategy for the destination automatically. There are two options:
Tenant
: The destination is cached for a tenant and different users will hit the same cache entry.TenantUser
: The destination is cached separately for each user of a tenant.
The SAP Cloud SDK sets the isolation based on the provided JWT:
- No JWT: Isolation is set to
Tenant
and the value for the tenant is the provider account. - JWT without
user_id
: Isolation is set toTenant
and the value for the tenant is thezid
of the JWT. - JWT with
user_id
: Isolation is set toTenantUser
and values are taken fromzid
anduser_id
.
With this, our application is ready, One last piece.
Part 4: Use multi-tenant capabilities of SAP Job Scheduling Service to schedule tenant-specific jobs to pull data from the S/4HANA system
Further Read:
Other multitenant applications and Missions: