Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

EclipseLink/Development/Indigo/Multi-Tenancy

< EclipseLink‎ | Development‎ | Indigo
Revision as of 11:04, 9 March 2011 by Guy.pelletier.oracle.com (Talk | contribs) (Annotation)

Enhancement request: bug 337323

Multi-Tenancy

The goal of this feature is to allow multiple application tenants to share the same schema using tenant identifying column(s). It is for shared 'striped' database data.

Requirements

  1. Support configuration of shared multi-tenant entity types using EclipseLink specific annotations and/or eclispelink-orm.xml with the XML overriding the annotation.
    • Augment database queries to limit query results to the tenant identifier values provided as property values
    • Ensure all INSERT, UPDATE, DELETE operations populate and limit their effect to the defined tenant identifiers
  2. Support accessing shared data at either the EntityManagerFactory or EntityManager
    • When using EMF the underlying cache must be unique to the provided tenant identifiers
  3. Support the tenant identifier columns being:
    • un-mapped
    • mapped
  4. Support schema generation including the specified tenant identifier column(s). Default type will be based on any mapping if available otherwise it will be assumed to be a string and override with the column's definition

Configuration

With this new feature developers will be able to enable shared tenant table(s) usage at the entity level using one or more columns associated with persistence unit or context property values that must be provided. The tenant id is completely application definable. The user can pick any property or column name they wish or let Eclipselink use defaults. Also there is no limit on the number of tenant ids an application can configure.

@Entity
@TenantColumn(name=“tenant-id”, columnName=“TENANT_ID”))
@Table(name=“EMP”)
public class Employee {
...
}
EMP_ID VERSION F_NAME L_NAME GENDER TENANT_ID
1 1 John Doe M 1
2 3 Jane Doe F 2

The following new EclipseLink metadata will be added.

Annotation

The tenant column(s) annotation states that the table(s) (@Table and @SecondaryTable) for the given entity is shared amongst tenants.

@Target({TYPE}) 
@Retention(RUNTIME)
public @interface TenantColumns {
   /**
    * (Required) One or more <code>TenantColumn</code> annotations.
    */
   TenantColumn[] value();
}
 
@Target({}) 
@Retention(RUNTIME)
public @interface TenantColumn {
  /**
   * (Optional) The name of the context property to apply to the 
   * tenant column.
   */
  String name() default "eclipselink.tenant-id";
 
  /**
   * (Optional) Defines the column that will be mapped for this tenant id.
   */
  String columnName() default "TENANT_ID";
 
  /**
   * (Optional) The type of object/column to use as a tenant column.
   * Defaults to {@link DiscriminatorType#STRING DiscriminatorType.STRING}.
   */
  DiscriminatorType discriminatorType() default STRING;
 
  /**
   * (Optional) The SQL fragment that is used when generating the DDL 
   * for the tenant column.
   * <p> Defaults to the provider-generated SQL to create a column 
   * of the specified tenant type.
   */
  String columnDefinition() default "";
 
  /**
   * (Optional) The name of the table that contains the tenant column. 
   * If absent the column is assumed to be in the primary table.
   */
  String table() default "";
 
  /** 
   * (Optional) The column length for String-based discriminator types. 
   * Ignored for other discriminator types.
   */
  int length() default 31;
 
  /**
   * (Optional) Specifies that the tenant column is part of the primary
   * key of the table.
   */
  boolean id() default false;
}

Eclipselink-orm.xml

<xsd:complexType name="tenant-id">
  <xsd:annotation>
    <xsd:documentation>
 
      ...
 
    </xsd:documentation>
  </xsd:annotation>
  <xsd:sequence>
    <xsd:element name="column" type="orm:column" minOccurs="0"/>    
  </xsd:sequence>
  <xsd:attribute name="name" type="xsd:string"/>    
</xsd:complexType>

Minimal Configuration

The column name when not specified will default to TENANT_ID. The tenant id name when not specified will default to eclipselink.tenant.id Meaning the minimal configuration is:

@Entity
@TenantId
public Employee() {
  ...
}

Specified examples:

@Entity
@Table(name="CUSTOMER")
@TenantId(name="multi-tenant.id", column=@Column(name="T_ID"))
public Customer() {
  ...
}
 
@Entity
@Table(name="EMPLOYEE")
@SecondaryTable(name="RESPONSIBILITIES")
@TenantIds({
  @TenantId(name="employee-tenant.id", column=@Column(name="TENANT_ID"))
  @TenantId(name="employee-tenant.code", column=@Column(name="TENANT_CODE", table="RESPONSIBILITIES"))
})
public Employee() {
  ...
}
<entity class="model.Customer">
  <table name="CUSTOMER" />
  <tenant-id property="multi-tenant.id"><column name="T_ID"/></tenant-id>
  ...
</entity>
 
<entity class="model.Employee">
  <table name="EMPLOYEE" />
  <secondary-table name="RESPONSIBILITIES"/>
  <tenant-id property="employee-tenant.id"><column name="TENANT_ID"/></tenant-id>
  <tenant-id property="employee-tenant.id"><column name="TENANT_CODE" table="RESPONSIBILITIES"/></tenant-id>
  ...
</entity>

Metadata Processing

The new metadata will available at the following levels:

  • @Entity/<entity>
  • @MappedSuperclass/<mapped-superclass>
  • <entity-mappings>
  • <persistence-unit-defaults>

When specified at the MappedSuperclass level, the tenant metadata will apply to all sub-entities of that class unless they specify their own tenant metadata. Within an inheritance hierarchy, tenant metadata can only be applied at the root level of the inheritance hierarchy. An exception will be thrown otherwise.

In the eclipselink-orm.xml, it is possible to specify a default tenant id metadata through the persistence unit metadata defaults.

<xsd:complexType name="persistence-unit-defaults">
  ...
    <xsd:sequence>
     ...
       <xsd:element name="tenant-id" type="orm:tenant-id" minOccurs="0" maxOccurs="unbounded"/>
     ...
    </xsd:sequence>
</xsd:complexType>

When this default value is specified, it will be applied to all entities of the persistence unit minus those that specify their own tenant metadata. Alternatively, users may specify tenant id metadata at the entity-mappings level as well which would override a persistence unit default and apply itself to all entities of the given mapping file (unless they individual entities have specified their own metadata). This follows similar JPA XML metadata overriding rules.

<xsd:element name="entity-mappings">
  ...
    <xsd:sequence>
     ...
       <xsd:element name="tenant-id" type="orm:tenant-id" minOccurs="0" maxOccurs="unbounded"/>
     ...
    </xsd:sequence>
</xsd:complexType>

Any entity not marked with tenant metadata and with no persistence unit default will not populate a tenant id in the database.

Metadata Processing Warnings and Exceptions

  • When tenant metadata is applied to subclasses of an entity hierarchy (JOINED or SINGLE_TABLE) a log warning will be issued
    • NOTE: Tenant id information can and should be provided at the TABLE_PER_CLASS level.
  • When multiple properties map the same column.
  • Duplicate tenant ids will log a warning message, e.g.
    • @TenantId(column=@Column(name="TENANT"))
    • @TenantId(name="tenant.id", column=@Column(name="TENANT"))

Defaults will always apply even when there are multiple tenant ids and no exception above has been raise. This allows users to map several columns for the same property. E.g. The code below would default the property name to "eclipselink.tenant.id" and states it should be writing the TENANT column for both the EMPLOYEE and SALARY table.

@Entity
@Table(name="EMPLOYEE")
@SecondaryTable(name="SALARY")
@TenantIds({
  @TenantId(column=@Column(name="TENANT"))
  @TenantId(column=@Column(name="TENANT", table="SALARY"))
})
public Employee() {
  ...
}

Property configuration and caching scope

At runtime the properties can be specified via a persistence unit definition or passed to a create entity manager factory call.

The properties may be included within a peristence unit definition in the persistence.xml file.

The order of precendence for tenant id properties is as follows:

  • EntityManager
  • EntityManagerFactory
  • Application context (when in a Java EE container)
<persistence-unit name="multi-tenant">
  ...
  <properties>
    <property name="tenant.id" value="707"/>
    ...
  </properties>
</persistence-unit>

Or alternatively (and most likely preferred) in code as follows:

HashMap properties = new HashMap();
properties.put("tenant.id", "707");
...     
EntityManager em = Persistence.createEntityManagerFactory("multi-tenant", properties).createEntityManager();

Entity Manager Factory

At this level, users will be required to provide a unique session name through the "eclipselink.session-name" property to ensure a unique server session (and cache) is provided for each tenant. This allows for user defined properties (without any prefixing).

HashMap properties = new HashMap();
properties.put("tenant.id", "707");
properties.put("eclipselink.session-name", "multi-tenant-707");
...     
EntityManager em = Persistence.createEntityManagerFactory("multi-tenant", properties).createEntityManager();
Shared Entity Manager Factory

When using a shared entity manager factory, no L2 cache 'striping' will be performed. All tenant id will be contained in the cache (unless isolation is turned on).

Entity Manager

At this level, users will be required to specify the caching strategies as the same server session can be employed for each tenant. Users may decide to us an isolation level here etc to ensure no 'shared' tenant information exists in the L2 cache.

HashMap properties = new HashMap();
properties.put("tenant.id", "707");
properties.put("eclipselink.cache.shared.Employee", "false");
properties.put("eclipselink.cache.size.Address", "10");
properties.put("eclipselink.cache.type.Contract", "NONE");
...     
EntityManager em = Persistence.createEntityManagerFactory("multi-tenant", properties).createEntityManager();
...


Core

The tenant id column(s) will be initialized during the pre-initialization of each descriptor of the persistence unit.

Those columns will then be applied in two places.

  1. We will leverage the current additional join expression from the DescriptorQueryManager to filter tenants. This is similar to the Additional Criteria feature. During postInitialization of the descriptor query manager after we have appended the additional criteria (if there is some), we will append the tenant id column(s) and its value(s) to the additional join expression.
  2. For inserts, we will append the tenant id column(s) and value(s) when building the row representation of an object. This is done in the following methods from ObjectBuilder (Note: this is similar to the handling of the discriminator column within an inheritance hierarchy)
    1. buildRow
    2. buildRowForShallowInsert
    3. buildRowForUpdate
    4. buildRowWithChangeSet
    5. buildTemplateInsertRow
    • NOTE: When the tenant id column is mapped, it need not be added to the row. Only it's value should be populated if it has not been already.

The tenant id column(s) are assumed to exist on the primary table. If using secondary tables the column metadata must specify the table if it is not on the primary.

Tenant id columns are not expected for the following tables (which refer back to their related entity through a primary key association):

  1. @CollectionTable
  2. @JoinTable
  3. JOINED Inheritance hierarchy tables
  4. SINGLE_TABLE Inheritance hierarchy

NOTE: This assumes id generation is shared across persistence units. Otherwise, in a multi-tenant environment, the tenant id becomes part of the primary key and all tables must then have a tenant id (which becomes another join column on the relation tables).

Core/Runtime Exceptions

  • An exception will be thrown when a named tenant property can not be found.

Querying

The tenant id column and value will be supported through the following entity manager operations:

  • persist
  • find
  • refresh

And the following queries:

  • named queries

NOTE: EclipseLink will not modify, therefore, support multi-tenancy through named native queries. When using these types of queries within a multi-tenant environment, the user will need to be aware and handle any multi-tenancy issues themselves directly in their native query. To all intent and purpose, named native queries should be avoided in a multi-tenant environment.

Support for update all and delete all queries should be included.

DDL generation

DDL generation will need to support the generation of tenant id columns (for all necessary tables). The DDL generation of columns is based off the descriptor's columns. During pre-initialization we therefore need to ensure that our tenant id columns are built and added to this list (if they are NOT mapped columns). This should be done after the descriptor table initialization (including inheritance hierarchies) has been preformed. Mapped tenant id columns are added automatically and we should avoid adding them more than once.

if (hasTenantIdFields()) {
  for (String property : tenantIdFields.keySet()) {
    for (DatabaseField tenantField : tenantIdFields.get(property)) {
      getFields().add(buildField(tenantField));
    }
  }
}

Open/Future items

  1. How can an admin user access data from multiple tenants?
  2. Allow tenant id to be part of the entity identifier
    1. Incorporate sequence generators

Back to the top