Skip to main content
Jump to: navigation, search

Difference between revisions of "EclipseLink/Development/Indigo/Multi-Tenancy"

(Accessibility)
(Accessibility, overridding and defaulting)
Line 247: Line 247:
 
</source>
 
</source>
  
=== Accessibility, overridding and defaulting ===
+
=== Accessibility and defaulting ===
  
Along with the availability from @Entity and the table element, the new metadata will available at the following levels to provide defaults. Usage at the these levels follows similar JPA metadata overriding rules.
+
Along with the availability from @Entity and the table element, the new metadata will available at the following levels to provide defaults. Usage at the these levels follows similar JPA metadata defaulting rules.
  
 
* persistence-unit-defaults  
 
* persistence-unit-defaults  

Revision as of 11:07, 17 March 2011

Enhancement request: bug 337323

Multi-Tenancy

The goal of this feature is to allow multiple application tenants to share the same schema using tenant discriminator 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 discriminator value(s) provided as property values
    • Ensure all INSERT, UPDATE, DELETE operations populate and limit their effect to the defined tenant discriminator column(s)
  2. Support accessing shared data at either the EntityManagerFactory or EntityManager
    • When using EMF the underlying cache must be unique to the provided tenant discriminator value(s)
  3. Support the tenant discriminator column(s) being:
    • un-mapped (see examples below)
    • mapped (see examples below)
  4. Support schema generation including the specified tenant discriminator 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

Metadata 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 discriminator column(s) 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 discriminator columns an application can configure.

This document will focus only on the SINGLE_TABLE multi-tenant type. The multi-tenant type SINGLE_TABLE states that the table(s) (@Table and @SecondaryTable) for the given entity is shared ("striped") amongst tenants.

Within a SINGLE_TABLE or JOINED inheritance hierarchy, multi-tenant metadata can only be applied at the root level of the inheritance hierarchy. A log warning will be issued otherwise. It is possible to specify multi-tenant metadata within a TABLE_PER_CLASS inheritance hierarchy.

@Entity
@Table(name=“EMP”)
@Multitenant(SINGLE_TABLE)
@TenantDiscriminator(name = “tenant-id”, columnName = “TENANT_ID”)
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.

Annotations

The new @Multitenant annotation is used in conjunction with the @Table metadata. Since we can not change the existing JPA @Table, the new annotation is specified outside the @Table (we can be more specific in the XML definition).

We will allow multi-tenant metadata to be applied at the mapped superclass level. When applied at that level, it will apply to all sub-entities unless they specify their own multi-tenant metadata.

@Target({TYPE}) 
@Retention(RUNTIME)
public @interface Multitenant {
    /**
     * (Optional) Specify the multi-tenant strategy to use.
     */
    MultitenantType value() default MultitenantType.SINGLE_TABLE;
}
 
@Target({TYPE}) 
@Retention(RUNTIME)
public @interface TenantDiscriminator {
    /**
     * (Optional) The name of the context property to apply to the 
     * tenant discriminator column.
     */
    String name() default "eclipselink.tenant-id";
 
    /**
     * (Optional) The name of column to be used for the discriminator.
     */
    String columnName() default "TENANT_ID";
 
    /**
     * (Optional) The type of object/column to use as a class discriminator.
     * Defaults to {@link DiscriminatorType#STRING DiscriminatorType.STRING}.
     */
    DiscriminatorType discriminatorType() default DiscriminatorType.STRING;
 
    /**
     * (Optional) The SQL fragment that is used when generating the DDL
     * for the discriminator column.
     * <p> Defaults to the provider-generated SQL to create a column
     * of the specified discriminator type.
     */
    String columnDefinition() default "";
 
    /**
     * (Optional) The column length for String-based discriminator types.
     * Ignored for other discriminator types.
     */
    int length() default 31;
 
    /**
     * (Optional) The name of the table that contains the column.
     * If absent the column is assumed to be in the primary table.
     */
    String table() default "";
 
    /**
     * Specifies that the tenant discriminator column is part of the primary 
     * key of the table.
     */
    boolean primaryKey() default false; 
}
 
@Target({TYPE}) 
@Retention(RUNTIME)
public @interface TenantDiscriminators {
   /**
    * (Required) One or more <code>TenantDiscriminator</code> annotations.
    */
   TenantDiscriminator[] value();
}
 
public enum MultitenantType {
    /**
     * Specifies that table(s) the entity maps to includes rows for multiple tenants. 
     * The tenant discriminator column(s) are used with application context values to
     * limit what a persistence context can access.
     */
    SINGLE_TABLE, 
 
    /**
     * Specifies that different tables are used for each tenant. The table scan be uniquely
     * identified by name, schema/tablespace.
     */
    TABLE_PER_TENANT 
}

Eclipselink-orm.xml

The multitenant metadata in XML will be available within the table complex element.

  <xsd:complexType name="table">
    <xsd:annotation>
      <xsd:documentation>
      ...
      </xsd:documentation>
    </xsd:annotation>
    <xsd:sequence>
      ...
      <xsd:element name="multitenant" type="orm:multitenant" minOccurs="0"/>
    </xsd:sequence>
    ...
  </xsd:complexType>
 
<!-- **************************************************** -->
 
<xsd:complexType name="multitenant">
  <xsd:annotation>
    <xsd:documentation>
      ...
    </xsd:documentation>
  </xsd:annotation>
  <xsd:sequence>
    <xsd:element name="tenant-discriminator" type="orm:tenant-discriminator" minOccurs="0" maxOccurs="unbounded"/>
  </xsd:sequence>
  <xsd:attribute name="type" type="orm:multitenant-type"/>
</xsd:complexType>
 
<!-- **************************************************** -->
 
<xsd:complexType name="tenant-discriminator">
  <xsd:annotation>
    <xsd:documentation>
      ...
    </xsd:documentation>
  </xsd:annotation>
  <xsd:attribute name="name" type="xsd:string"/>
  <xsd:attribute name="column-name" type="xsd:string"/>
  <xsd:attribute name="discriminator-type" type="orm:discriminator-type"/>
  <xsd:attribute name="column-definition" type="xsd:string"/>
  <xsd:attribute name="table" type="xsd:string"/>
  <xsd:attribute name="length" type="xsd:int"/>
  <xsd:attribute name="primary-key" type="xsd:boolean"/>
</xsd:complexType>
 
<!-- **************************************************** -->
 
<xsd:simpleType name="multitenant-type">
  <xsd:annotation>
    <xsd:documentation>
      ...
    </xsd:documentation>
  </xsd:annotation>
  <xsd:restriction base="xsd:token">
    <xsd:enumeration value="SINGLE_TABLE"/>
    <xsd:enumeration value="TABLE_PER_TENANT"/>
  </xsd:restriction>
</xsd:simpleType>

Minimal Configuration

All parts of the multi-tenant and tenant discriminator metadata are defaulted (see annotation definition above), therefore the minimal configuration is:

@Entity
@Table(name="EMP")
@Multitenant
public Employee() {
  ...
}
 
@Entity
@Table(name="EMP")
@TenantDiscriminator
public Employee() {
  ...
}
<entity class="model.Employee">
  <table name="EMP">
    <multitenant/>
  </table>
  ...
</entity>

Accessibility and defaulting

Along with the availability from @Entity and the table element, the new metadata will available at the following levels to provide defaults. Usage at the these levels follows similar JPA metadata defaulting rules.

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

persistence-unit-defaults

In the eclipselink-orm.xml, it is possible to specify a default multi-tenant metadata through the persistence unit metadata defaults. When this default value is specified, it will apply to all entities of the persistence unit, minus those that specify their own multi-tenant metadata.

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

entity-mappings

Alternatively, users may specify multi-tenant 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 an individual entity has specified its own multi-tenant metadata).

<xsd:element name="entity-mappings">
  ...
    <xsd:sequence>
     ...
       <xsd:element name="multitenant" type="orm:multitenant" minOccurs="0"/>
     ...
    </xsd:sequence>
</xsd:complexType>

Mapped superclass

Users can further configure multi-tenant metadata at the mapped superclass which would override both a persistence unit default and entity mappings setting (unless a sub-entity class or mapped superclass has specified its own multi-tenant metadata).

Any entity not marked with multi-tenant metadata and with no persistence unit default, entity mapping or mapped superclass level metadata, will not use any multi-tenancy in the database.

Mapped vs. Unmapped Tenant Discriminator

  • When a tenant discriminator is mapped, its associated attribute should be marked as read only. If it is not, an exception will be raised. With this restriction in place, a tenant discriminator can not be part of the entity identifier. NOTE: it can only be part of the primary key specification on the database (see the annotation definition above)
  • On persist, the value of the mapped tenant discriminator mapping is populated from its associated session property.
  • Both mapped and unmapped properties are used to form the additional criteria when issuing a select query.
  • Unmapped tenant discriminators will require EclipseLink to populate the row with the tenant discriminators associated session property value. See Core section below.

Metadata Processing Warnings and Exceptions

  • When multi-tenant metadata is applied to subclasses of an entity hierarchy (JOINED or SINGLE_TABLE) a log warning will be issued
    • NOTE: multi-tenant metadata can be provided in a TABLE_PER_CLASS inheritance hierarchy.
  • When multiple properties map the same column.
  • Duplicate tenant discriminators will log a warning message, e.g.
    • @TenantDiscriminator(columnName="TENANT")
    • @TenantDiscriminator(name="eclipselink.tenant-id", columnName="TENANT")
  • A mapped tenant discriminator who's attribute is not marked read only will throw an exception.

Defaults will always apply even when there are multiple tenant discriminators 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")
@MultiTenant(SINGLE_TABLE)
@TenantDiscriminators({
    @TenantDiscriminator(columnName = "TENANT")
    @TenantDiscriminator(columnName = "TENANT", table = "SALARY")
  }
)
public Employee() {
  ...
}

Examples

Annotation examples

/** Single tenant column **/
 
@Entity
@Table(name = "CUSTOMER")
@Multitenant
@TenantDescriminator(name = "multi-tenant.id", columnName = "TENANT")
public Customer() {
  ...
}
 
/** Multiple tenant columns using multiple tables **/
 
@Entity
@Table(name = "EMPLOYEE")
@SecondaryTable(name = "RESPONSIBILITIES")
@Multitenant(SINGLE_TABLE)
@TenantDiscriminators({
    @TenantDiscriminator(name = "employee-tenant.id", columnName = "TENANT_ID", length = 20)
    @TenantDiscriminator(name = "employee-tenant.code", columnName = "TENANT_CODE", discriminatorType = STRING, table = "RESPONSIBILITIES")
  }
)
public Employee() {
  ...
}
 
/** Tenant column mapped as part of the primary key on the database **/
 
@Entity
@Table(name = "ADDRESS")
@Multitenant
@TenantDiscriminator(name = "tenant.id", columnName = "TENANT", primaryKey = true)
public Address() {
  ...
}
 
/** Mapped Tenant column **/
 
@Entity
@Table(name = "Player")
@Multitenant
@TenantDiscriminator(name = "tenant.age", columnName = "AGE")
public Player() {
  ...
 
  @Basic
  @Column(name="AGE")
  @ReadOnly
  public int age;
}

XML examples

<!-- Single tenant column -->
 
<entity class="model.Customer">
  <table name="CUSTOMER">
    <multitenant>
      <tenant-discriminator name="multi-tenant.id" column-name="TENANT"/>
    </multitenant>
  </table>
  ...
</entity>
 
<!-- Multiple tenant columns using multiple tables -->
 
<entity class="model.Employee">
  <table name="EMPLOYEE">
    <multitenant type="SINGLE_TABLE">
      <tenant-discriminator name="employee-tenant.id" column-name="TENANT_ID" length="20"/>
      <tenant-discriminator name="employee-tenant.id" column-name="TENANT_CODE" discriminator-type="STRING" table="RESPONSIBILITIES"/>
    </multitenant>
  </table
  <secondary-table name="RESPONSIBILITIES"/>
  ...
</entity>
 
<!-- Tenant column mapped as part of the primary key on the database -->
 
<entity class="model.Address">
  <table name="ADDRESS">
    <multitenant>
      <tenant-discriminator name="multi-tenant.id" column-name="TENANT" primary-key="true"/>
    </multitenant>
  </table>
  ...
</entity>
 
<!-- Mapped Tenant column -->
 
<entity class="model.Player">
  <table name="PLAYER">
    <multi-tenant>
      <tenant-discriminator name="tenant.age" column-name="AGE"/>
    </multi-tenant>
  </table>
  ...
  <attributes>
    <basic name="age" read-only="true">
      <column name="AGE"/>
    </basic>
    ...
  </attributes>
  ...
</entity>

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). In further iterations we will look to augment the session name automatically for the user based on their tenant property values (or something thereof).

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 tenants could be contained in the cache (unless isolation is turned on). However, through the additional criteria portion of this feature only those from the active tenant should be returned.

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. These settings are set when creating the entity manager factory.

Swapping tenant id during a live EntityManager is not allowed.

HashMap tenantProperties = new HashMap();
properties.put("tenant.id", "707");
 
HashMap cacheProperties = new HashMap();
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", cacheProperties).createEntityManager(tenantProperties);
...

Core

The tenant dsicriminator 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 discriminator column(s) to the additional join expression.
    1. The tenant discriminator column value(s) will be added as arguments when issuing queries.
  2. For inserts, we will append the tenant discriminator 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 discriminator column is mapped, it need not be added to the row. Only its value should be populated if it has not already been done.

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

Tenant discriminator column(s) 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 (see future section below). 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 discriminator 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 discriminator 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 discriminator 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 discriminator columns are added automatically and we should avoid adding them more than once.

if (hasTenantDiscriminatorColumns()) {
  for (String property : tenantDiscriminatorColumns.keySet()) {
    for (DatabaseField tenantDiscriminatorColumn : tenantDiscriminatorColumns.get(property)) {
      getFields().add(buildField(tenantDiscriminatorColumn));
    }
  }
}

Open/Future items

  1. How can an admin user access data from multiple tenants?
  2. Tenant column when part of the entity identifier
    1. Incorporate sequence generators
  3. Augment the session name (or something of the sort) for the user removing the dependency of providing a unique session name.

Back to the top