CodamAIDocs
Topicdone

Which database? The persistence target

The decision whether an access lands in the system DB or in the tenant DB, as a complete decision table. Without a fallback to the system DB.

Variants
system modeltenant model with tenantwithout context → 500without tenant → 400tenant not allowed → 403operating mode SINGLEdatabase missing or unreachable

What this is about

Every time CDMS reads or writes an object, it has to know which database the access goes to. That database is called the persistence target. There are only two kinds of targets:

  • the system database with the key system,
  • the database of a tenant with the tenant key, for example acme.

The decision is made for each model separately, not once per request. Two things determine it: the level of the model and the tenant in the RequestContext. A header, a field in the data or a URL parameter play no part.

The flow

flowchart TD
    A(["Access to a model"]) --> S{"System model?"}
    S -- yes --> SYS[("System database")]
    S -- no --> K{"RequestContext present?"}
    K -- no --> E500["500 CDMS_PERSISTENCE_CONTEXT_MISSING"]
    K -- yes --> T{"Tenant set?"}
    T -- no --> M1{"Operating mode?"}
    M1 -- SINGLE --> ONE[("the one database")]
    M1 -- MULTI --> E400["400 CDMS_TENANT_REQUIRED"]
    T -- yes --> M2{"Operating mode?"}
    M2 -- SINGLE --> ONE
    M2 -- MULTI --> R{"reserved key?<br/>system, single"}
    R -- yes --> E403R["403 CDMS_TENANT_KEY_RESERVED"]
    R -- no --> L{"Tenant and switch target<br/>in the list of allowed tenants?"}
    L -- no --> E403["403 CDMS_TENANT_SWITCH_NOT_AUTHORIZED"]
    L -- yes --> TDB[("Database of the tenant")]

The order is deliberate: the question “system model?” comes before any check of the context. A missing or wrong RequestContext can therefore neither redirect a system model to another database nor block it.

The decision table

Where an access goes
Level of the modelRequestContextTenantOperating modein the list of allowed tenantsPersistence target
System––––system database
Tenant / usermissing–––500 CDMS_PERSISTENCE_CONTEXT_MISSING
Tenant / userpresentmissingSINGLE–the one database
Tenant / userpresentmissingMULTI–400 CDMS_TENANT_REQUIRED
Tenant / userpresentacmeSINGLE–the one database; the tenant does not count
Tenant / userpresentacmeMULTIyesdatabase acme
Tenant / userpresentacmeMULTIno403 CDMS_TENANT_SWITCH_NOT_AUTHORIZED
Tenant / userpresentsystem or singleMULTI–403 CDMS_TENANT_KEY_RESERVED, even if the key is in the list

You set the level of a model in the hub, see Model levels. The generator turns it into the base class of the entity: AbstractSystemModel, AbstractTenantModel or AbstractUserModel. At runtime CDMS reads from it whether a model is a system model.

The error cases

When no persistence target is found

When: Code accesses a tenant model while no RequestContext exists.

  1. 1
    CDMS
    looks for the RequestContext of the current thread and finds none
  2. 2
    CDMS
    500 CDMS_PERSISTENCE_CONTEXT_MISSING

Result: This does not happen with normal requests but in custom code: an own thread, a scheduled job, a listener. Such code has to obtain the context itself, see Working for a tenant without a request.

When: MULTI, the RequestContext contains no tenant.

  1. 1
    CDMS
    wants to read or write a tenant model
  2. 2
    CDMS
    400 CDMS_TENANT_REQUIRED

Result: Normal requests without a tenant are already refused by the filter chain with 403, see Where the tenant of a request comes from. This check is the second safeguard behind it.

When: MULTI, the tenant or the target of a switch wish is not in the list of allowed tenants.

  1. 1
    CDMS
    compares tenant and switch target with the list from the token
  2. 2
    CDMS
    403 CDMS_TENANT_SWITCH_NOT_AUTHORIZED

Result: A refused switch thus becomes visible instead of silently continuing in the own tenant. See Tenant switch by header.

From tenant key to database

Once the target is fixed, CDMS builds the connection from it. The database URL of the installation contains the placeholder {tenant}:

CODAMAI_PERSISTENCE_DATABASE_URL=jdbc:mysql://db:3306/{tenant}
TargetDatabase
system database in MULTIjdbc:mysql://db:3306/system
tenant acme in MULTIjdbc:mysql://db:3306/acme
everything in SINGLEjdbc:mysql://db:3306/single

So all databases live on the same database server and use the same credentials. They are separated by the database name.

If the database does not exist yet or the server cannot be reached, the access ends with its own error, see Databases, pools, migration:

ErrorStatusMeaning
CDMS_TENANT_DATASOURCE_NOT_FOUND500The tenant’s database is missing and automatic creation is not approved.
CDMS_TENANT_DATASOURCE_UNAVAILABLE503The database server does not answer. CDMS then does not try to create anything.
CDMS_ENTITY_MANAGER_CREATION_FAILED500The database exists, but access to it could not be set up.

One request, two targets

A request can touch both kinds of targets, for example when a hook on a tenant model also writes a system model. CDMS then opens a separate connection and a separate transaction for each target. What that means for errors is explained in No atomicity across two databases.

Pitfalls

Where to go next

Sources in the code and the knowledge base
  • commons-persistence – DatabaseRequestContext (getEntityManager, resolveTenant, requireAllowedTenant, isTenantAllowed)
  • commons-persistence – TenantIdentifierResolver.DEFAULT_TENANT, TenantEntityManagerFactory (cacheKey), DataSourceManager (getConnection, {tenant} in the URL)
  • commons-persistence – PersistenceErrorCode (CDMS_PERSISTENCE_CONTEXT_MISSING 500, CDMS_TENANT_REQUIRED 400, CDMS_TENANT_SWITCH_NOT_AUTHORIZED 403, CDMS_TENANT_KEY_RESERVED 403, CDMS_TENANT_DATASOURCE_NOT_FOUND 500, CDMS_TENANT_DATASOURCE_UNAVAILABLE 503, CDMS_ENTITY_MANAGER_CREATION_FAILED 500)
  • CDMS/cdms-persistence-database – AbstractSystemModel, AbstractTenantModel, AbstractUserModel, EntityClassFilterService
  • documentation/30-daten-und-persistenz/01-mandantentrennung.md (step 3)
Search