Multitenancy ermöglicht es, eine ABAP Cloud-Anwendung für mehrere Kunden (Tenants) zu betreiben, ohne den Code mehrfach zu deployen. Für SaaS-Anbieter ist dies das Fundament für skalierbare und kosteneffiziente Lösungen.
Was ist Multitenancy?
In einer mandantenfähigen Architektur teilen sich mehrere Kunden dieselbe Anwendungsinstanz, während ihre Daten strikt voneinander getrennt bleiben:
| Begriff | Beschreibung |
|---|---|
| Tenant | Ein Kunde oder Mandant mit eigenen Daten |
| Provider Account | SAP BTP-Account des SaaS-Anbieters |
| Consumer Subaccount | BTP-Subaccount eines SaaS-Kunden |
| Tenant ID | Eindeutiger Identifier für jeden Mandanten |
| Tenant-aware | Code, der den aktuellen Tenant berücksichtigt |
Multitenancy-Modelle auf SAP BTP
| Modell | Beschreibung | Anwendungsfall |
|---|---|---|
| Shared Schema | Alle Tenants in einer Datenbank-Instanz | Standard für ABAP Cloud |
| Separate Schema | Eigenes DB-Schema pro Tenant | Strenge Isolation |
| Separate Instance | Eigene ABAP-Instanz pro Tenant | Maximale Isolation |
ABAP Cloud nutzt primär das Shared Schema-Modell mit mandantenabhängigen Tabellen. Die Trennung erfolgt über das Feld CLIENT (Mandant).
Architektur einer Multitenant-SaaS-Anwendung
┌─────────────────────────────────────────────────────────────────────────────┐│ SAP BTP Global Account ││ ││ ┌────────────────────────────────────────────────────────────────────────┐ ││ │ Provider Subaccount │ ││ │ ┌────────────────────┐ ┌─────────────────────────────────────────┐ │ ││ │ │ ABAP Cloud System │ │ SaaS Provisioning Service │ │ ││ │ │ (Multitenant) │ │ - Tenant Onboarding │ │ ││ │ │ │ │ - Tenant Offboarding │ │ ││ │ │ - Shared Code │ │ - Lifecycle Callbacks │ │ ││ │ │ - Shared Tables │ └─────────────────────────────────────────┘ │ ││ │ │ - Mandant pro │ │ ││ │ │ Tenant │ ┌─────────────────────────────────────────┐ │ ││ │ └────────────────────┘ │ XSUAA Service │ │ ││ │ │ - Tenant-spezifische JWT-Tokens │ │ ││ └────────────────────────────┴─────────────────────────────────────────┘─┘ ││ ││ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ ││ │ Consumer │ │ Consumer │ │ Consumer │ ││ │ Subaccount A │ │ Subaccount B │ │ Subaccount C │ ││ │ (Kunde Alpha) │ │ (Kunde Beta) │ │ (Kunde Gamma) │ ││ │ │ │ │ │ │ ││ │ Mandant: 100 │ │ Mandant: 200 │ │ Mandant: 300 │ ││ └──────────────────┘ └──────────────────┘ └──────────────────┘ │└─────────────────────────────────────────────────────────────────────────────┘Kernkomponenten
| Komponente | Funktion |
|---|---|
| ABAP System | Multitenant-fähiges ABAP Environment |
| SaaS Provisioning Service | Verwaltet Tenant-Lifecycle |
| XSUAA | Authentifizierung und Autorisierung pro Tenant |
| Destination Service | Tenant-spezifische Verbindungskonfigurationen |
Tenant Context in ABAP Cloud
Der aktuelle Tenant-Kontext ist über verschiedene APIs verfügbar:
Tenant-ID auslesen
CLASS zcl_tenant_utils DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. CLASS-METHODS: get_current_tenant_id RETURNING VALUE(rv_tenant_id) TYPE string,
get_current_tenant_zone_id RETURNING VALUE(rv_zone_id) TYPE string,
is_provider_tenant RETURNING VALUE(rv_is_provider) TYPE abap_bool.
ENDCLASS.
CLASS zcl_tenant_utils IMPLEMENTATION.
METHOD get_current_tenant_id. " Tenant-ID aus dem Security Context DATA(lo_context) = cl_abap_context_info=>get_system_context( ). rv_tenant_id = lo_context->get_attribute( 'zoneId' ). ENDMETHOD.
METHOD get_current_tenant_zone_id. " Zone-ID entspricht der Subaccount-GUID TRY. DATA(lo_context) = cl_abap_context_info=>get_system_context( ). rv_zone_id = lo_context->get_attribute( 'zoneId' ). CATCH cx_abap_context_info_error. " Fallback: leerer String rv_zone_id = ''. ENDTRY. ENDMETHOD.
METHOD is_provider_tenant. " Prüfen ob aktueller Tenant der Provider ist DATA(lv_zone_id) = get_current_tenant_zone_id( ). DATA(lv_provider_zone) = cl_abap_context_info=>get_system_context( )->get_attribute( 'providerZoneId' ).
rv_is_provider = xsdbool( lv_zone_id = lv_provider_zone ). ENDMETHOD.
ENDCLASS.Mandant-abhängige Daten
In ABAP Cloud werden Tenant-Daten über den Mandanten (CLIENT) getrennt. Jeder Tenant erhält einen eigenen Mandanten:
CLASS zcl_tenant_data DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. METHODS: get_tenant_specific_config RETURNING VALUE(rs_config) TYPE zconfig_s.
ENDCLASS.
CLASS zcl_tenant_data IMPLEMENTATION.
METHOD get_tenant_specific_config. " Standard-Select liest automatisch mandantenabhängig " Der Mandant wird vom System basierend auf dem JWT-Token gesetzt SELECT SINGLE * FROM ztenant_config INTO rs_config.
" Keine WHERE-Klausel für CLIENT nötig - das passiert automatisch ENDMETHOD.
ENDCLASS.Tenant-aware CDS Views
CDS Views sind automatisch mandantenabhängig, wenn die zugrundeliegende Datenbanktabelle mandantenabhängig ist:
Standard Mandanten-Verhalten
@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Tenant-spezifische Produkte'define view entity ZI_Product as select from zproduct{ key product_id as ProductId, product_name as ProductName, product_price as ProductPrice, created_by as CreatedBy, created_at as CreatedAt}Die WHERE-Klausel WHERE mandt = sy-mandt wird vom System automatisch hinzugefügt. Jeder Tenant sieht nur seine eigenen Daten.
Cross-Client Zugriff für Provider
In bestimmten Fällen muss der Provider auf Daten aller Tenants zugreifen (z.B. für Reporting oder Administration):
@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Alle Produkte (Cross-Client)'@ClientHandling.type: #CLIENT_INDEPENDENTdefine view entity ZI_ProductCrossClient as select from zproduct{ key mandt as Client, key product_id as ProductId, product_name as ProductName, tenant_id as TenantId}Wichtig: Cross-Client Views erfordern besondere Berechtigungsprüfungen, um Datenlecks zu vermeiden.
Berechtigungsprüfung für Cross-Client Views
@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Provider-Reporting View'define view entity ZI_ProviderReporting as select from zproduct association [1..1] to ZI_TenantMaster as _Tenant on $projection.TenantId = _Tenant.TenantId{ key mandt as Client, key product_id as ProductId, product_name as ProductName, tenant_id as TenantId, _Tenant}where -- Nur für Provider-Tenant sichtbar $session.system_date is not nullMit Access Control:
@EndUserText.label: 'Provider Reporting DCL'@MappingRole: truedefine role ZR_ProviderReporting { grant select on ZI_ProviderReporting where ( ) = aspect pfcg_auth ( Z_PROVIDER, ACTVT = '03' );}Lifecycle Management
Das Tenant-Lifecycle-Management umfasst Onboarding (neue Kunden aufnehmen) und Offboarding (Kunden entfernen).
SaaS Provisioning Service
Der SaaS Provisioning Service ruft bei Tenant-Ereignissen Callbacks auf:
┌─────────────────────────────────────────────────────────────────┐│ Tenant Lifecycle ││ ││ ┌──────────┐ ┌─────────────┐ ┌─────────────────────────┐ ││ │ Subscribe│───>│ SaaS │───>│ ABAP Cloud │ ││ │ (Kunde) │ │ Provisioning│ │ Onboarding Callback │ ││ └──────────┘ └─────────────┘ │ - Mandant anlegen │ ││ │ - Initiale Daten laden │ ││ │ - Berechtigungen setup │ ││ └─────────────────────────┘ ││ ││ ┌────────────┐ ┌─────────────┐ ┌─────────────────────────┐ ││ │ Unsubscribe│─>│ SaaS │───>│ ABAP Cloud │ ││ │ (Kunde) │ │ Provisioning│ │ Offboarding Callback │ ││ └────────────┘ └─────────────┘ │ - Daten archivieren │ ││ │ - Mandant deaktivieren │ ││ └─────────────────────────┘ │└─────────────────────────────────────────────────────────────────┘Onboarding Callback implementieren
CLASS zcl_tenant_provisioning DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES if_saas_provisioning_callback.
ENDCLASS.
CLASS zcl_tenant_provisioning IMPLEMENTATION.
METHOD if_saas_provisioning_callback~on_subscription. " Wird aufgerufen, wenn ein neuer Tenant abonniert DATA(lv_tenant_id) = iv_tenant_id. DATA(lv_subdomain) = iv_subdomain.
TRY. " 1. Initiale Konfiguration für den Tenant anlegen create_tenant_config( iv_tenant_id = lv_tenant_id iv_subdomain = lv_subdomain ).
" 2. Standard-Stammdaten laden load_initial_master_data( lv_tenant_id ).
" 3. Admin-Benutzer einrichten setup_tenant_admin( iv_tenant_id = lv_tenant_id iv_admin_email = is_subscription_params-admin_email ).
" Erfolg melden ev_status = if_saas_provisioning_callback=>cs_status-success.
CATCH cx_root INTO DATA(lx_error). " Fehler beim Onboarding ev_status = if_saas_provisioning_callback=>cs_status-failed. ev_message = lx_error->get_text( ). ENDTRY. ENDMETHOD.
METHOD if_saas_provisioning_callback~on_unsubscription. " Wird aufgerufen, wenn ein Tenant kündigt DATA(lv_tenant_id) = iv_tenant_id.
TRY. " 1. Daten für gesetzliche Aufbewahrung archivieren archive_tenant_data( lv_tenant_id ).
" 2. Aktive Sessions beenden terminate_tenant_sessions( lv_tenant_id ).
" 3. Tenant-Konfiguration deaktivieren deactivate_tenant( lv_tenant_id ).
ev_status = if_saas_provisioning_callback=>cs_status-success.
CATCH cx_root INTO DATA(lx_error). ev_status = if_saas_provisioning_callback=>cs_status-failed. ev_message = lx_error->get_text( ). ENDTRY. ENDMETHOD.
METHOD if_saas_provisioning_callback~get_dependencies. " Abhängigkeiten zu anderen SaaS-Services deklarieren " z.B. wenn dieser Service einen anderen SaaS-Service benötigt rt_dependencies = VALUE #( ). ENDMETHOD.
ENDCLASS.Tenant-spezifische Initialdaten
CLASS zcl_tenant_initializer DEFINITION PUBLIC FINAL CREATE PRIVATE.
PUBLIC SECTION. CLASS-METHODS: create IMPORTING iv_tenant_id TYPE string RETURNING VALUE(ro_instance) TYPE REF TO zcl_tenant_initializer.
METHODS: initialize_master_data, initialize_customizing, initialize_number_ranges.
PRIVATE SECTION. DATA mv_tenant_id TYPE string.
ENDCLASS.
CLASS zcl_tenant_initializer IMPLEMENTATION.
METHOD create. ro_instance = NEW #( ). ro_instance->mv_tenant_id = iv_tenant_id. ENDMETHOD.
METHOD initialize_master_data. " Standard-Stammdaten für jeden neuen Tenant DATA lt_default_categories TYPE STANDARD TABLE OF zcategory.
lt_default_categories = VALUE #( ( category_id = 'CAT01' name = 'Standard' description = 'Standardkategorie' ) ( category_id = 'CAT02' name = 'Premium' description = 'Premiumkategorie' ) ).
INSERT zcategory FROM TABLE lt_default_categories. ENDMETHOD.
METHOD initialize_customizing. " Tenant-spezifisches Customizing DATA(ls_config) = VALUE zconfig_s( tenant_id = mv_tenant_id currency = 'EUR' language = 'DE' date_format = 'DD.MM.YYYY' time_zone = 'CET' ).
INSERT ztenant_config FROM ls_config. ENDMETHOD.
METHOD initialize_number_ranges. " Nummernkreise pro Tenant initialisieren " In ABAP Cloud über Nummernkreis-Objekte DATA(lo_nr_runtime) = cl_numberrange_runtime=>create( iv_object = 'ZORDER' ).
" Intervall für den Tenant anlegen lo_nr_runtime->create_interval( is_interval = VALUE #( nrrangenr = '01' fromnumber = '0000000001' tonumber = '9999999999' procind = 'I' " Interner Nummernkreis ) ). ENDMETHOD.
ENDCLASS.Tenant-spezifische Berechtigungen
Jeder Tenant hat eigene Berechtigungsrollen:
IAM Konzept für Multitenancy
┌───────────────────────────────────────────────────────────────────┐│ Berechtigungsarchitektur ││ ││ Provider: ││ ┌─────────────────────────────────────────────────────────────┐ ││ │ IAM Business Catalog (Provider) │ ││ │ - Z_PROVIDER_ADMIN: Alle Tenants verwalten │ ││ │ - Z_PROVIDER_SUPPORT: Tenant-Support │ ││ │ - Z_PROVIDER_BILLING: Abrechnungszugriff │ ││ └─────────────────────────────────────────────────────────────┘ ││ ││ Consumer (pro Tenant): ││ ┌─────────────────────────────────────────────────────────────┐ ││ │ IAM Business Catalog (Tenant) │ ││ │ - Z_TENANT_ADMIN: Eigenen Tenant verwalten │ ││ │ - Z_TENANT_USER: Standardnutzer │ ││ │ - Z_TENANT_VIEWER: Nur Lesezugriff │ ││ └─────────────────────────────────────────────────────────────┘ │└───────────────────────────────────────────────────────────────────┘Tenant-Isolation in RAP
CLASS lhc_product DEFINITION INHERITING FROM cl_abap_behavior_handler. PRIVATE SECTION. METHODS: authorize_create FOR VALIDATE ON SAVE IMPORTING keys FOR Product~authorizeCreate,
check_tenant_access FOR VALIDATE ON SAVE IMPORTING keys FOR Product~checkTenantAccess.
ENDCLASS.
CLASS lhc_product IMPLEMENTATION.
METHOD authorize_create. " Standard-Berechtigungsprüfung READ ENTITIES OF ZI_Product IN LOCAL MODE ENTITY Product ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_products).
LOOP AT lt_products INTO DATA(ls_product). " Berechtigungsobjekt für Create prüfen AUTHORITY-CHECK OBJECT 'Z_PRODUCT' ID 'ACTVT' FIELD '01'.
IF sy-subrc <> 0. APPEND VALUE #( %tky = ls_product-%tky %msg = NEW zcx_product_auth( severity = if_abap_behv_message=>severity-error ) ) TO failed-product. ENDIF. ENDLOOP. ENDMETHOD.
METHOD check_tenant_access. " Zusätzliche Tenant-Prüfung für Cross-Tenant-Szenarien " Nur relevant wenn Provider auf Tenant-Daten zugreift
DATA(lv_current_tenant) = zcl_tenant_utils=>get_current_tenant_id( ). DATA(lv_is_provider) = zcl_tenant_utils=>is_provider_tenant( ).
READ ENTITIES OF ZI_Product IN LOCAL MODE ENTITY Product ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_products).
LOOP AT lt_products INTO DATA(ls_product). " Provider darf alle Daten sehen IF lv_is_provider = abap_true. CONTINUE. ENDIF.
" Consumer darf nur eigene Tenant-Daten ändern IF ls_product-TenantId <> lv_current_tenant. APPEND VALUE #( %tky = ls_product-%tky %msg = NEW zcx_tenant_violation( ) ) TO failed-product. ENDIF. ENDLOOP. ENDMETHOD.
ENDCLASS.Tenant-spezifische Konfiguration
Customizing pro Tenant
CLASS zcl_tenant_customizing DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. METHODS: get_config RETURNING VALUE(rs_config) TYPE zconfig_s,
get_feature_flags RETURNING VALUE(rt_flags) TYPE zfeature_flags_t,
is_feature_enabled IMPORTING iv_feature TYPE string RETURNING VALUE(rv_enabled) TYPE abap_bool.
PRIVATE SECTION. DATA ms_config TYPE zconfig_s. DATA mt_feature_flags TYPE zfeature_flags_t. DATA mv_loaded TYPE abap_bool.
METHODS load_config.
ENDCLASS.
CLASS zcl_tenant_customizing IMPLEMENTATION.
METHOD get_config. IF mv_loaded = abap_false. load_config( ). ENDIF. rs_config = ms_config. ENDMETHOD.
METHOD load_config. " Mandantenabhängige Konfiguration laden SELECT SINGLE * FROM ztenant_config INTO @ms_config.
" Feature Flags laden SELECT * FROM zfeature_flags INTO TABLE @mt_feature_flags.
mv_loaded = abap_true. ENDMETHOD.
METHOD get_feature_flags. IF mv_loaded = abap_false. load_config( ). ENDIF. rt_flags = mt_feature_flags. ENDMETHOD.
METHOD is_feature_enabled. DATA(lt_flags) = get_feature_flags( ).
READ TABLE lt_flags INTO DATA(ls_flag) WITH KEY feature = iv_feature.
rv_enabled = xsdbool( sy-subrc = 0 AND ls_flag-enabled = abap_true ). ENDMETHOD.
ENDCLASS.Tenant-spezifische Destinations
Jeder Tenant kann eigene externe Verbindungen konfigurieren:
CLASS zcl_tenant_destination DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. METHODS: get_http_destination IMPORTING iv_destination_name TYPE string RETURNING VALUE(ro_destination) TYPE REF TO if_http_destination RAISING cx_http_dest_provider_error.
ENDCLASS.
CLASS zcl_tenant_destination IMPLEMENTATION.
METHOD get_http_destination. " Tenant-spezifische Destination aus dem Destination Service " Der Destination Service liefert automatisch die Destination " für den aktuellen Tenant-Kontext
ro_destination = cl_http_destination_provider=>create_by_cloud_destination( i_name = iv_destination_name i_service_instance_name = 'destination-service' i_authn_mode = if_a4c_cp_service=>service_specific ). ENDMETHOD.
ENDCLASS.Testing in Multitenancy-Szenarien
Unit Tests mit Tenant-Mocking
CLASS ltcl_tenant_aware DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION. CLASS-DATA: environment TYPE REF TO if_cds_test_environment.
CLASS-METHODS: class_setup, class_teardown.
METHODS: test_tenant_isolation FOR TESTING, test_cross_tenant_access FOR TESTING.
ENDCLASS.
CLASS ltcl_tenant_aware IMPLEMENTATION.
METHOD class_setup. " Test-Environment für CDS Views erstellen environment = cl_cds_test_environment=>create( i_for_entity = 'ZI_PRODUCT' ). ENDMETHOD.
METHOD class_teardown. environment->destroy( ). ENDMETHOD.
METHOD test_tenant_isolation. " Testdaten für verschiedene Mandanten DATA lt_test_data TYPE STANDARD TABLE OF zproduct.
lt_test_data = VALUE #( ( mandt = '100' product_id = 'P001' product_name = 'Tenant A Product' ) ( mandt = '200' product_id = 'P002' product_name = 'Tenant B Product' ) ).
environment->insert_test_data( lt_test_data ).
" Test: Tenant A sieht nur seine Produkte " Der aktuelle Mandant wird vom Testframework auf 100 gesetzt SELECT COUNT(*) FROM zi_product INTO @DATA(lv_count).
cl_abap_unit_assert=>assert_equals( act = lv_count exp = 1 msg = 'Tenant sollte nur eigene Produkte sehen' ). ENDMETHOD.
METHOD test_cross_tenant_access. " Test für Provider-Cross-Tenant-Zugriff " Benötigt spezielle Berechtigung Z_PROVIDER " ... ENDMETHOD.
ENDCLASS.Best Practices
Do’s
| Empfehlung | Begründung |
|---|---|
| Mandantenabhängige Tabellen nutzen | Automatische Isolation durch das System |
| Tenant-ID in Logs aufnehmen | Für Debugging und Audit-Trail |
| Feature Flags pro Tenant | Individuelle Feature-Rollouts |
| Separate Nummernkreise | Keine Kollisionen zwischen Tenants |
| Tenant-spezifische Destinations | Jeder Kunde hat eigene Backends |
Don’ts
| Vermeiden | Risiko |
|---|---|
| Hardcodierte Mandanten | Funktioniert nicht in Multitenancy |
| Cross-Client ohne Berechtigung | Datenleck zwischen Tenants |
| Globale Variablen für Tenant-Daten | Race Conditions |
| Tenant-ID aus URL parsen | Sicherheitslücke |
| Synchrone Tenant-Operationen | Performance-Probleme |
Vergleich: Single-Tenant vs. Multi-Tenant
| Aspekt | Single-Tenant | Multi-Tenant |
|---|---|---|
| Isolation | Physisch getrennt | Logisch getrennt |
| Kosten | Höher (pro Instanz) | Niedriger (geteilt) |
| Skalierung | Pro Tenant | Über alle Tenants |
| Customizing | Unbegrenzt | Im Rahmen der Plattform |
| Updates | Individuell | Für alle gleichzeitig |
| Compliance | Einfacher | Komplexer |
Weiterführende Themen
- ABAP On-Premise vs. BTP - Grundlagen der BTP-Architektur
- Authorization Checks - Berechtigungsprüfungen im Detail
- RAP Authorization - Berechtigungen in RAP
- IAM Business Catalogs - Berechtigungskataloge erstellen
Zusammenfassung
Multitenancy in ABAP Cloud ermöglicht skalierbare SaaS-Anwendungen:
- Shared Schema: Ein ABAP-System für alle Tenants, Trennung über Mandanten
- Automatische Isolation: CDS Views und Datenbankzugriffe sind standardmäßig mandantenabhängig
- Lifecycle Management: SaaS Provisioning Service für Onboarding und Offboarding
- Tenant Context:
cl_abap_context_infofür Zugriff auf Tenant-Informationen - Berechtigungen: IAM Business Catalogs für Provider- und Consumer-Rollen
Die Kombination aus SAP BTP-Services (XSUAA, Destination Service, SaaS Provisioning) und ABAP Cloud bietet eine solide Grundlage für mandantenfähige Geschäftsanwendungen.