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

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

(Configuration)
m (Annotation examples)
 
(416 intermediate revisions by 4 users not shown)
Line 3: Line 3:
 
</div>
 
</div>
  
[https://bugs.eclipse.org/bugs/show_bug.cgi?id=?????? Bug ??????]
+
Enhancement request: [https://bugs.eclipse.org/bugs/show_bug.cgi?id=337323 bug 337323]
  
 
== Multi-Tenancy ==
 
== Multi-Tenancy ==
  
The goal of this feature is to allow multiple application tenants to share the same schema using tenant identifying column(s).
+
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 and this feature will target only the SINGLE_TABLE solution outlined in the new annotation and xml metadata to follow. The TABLE_PER_TENANT strategy will not be addressed in this feature doc.
  
 
=== Requirements ===
 
=== Requirements ===
  
# Support configuration of shared multi-tenant entity types using a EclipseLink specific annotations or eclispelink-orm.xml with the XML overriding the annotation.
+
# Support configuration of shared multi-tenant entity types using EclipseLink specific annotations and/or eclipselink-orm.xml with the XML overriding the annotation.
# Support accessing shared data at eithe rthe EntityManagerFactory or EntityManager
+
#* Augment database queries to limit query results to the tenant discriminator value(s) provided as property values
#* When using EMF the underlying cache must be unque to the provided tenant identifiers
+
#* Ensure all INSERT, UPDATE, DELETE operations populate and limit their effect to the defined tenant discriminator column(s)
# Support the tenant identifier columns being:
+
# Support accessing shared data at either the EntityManagerFactory or EntityManager
#* un-mapped  
+
#* When using EMF the underlying cache must be unique to the provided tenant discriminator value(s)
#* mapped  
+
# Support the tenant discriminator column(s) being:
#* part of the identifier of the entity (mapped)
+
#* un-mapped
 +
#* mapped
 +
# Support schema generation including the specified tenant discriminator column(s).
  
=== Configuration ===
+
== 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.
+
This document will focus only on the SINGLE_TABLE multi-tenant type. The type will 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 by the user.
  
==== Annotation ====
+
=== Highlights ===
  
<source lang="java">
+
* The SINGLE_TABLE states that the table(s) (Table and SecondaryTable) for the given entity is shared ("striped") amongst tenants.
@TenantShared(
+
* A SINGLE_TABLE type is used in conjunction with tenant discriminator column(s).
  @TenantId(property="tenant-id", column=@Column(name="TENANT_ID"))
+
* 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.
</source>
+
* There is no limit on the number of tenant discriminator column(s) an application can configure.
  
==== eclipselink-orm.xml ====
+
==== Important notes ====
  
<source lang="xml">
+
* A TABLE_PER_TENANT multi-tenant specification will cause an exception to be thrown (un-supported). 
<entity class="model.Employee">
+
* Multi-tenant metadata can only be applied at the root level of the inheritance hierarchy when using a SINGLE_TABLE or JOINED inheritance strategy. A log warning will be issued otherwise.
    <table name="EMPLOYEE_X" />
+
* It is possible to specify multi-tenant metadata within a TABLE_PER_CLASS inheritance hierarchy.
    <tenant-shared>
+
 
        <tenant-id property="tenant-id"><column name="TENANT_ID"/></tenant-id>
+
==== Short example ====
     </tenant-shared>
+
 
 +
<source lang="java">
 +
@Entity
 +
@Table(name=“EMP”)
 +
@Multitenant(SINGLE_TABLE)
 +
@TenantDiscriminatorColumn(name = “TENANT_ID”, contextProperty = “tenant-id”)
 +
public class Employee {
 +
     ...
 +
}
 
</source>
 
</source>
The tenant id field and a tenant id value can be configured through the following EclipseLink properties.
 
  
* eclipselink.multi-tenant.id
+
{|{{BMTableStyle}}
* eclipselink.multi-tenant.id-column
+
|-{{BMTHStyle}}
 +
! EMP_ID
 +
! VERSION
 +
! F_NAME
 +
! L_NAME
 +
! GENDER
 +
! TENANT_ID
 +
|-  
 +
| 1
 +
| 1
 +
| John
 +
| Doe
 +
| M
 +
| 1
 +
|-  
 +
| 2
 +
| 3
 +
| Jane
 +
| Doe
 +
| F
 +
| 2
 +
|}
  
When a multi-tenant id is specified, the multi-tenant column will default to "TENANT_ID" if it is not specified by the user. That column is then expected to be available from the following tables of the schema:
+
The following new EclipseLink metadata will be added.
  
# @Table
+
=== Annotation usage ===
# @SecondaryTable
+
  
It is not expected for the following tables (which refers back to their related entity through a primary key association):
+
The new @Multitenant annotation is specified at the entity (or mapped superclass level). To enable multi-tenancy on a given entity the @Multitenant must be specified (@see minimal configuration).
  
# @CollectionTable
+
From the multi-tenant type, its companion annotation(s) (e.g. @TenantDiscriminatorColumn) are determined and processed accordingly.
# @JoinTable
+
  
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).
+
Multitenant metadata can be applied at the mapped superclass level. When applied at this level it is applied to all sub-entities unless they specify their own multi-tenant metadata. Note: multi-tenancy and its companion annotation(s) are homogeneous and will continue to be when new types are included. Meaning the following:
  
When using DDL generation, the user need not worry about this. The DDL generation framework will be responsible for ensuring all necessary tables have a tenant id column.
+
<source lang="java">
 +
@MappedSuperclass
 +
@Multitenant(SINGLE_TABLE)
 +
@TenantDiscriminatorColumn(name = "TENANT_ID")
 +
@TenantTableDiscriminator(...)
 +
public MySuperclass {
 +
}
 +
</source>
  
==== Persistence.xml configuration ====
+
Implies that all subclasses that do not define @Multitenant will default to:
The properties may be includes within a peristence unit definition in the persistence.xml file.
+
  
<pre>
+
<source lang="java">
<persistence-unit name="multi-tenant-707">
+
@Multitenant(SINGLE_TABLE)
  <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
+
@TenantDiscriminatorColumn(name = "TENANT_ID")
  <class>org.eclipse.persistence.testing.models.jpa.advanced.multitenant.Boss</class>
+
</source>
  <class>org.eclipse.persistence.testing.models.jpa.advanced.multitenant.Capo</class>
+
  <class>org.eclipse.persistence.testing.models.jpa.advanced.multitenant.Contract</class>
+
  <class>org.eclipse.persistence.testing.models.jpa.advanced.multitenant.MafiaFamily</class>
+
  <class>org.eclipse.persistence.testing.models.jpa.advanced.multitenant.Mafioso</class>
+
  <class>org.eclipse.persistence.testing.models.jpa.advanced.multitenant.Soldier</class>
+
  <class>org.eclipse.persistence.testing.models.jpa.advanced.multitenant.Underboss</class>
+
  <exclude-unlisted-classes>true</exclude-unlisted-classes>
+
  <properties>
+
    <property name="eclipselink.multi-tenant.id" value="707"/>
+
    <property name="eclipselink.logging.level" value="FINEST"/>
+
    <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
+
    <property name="eclipselink.create-ddl-jdbc-file-name" value="createMafia707_ddlGeneration.jdbc"/>
+
    <property name="eclipselink.drop-ddl-jdbc-file-name" value="dropMafia707_ddlGeneration.jdbc"/>
+
    <property name="eclipselink.ddl-generation.output-mode" value="sql-script"/>
+
  </properties>
+
</persistence-unit>
+
</pre>
+
  
==== CreateEntityManagerFactory configuration ====
+
and the @TenantTableDiscriminator metadata is SILENTLY ignored and will not apply to sub-entities that define @Multitenant(TABLE_PER_TENANT).
  
The properties may alternatively be configured in code as follows:
+
Another use case, given the following:
  
<pre>
+
<source lang="java">
HashMap properties = new HashMap();
+
@MappedSuperclass
properties.put("eclipselink.multi-tenant.id", "007");
+
@Multitenant(SINGLE_TABLE)
properties.put("eclipselink.logging.level", "FINEST");
+
public MySuperclass {
properties.put("javax.persistence.jdbc.driver", "oracle.jdbc.OracleDriver");
+
}
properties.put("javax.persistence.jdbc.url", "...");
+
properties.put("javax.persistence.jdbc.user", "...");
+
properties.put("javax.persistence.jdbc.password", "...");
+
properties.put("eclipselink.ddl-generation", "drop-and-create-tables");
+
properties.put("eclipselink.create-ddl-jdbc-file-name", "createMafia007_ddlGeneration.jdbc");
+
properties.put("eclipselink.drop-ddl-jdbc-file-name", "dropMafia007_ddlGeneration.jdbc");
+
properties.put("eclipselink.ddl-generation.output-mode", "sql-script");
+
       
+
EntityManager em = Persistence.createEntityManagerFactory("puName007", properties).createEntityManager();
+
</pre>
+
  
==== Metadata ====
+
@Entity
 +
@TenantDiscriminatorColumn(name = "T_ID", contextProperty = "my.tenant.id")
 +
public MyEntity {
 +
}
 +
</source>
  
To further configure the tenant id field, we could provide and allow the following metadata at the @Entity and @MappedSuperclass level.
+
MyEntity's @TenantDiscriminatorColumn is ignored and the defaults will apply to the @Multitenant specification from MySuperclass (where the tenant discriminator column is defaulted).  
  
At the @MappedSuperclass level, the tenant column would apply to all child entities unless they provide their own. Would also be valid to have different tenant columns in a joined inheritance hierarchy.
+
==== Annotation definitions ====
  
===== Annotation =====
+
<source lang="java">
<pre>
+
 
@Target({TYPE})  
 
@Target({TYPE})  
 
@Retention(RUNTIME)
 
@Retention(RUNTIME)
public @interface @TenantColumn{
+
public @interface Multitenant {
 +
    /**
 +
    * (Optional) Specify the multi-tenant strategy to use.
 +
    */
 +
    MultitenantType value() default MultitenantType.SINGLE_TABLE;
 +
}
 +
 
 +
@Target({TYPE})
 +
@Retention(RUNTIME)
 +
public @interface TenantDiscriminatorColumn {
 +
    /**
 +
    * (Optional) The name of column to be used for the discriminator.
 +
    */
 +
    String name() default "TENANT_ID";
 +
 
 +
    /**
 +
    * (Optional) The name of the context property to apply to the
 +
    * tenant discriminator column.
 +
    */
 +
    String contextProperty() default "eclipselink.tenant-id";
  
 
     /**
 
     /**
     * (Optional) The name of the column. Defaults to  
+
     * (Optional) The type of object/column to use as a class discriminator.
     * the property or field name.
+
     * Defaults to {@link DiscriminatorType#STRING DiscriminatorType.STRING}.
 
     */
 
     */
     String name() default "";
+
     DiscriminatorType discriminatorType() default DiscriminatorType.STRING;
  
 
     /**
 
     /**
     * (Optional) The SQL fragment that is used when  
+
     * (Optional) The SQL fragment that is used when generating the DDL
     * generating the DDL for the column.
+
     * for the discriminator column. Defaults to the provider-generated  
    * <p> Defaults to the generated SQL to create a
+
    * SQL to create a column of the specified discriminator type.
    * column of the inferred type.
+
 
     */
 
     */
 
     String columnDefinition() default "";
 
     String columnDefinition() default "";
  
 
     /**
 
     /**
     * (Optional) The column length. (Applies only if a
+
     * (Optional) The column length for String-based discriminator types.
     * string-valued column is used.)
+
     * Ignored for other discriminator types.
 
     */
 
     */
     int length() default 255;
+
     int length() default 31;
  
 
     /**
 
     /**
     * (Optional) The precision for a decimal (exact numeric)
+
     * (Optional) The name of the table that contains the column.
     * column. (Applies only if a decimal column is used.)
+
     * If absent the column is assumed to be in the primary table.
    * Value must be set by developer if used when generating
+
    * the DDL for the column.
+
 
     */
 
     */
     int precision() default 0;
+
     String table() default "";
  
 
     /**
 
     /**
     * (Optional) The scale for a decimal (exact numeric) column.
+
     * Specifies that the tenant discriminator column is part of the primary
     * (Applies only if a decimal column is used.)
+
     * key definition of the table.
 
     */
 
     */
     int scale() default 0;
+
     boolean primaryKey() default false;  
 
}
 
}
</pre>
 
  
===== XML =====
+
@Target({TYPE})
<pre>
+
@Retention(RUNTIME)
   <xsd:complexType name="tenant-column">
+
public @interface TenantDiscriminatorColumns {
 +
  /**
 +
    * (Required) One or more <code>TenantDiscriminatorColumn</code> annotations.
 +
    */
 +
  TenantDiscriminatorColumn[] 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
 +
}
 +
</source>
 +
 
 +
=== XML usage ===
 +
 
 +
The multitenant metadata in XML will be available at within the entity and mapped superclass element. When support for other multi-tenant types are added, their companion metadata should be surrounded by an xml choice element.
 +
 
 +
==== Eclipselink-orm.xml definition ====
 +
 
 +
<source lang="XML">
 +
 
 +
   <xsd:complexType name="entity">
 
     <xsd:annotation>
 
     <xsd:annotation>
 
       <xsd:documentation>
 
       <xsd:documentation>
 +
      ...
 +
      </xsd:documentation>
 +
    </xsd:annotation>
 +
    <xsd:sequence>
 +
      ...
 +
      <xsd:element name="multitenant" type="orm:multitenant" minOccurs="0"/>
 +
      ...
 +
    </xsd:sequence>
 +
    ...
 +
  </xsd:complexType>
  
        ...
+
  <xsd:complexType name="mapped-superclass">
 
+
    <xsd:annotation>
 +
      <xsd:documentation>
 +
      ...
 
       </xsd:documentation>
 
       </xsd:documentation>
 
     </xsd:annotation>
 
     </xsd:annotation>
     <xsd:attribute name="name" type="xsd:string"/>
+
     <xsd:sequence>
    <xsd:attribute name="column-definition" type="xsd:string"/>
+
      ...
    <xsd:attribute name="length" type="xsd:int"/>
+
      <xsd:element name="multitenant" type="orm:multitenant" minOccurs="0"/>
     <xsd:attribute name="precision" type="xsd:int"/>
+
      ...
     <xsd:attribute name="scale" type="xsd:int"/>
+
     </xsd:sequence>
 +
     ...
 
   </xsd:complexType>
 
   </xsd:complexType>
</pre>
 
  
=== Core ===
+
<!-- **************************************************** -->
  
The tenant id field(s) (multiple if using secondary tables) will be initialized during the pre-initialization of each descriptor of the persistence unit.
+
<xsd:complexType name="multitenant">
 +
  <xsd:annotation>
 +
    <xsd:documentation>
 +
      ...
 +
    </xsd:documentation>
 +
  </xsd:annotation>
 +
  <xsd:sequence>
 +
    <xsd:element name="tenant-discriminator-column" type="orm:tenant-discriminator-column" minOccurs="0" maxOccurs="unbounded"/>
 +
  </xsd:sequence>
 +
  <xsd:attribute name="type" type="orm:multitenant-type"/>
 +
</xsd:complexType>
  
Those fields will then be applied in two places.
+
<!-- **************************************************** -->
  
# We will leverage the current additional join expression from the DescriptorQueryManager to filter tenants. This is similar to the [[EclipseLink/Development/AdditionalCriteria | Additional Criteria]] feature. During postInitialization of the descriptor query manager after we had appended the additional criteria (if there is some), we will append the tenant id field and its value to the additional join expression.
+
<xsd:complexType name="tenant-discriminator-column">
# For inserts, we will append the tenant id column and value when building the row representation of an object. This is done in the following methods from ObjectBuilder (Note: similar thing is done for the the handling of the discriminiator column within an inheritance hierarchy)
+
  <xsd:annotation>
 +
    <xsd:documentation>
 +
      ...
 +
    </xsd:documentation>
 +
  </xsd:annotation>
 +
  <xsd:attribute name="name" type="xsd:string"/>
 +
  <xsd:attribute name="context-property" 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>
 +
 
 +
</source>
 +
 
 +
=== Minimal Configuration ===
 +
 
 +
All parts of the multi-tenant and tenant discriminator metadata are defaulted (see annotation definition above), however to enable multi-tenancy @Multitenant or <multitenant> must be specified. Therefore the minimal configuration is:
 +
 
 +
<source lang="java">
 +
@Entity
 +
@Table(name="EMP")
 +
@Multitenant
 +
public Employee() {
 +
  ...
 +
}
 +
 
 +
 
 +
// NOTE: The following example is NOT a minimal configuration. This would lead to no multi-tenancy as the solo @TableDiscriminator is
 +
// ignored since @Multitenant was not specified.
 +
 
 +
@Entity
 +
@Table(name="EMP")
 +
@TenantDiscriminatorColumn
 +
public Employee() {
 +
  ...
 +
}
 +
</source>
 +
 
 +
<source lang="xml">
 +
<entity class="model.Employee">
 +
  <multitenant/>
 +
  <table name="EMP"/>
 +
  ...
 +
</entity>
 +
</source>
 +
 
 +
=== Persistence unit and entity mappings defaulting ===
 +
 
 +
Along with the availability from the entity and mapped superclass levels, the new metadata will available at the following levels to provide defaults. Usage at the these levels follows similar JPA metadata defaulting and overriding rules.
 +
 
 +
* persistence-unit-defaults
 +
* entity-mappings
 +
 
 +
==== persistence-unit-defaults ====
 +
 
 +
In the eclipselink-orm.xml, it is possible to specify default tenant discriminator column metadata through the persistence unit metadata defaults. When defined at this level, it will apply to all entities of the persistence unit that have specified a multi-tenant type of SINGLE_TABLE minus those that specify their own tenant discriminator metadata.
 +
 
 +
Note: with no defaults, an entity not marked with multi-tenant metadata will not use any multi-tenancy strategy.
 +
 
 +
<source lang="xml">
 +
<xsd:complexType name="persistence-unit-defaults">
 +
  ...
 +
    <xsd:sequence>
 +
    ...
 +
      <xsd:element name="tenant-discriminator-column" type="orm:tenant-discriminator-column" minOccurs="0" maxOccurs="unbounded"/>
 +
    ...
 +
    </xsd:sequence>
 +
</xsd:complexType>
 +
</source>
 +
 
 +
==== entity-mappings ====
 +
 
 +
Alternatively, users may specify tenant discriminator column metadata at the entity-mappings level as well which would override a persistence unit default and apply itself to all entities with a multitenant type of SINGLE_TABLE of the given mapping file (unless an individual entity has specified its own tenant discriminator metadata).
 +
 
 +
<source lang="xml">
 +
<xsd:element name="entity-mappings">
 +
  ...
 +
    <xsd:sequence>
 +
    ...
 +
      <xsd:element name="tenant-discriminator-column" type="orm:tenant-discriminator-column" minOccurs="0" maxOccurs="unbounded"/>
 +
    ...
 +
    </xsd:sequence>
 +
</xsd:complexType>
 +
</source>
 +
 
 +
=== Mapped vs. Unmapped Tenant Discriminator ===
 +
 
 +
* When a tenant discriminator column is mapped, its associated mapping attribute should be marked as read only. If it is not, an exception will be raised. With this restriction in place, a tenant discriminator column can not be part of the entity identifier (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 column mapping is populated from its associated context property.
 +
* Both mapped and unmapped properties are used to form the additional criteria when issuing a select query.
 +
* Unmapped tenant discriminator columns will require EclipseLink to populate the row with the tenant discriminator columns associated context 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 (and the metadata is IGNORED)
 +
** NOTE: multi-tenant metadata can be provided in a TABLE_PER_CLASS inheritance hierarchy.
 +
* When multiple properties map the same column.
 +
* Duplicate tenant discriminator columns will log a warning message, e.g.
 +
** @TenantDiscriminatorColumn(name="TENANT")
 +
** @TenantDiscriminatorColumn(name="TENANT", contextProperty="eclipselink.tenant-id")
 +
* A mapped tenant discriminator column 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 <b>"eclipselink.tenant-id"</b> and states it should be writing the <b>TENANT</b> column for both the EMPLOYEE and SALARY table.
 +
 
 +
<source lang="java">
 +
@Entity
 +
@Table(name = "EMPLOYEE")
 +
@SecondaryTable(name = "SALARY")
 +
@MultiTenant(SINGLE_TABLE)
 +
@TenantDiscriminatorColumns({
 +
    @TenantDiscriminatorColumn(name = "TENANT")
 +
    @TenantDiscriminatorColumn(name = "TENANT", table = "SALARY")
 +
  }
 +
)
 +
public Employee() {
 +
  ...
 +
}
 +
</source>
 +
 
 +
=== More Examples ===
 +
 
 +
==== Annotation examples ====
 +
<source lang="java">
 +
 
 +
/** Single discriminator tenant column **/
 +
 
 +
@Entity
 +
@Table(name = "CUSTOMER")
 +
@Multitenant
 +
@TenantDiscriminatorColumn(name = "TENANT", contextProperty = "multi-tenant.id")
 +
public Customer() {
 +
  ...
 +
}
 +
 
 +
/** Multiple tenant discriminator columns using multiple tables **/
 +
 
 +
@Entity
 +
@Table(name = "EMPLOYEE")
 +
@SecondaryTable(name = "RESPONSIBILITIES")
 +
@Multitenant(SINGLE_TABLE)
 +
@TenantDiscriminatorColumns({
 +
    @TenantDiscriminatorColumn(name = "TENANT_ID", contextProperty = "employee-tenant.id", length = 20)
 +
    @TenantDiscriminatorColumn(name = "TENANT_CODE", contextProperty = "employee-tenant.code", discriminatorType = STRING, table = "RESPONSIBILITIES")
 +
  }
 +
)
 +
public Employee() {
 +
  ...
 +
}
 +
 
 +
/** Tenant discriminator column mapped as part of the primary key on the database **/
 +
 
 +
@Entity
 +
@Table(name = "ADDRESS")
 +
@Multitenant
 +
@TenantDiscriminatorColumn(name = "TENANT", contextProperty = "tenant.id", primaryKey = true)
 +
public Address() {
 +
  ...
 +
}
 +
 
 +
/** Mapped tenant discriminator column **/
 +
 
 +
@Entity
 +
@Table(name = "Player")
 +
@Multitenant
 +
@TenantDiscriminatorColumn(name = "AGE", contextProperty = "tenant.age")
 +
public Player() {
 +
  ...
 +
 
 +
  @Basic
 +
  @Column(name="AGE", insertable="false", updatable="false")
 +
  public int age;
 +
}
 +
 
 +
</source>
 +
 
 +
==== XML examples ====
 +
 
 +
<source lang="xml">
 +
 
 +
<!-- Single tenant discriminator column -->
 +
 
 +
<entity class="model.Customer">
 +
  <multitenant>
 +
    <tenant-discriminator-column name="TENANT context-property="multi-tenant.id""/>
 +
  </multitenant>
 +
  <table name="CUSTOMER"/>
 +
  ...
 +
</entity>
 +
 
 +
<!-- Multiple tenant discriminator columns using multiple tables -->
 +
 
 +
<entity class="model.Employee">
 +
  <multitenant type="SINGLE_TABLE">
 +
    <tenant-discriminator-column name="TENANT_ID" context-property="employee-tenant.id" length="20"/>
 +
    <tenant-discriminator-column name="TENANT_CODE" context-property="employee-tenant.id" discriminator-type="STRING" table="RESPONSIBILITIES"/>
 +
  </multitenant>
 +
  <table name="EMPLOYEE"/>
 +
  <secondary-table name="RESPONSIBILITIES"/>
 +
  ...
 +
</entity>
 +
 
 +
<!-- Tenant discriminator column mapped as part of the primary key on the database -->
 +
 
 +
<entity class="model.Address">
 +
  <multitenant>
 +
    <tenant-discriminator-column name="TENANT" context-property="multi-tenant.id" primary-key="true"/>
 +
  </multitenant>
 +
  <table name="ADDRESS"/>
 +
  ...
 +
</entity>
 +
 
 +
<!-- Mapped tenant discriminator column -->
 +
 
 +
<entity class="model.Player">
 +
  <multi-tenant>
 +
    <tenant-discriminator-column name="AGE" context-property="tenant.age"/>
 +
  </multi-tenant>
 +
  <table name="PLAYER"/>
 +
  ...
 +
  <attributes>
 +
    <basic name="age" insertable="false" updatable="false">
 +
      <column name="AGE"/>
 +
    </basic>
 +
    ...
 +
  </attributes>
 +
  ...
 +
</entity>
 +
 
 +
</source>
 +
 
 +
== 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 order of precendence for tenant discriminator column properties is as follows:
 +
* EntityManager
 +
* EntityManagerFactory
 +
* Application context (when in a Java EE container)
 +
 
 +
<source lang="xml">
 +
<persistence-unit name="multi-tenant">
 +
  ...
 +
  <properties>
 +
    <property name="tenant.id" value="707"/>
 +
    ...
 +
  </properties>
 +
</persistence-unit>
 +
</source>
 +
 
 +
Or alternatively (and most likely preferred) in code as follows:
 +
 
 +
<source lang="java">
 +
HashMap properties = new HashMap();
 +
properties.put("tenant.id", "707");
 +
...   
 +
EntityManager em = Persistence.createEntityManagerFactory("multi-tenant", properties).createEntityManager();
 +
</source>
 +
 
 +
==== 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).
 +
 
 +
<source lang="java">
 +
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();
 +
</source>
 +
 
 +
===== Shared Entity Manager Factory =====
 +
 
 +
A shared entity manager factory will be configured by default. When using a shared entity manager factory, by default the cache (L2) is not shared, meaning multitenant entities will have an ISOLATED cache setting. If users want to share the cache, setting the eclipselink.multitenant.tenants-share-cache to true will allow this and multitenant entities will have a PROTECTED cache setting.
 +
 
 +
To not share the entity manager factory users can set the eclipselink.multitenant.tenants-share-emf property to false.
 +
 
 +
Swapping tenant ID during an active EntityManager is not allowed.
 +
 
 +
==== 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.
 +
 
 +
<source lang="java">
 +
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);
 +
...
 +
</source>
 +
 
 +
== 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.
 +
 
 +
# We will leverage the current additional join expression from the DescriptorQueryManager to filter tenants. This is similar to the [[EclipseLink/Development/AdditionalCriteria | 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.
 +
## The tenant discriminator column value(s) will be added as arguments when issuing queries.
 +
# 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)
 
## buildRow
 
## buildRow
 
## buildRowForShallowInsert
 
## buildRowForShallowInsert
Line 183: Line 582:
 
## buildRowWithChangeSet
 
## buildRowWithChangeSet
 
## buildTemplateInsertRow
 
## 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.
 +
# For reading, to enable the additional criteria (tenant discriminator columns) we will need to modify the ReadObjectQuery and ReadAllQuery slightly in the prepareForExecution method.
 +
## There we will clone the translation row (if need be) and append the tenant discriminator columns to it before execution.
 +
The tenant discriminator column(s) are assumed to exist on the primary table. If using secondary tables the tenant 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):
 +
 +
# @CollectionTable
 +
# @JoinTable
 +
# JOINED inheritance hierarchy tables
 +
# SINGLE_TABLE inheritance hierarchy
 +
 +
NOTE: Id generation is shared across persistence units (see future section below).
 +
 +
==== Core/Runtime Exceptions ====
 +
* An exception will be thrown when a named tenant property can not be found.
 +
 +
== Properties ==
 +
 +
The public default context property (eclipselink.tenant-id) definition will be available from:
 +
 +
* org.eclipse.persistence.config.EntityManagerProperties.MULTITENANT_PROPERTY_DEFAULT
 +
* org.eclipse.persistence.config.PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT
 +
 +
=== Java example ===
 +
<source lang="java">
 +
EntityManager em = createEntityManager(MULTI_TENANT_PU);
 +
em.setProperty("tenant.id", "707");
 +
em.setProperty(EntityManagerProperties.MULTITENANT_PROPERTY_DEFAULT, "707");
 +
</source>
  
=== Querying ===
+
== Querying ==
  
The tenant id column and value will be supported through the following entity manager operations:
+
The tenant discriminator column and value will be supported through the following entity manager operations:
  
 
* persist
 
* persist
Line 194: Line 623:
 
And the following queries:
 
And the following queries:
 
* named queries
 
* named queries
 +
* update all
 +
* delete all
  
 
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.
 
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.
  
=== DDL generation ===
+
== DDL generation ==
  
DDL generation will need to support the generation of tenant id columns. The DDL generation of fields is based off the descriptor's fields. During pre-initialization we therefore need to ensure that our tenant id fields are built and added to this list. This should be done after the descriptor table initialization (including inheritance hierarchies) has been preformed.
+
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.
  
 
<pre>
 
<pre>
if (session.hasTenantId()) {
+
if (hasTenantDiscriminatorFields()) {
  for (DatabaseTable table : getTables()) {
+
    for (DatabaseField discriminatorField : tenantDiscriminatorFields.keySet()) {
    DatabaseField tenantIdField = session.getTenantIdField();
+
        getFields().add(buildField(discriminatorField));
    tenantIdField.setTable(table);
+
    }
    getFields().add(buildField(tenantIdField));  
+
  }  
+
 
}
 
}
 
</pre>
 
</pre>
 +
 +
=== DefaultTableGenerator ===
 +
 +
Only a small change will be required to the DDL generator to ensure we capture a primary key setting for a tenant discriminator column. The following line will be added to the initTableSchema(ClassDescriptor) method:
 +
 +
<source lang="java">
 +
isPKField = isPKField || dbField.isPrimaryKey();
 +
</source>
 +
 +
== Open/Future items ==
 +
 +
# [https://bugs.eclipse.org/bugs/show_bug.cgi?id=355458 Bug 355458]: Provide admin user access data from multiple tenants
 +
# Tenant column when part of the entity identifier
 +
## Incorporate sequence generators
 +
# Augment the session name (or something of the sort) for the user removing the dependency of providing a unique session name to achieve cache isolation.

Latest revision as of 15:43, 8 April 2013

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 and this feature will target only the SINGLE_TABLE solution outlined in the new annotation and xml metadata to follow. The TABLE_PER_TENANT strategy will not be addressed in this feature doc.

Requirements

  1. Support configuration of shared multi-tenant entity types using EclipseLink specific annotations and/or eclipselink-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
    • mapped
  4. Support schema generation including the specified tenant discriminator column(s).

Metadata Configuration

This document will focus only on the SINGLE_TABLE multi-tenant type. The type will 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 by the user.

Highlights

  • The SINGLE_TABLE states that the table(s) (Table and SecondaryTable) for the given entity is shared ("striped") amongst tenants.
  • A SINGLE_TABLE type is used in conjunction with tenant discriminator column(s).
  • 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.
  • There is no limit on the number of tenant discriminator column(s) an application can configure.

Important notes

  • A TABLE_PER_TENANT multi-tenant specification will cause an exception to be thrown (un-supported).
  • Multi-tenant metadata can only be applied at the root level of the inheritance hierarchy when using a SINGLE_TABLE or JOINED inheritance strategy. A log warning will be issued otherwise.
  • It is possible to specify multi-tenant metadata within a TABLE_PER_CLASS inheritance hierarchy.

Short example

@Entity
@Table(name=“EMP”)
@Multitenant(SINGLE_TABLE)
@TenantDiscriminatorColumn(name = “TENANT_ID”, contextProperty = “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.

Annotation usage

The new @Multitenant annotation is specified at the entity (or mapped superclass level). To enable multi-tenancy on a given entity the @Multitenant must be specified (@see minimal configuration).

From the multi-tenant type, its companion annotation(s) (e.g. @TenantDiscriminatorColumn) are determined and processed accordingly.

Multitenant metadata can be applied at the mapped superclass level. When applied at this level it is applied to all sub-entities unless they specify their own multi-tenant metadata. Note: multi-tenancy and its companion annotation(s) are homogeneous and will continue to be when new types are included. Meaning the following:

@MappedSuperclass
@Multitenant(SINGLE_TABLE)
@TenantDiscriminatorColumn(name = "TENANT_ID")
@TenantTableDiscriminator(...)
public MySuperclass {
}

Implies that all subclasses that do not define @Multitenant will default to:

@Multitenant(SINGLE_TABLE)
@TenantDiscriminatorColumn(name = "TENANT_ID")

and the @TenantTableDiscriminator metadata is SILENTLY ignored and will not apply to sub-entities that define @Multitenant(TABLE_PER_TENANT).

Another use case, given the following:

@MappedSuperclass
@Multitenant(SINGLE_TABLE)
public MySuperclass {
}
 
@Entity
@TenantDiscriminatorColumn(name = "T_ID", contextProperty = "my.tenant.id")
public MyEntity {
}

MyEntity's @TenantDiscriminatorColumn is ignored and the defaults will apply to the @Multitenant specification from MySuperclass (where the tenant discriminator column is defaulted).

Annotation definitions

@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 TenantDiscriminatorColumn {
    /**
     * (Optional) The name of column to be used for the discriminator.
     */
    String name() default "TENANT_ID";
 
    /**
     * (Optional) The name of the context property to apply to the 
     * tenant discriminator column.
     */
    String contextProperty() default "eclipselink.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. 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 definition of the table.
     */
    boolean primaryKey() default false; 
}
 
@Target({TYPE}) 
@Retention(RUNTIME)
public @interface TenantDiscriminatorColumns {
   /**
    * (Required) One or more <code>TenantDiscriminatorColumn</code> annotations.
    */
   TenantDiscriminatorColumn[] 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 
}

XML usage

The multitenant metadata in XML will be available at within the entity and mapped superclass element. When support for other multi-tenant types are added, their companion metadata should be surrounded by an xml choice element.

Eclipselink-orm.xml definition

  <xsd:complexType name="entity">
    <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="mapped-superclass">
    <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-column" type="orm:tenant-discriminator-column" minOccurs="0" maxOccurs="unbounded"/>
  </xsd:sequence>
  <xsd:attribute name="type" type="orm:multitenant-type"/>
</xsd:complexType>
 
<!-- **************************************************** -->
 
<xsd:complexType name="tenant-discriminator-column">
  <xsd:annotation>
    <xsd:documentation>
      ...
    </xsd:documentation>
  </xsd:annotation>
  <xsd:attribute name="name" type="xsd:string"/>
  <xsd:attribute name="context-property" 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), however to enable multi-tenancy @Multitenant or <multitenant> must be specified. Therefore the minimal configuration is:

@Entity
@Table(name="EMP")
@Multitenant
public Employee() {
  ...
}
 
 
// NOTE: The following example is NOT a minimal configuration. This would lead to no multi-tenancy as the solo @TableDiscriminator is 
// ignored since @Multitenant was not specified.
 
@Entity
@Table(name="EMP")
@TenantDiscriminatorColumn
public Employee() {
  ...
}
<entity class="model.Employee">
  <multitenant/>
  <table name="EMP"/>
  ...
</entity>

Persistence unit and entity mappings defaulting

Along with the availability from the entity and mapped superclass levels, the new metadata will available at the following levels to provide defaults. Usage at the these levels follows similar JPA metadata defaulting and overriding rules.

  • persistence-unit-defaults
  • entity-mappings

persistence-unit-defaults

In the eclipselink-orm.xml, it is possible to specify default tenant discriminator column metadata through the persistence unit metadata defaults. When defined at this level, it will apply to all entities of the persistence unit that have specified a multi-tenant type of SINGLE_TABLE minus those that specify their own tenant discriminator metadata.

Note: with no defaults, an entity not marked with multi-tenant metadata will not use any multi-tenancy strategy.

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

entity-mappings

Alternatively, users may specify tenant discriminator column metadata at the entity-mappings level as well which would override a persistence unit default and apply itself to all entities with a multitenant type of SINGLE_TABLE of the given mapping file (unless an individual entity has specified its own tenant discriminator metadata).

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

Mapped vs. Unmapped Tenant Discriminator

  • When a tenant discriminator column is mapped, its associated mapping attribute should be marked as read only. If it is not, an exception will be raised. With this restriction in place, a tenant discriminator column can not be part of the entity identifier (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 column mapping is populated from its associated context property.
  • Both mapped and unmapped properties are used to form the additional criteria when issuing a select query.
  • Unmapped tenant discriminator columns will require EclipseLink to populate the row with the tenant discriminator columns associated context 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 (and the metadata is IGNORED)
    • NOTE: multi-tenant metadata can be provided in a TABLE_PER_CLASS inheritance hierarchy.
  • When multiple properties map the same column.
  • Duplicate tenant discriminator columns will log a warning message, e.g.
    • @TenantDiscriminatorColumn(name="TENANT")
    • @TenantDiscriminatorColumn(name="TENANT", contextProperty="eclipselink.tenant-id")
  • A mapped tenant discriminator column 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)
@TenantDiscriminatorColumns({
    @TenantDiscriminatorColumn(name = "TENANT")
    @TenantDiscriminatorColumn(name = "TENANT", table = "SALARY")
  }
)
public Employee() {
  ...
}

More Examples

Annotation examples

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

XML examples

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

A shared entity manager factory will be configured by default. When using a shared entity manager factory, by default the cache (L2) is not shared, meaning multitenant entities will have an ISOLATED cache setting. If users want to share the cache, setting the eclipselink.multitenant.tenants-share-cache to true will allow this and multitenant entities will have a PROTECTED cache setting.

To not share the entity manager factory users can set the eclipselink.multitenant.tenants-share-emf property to false.

Swapping tenant ID during an active EntityManager is not allowed.

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.
  3. For reading, to enable the additional criteria (tenant discriminator columns) we will need to modify the ReadObjectQuery and ReadAllQuery slightly in the prepareForExecution method.
    1. There we will clone the translation row (if need be) and append the tenant discriminator columns to it before execution.

The tenant discriminator column(s) are assumed to exist on the primary table. If using secondary tables the tenant 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: Id generation is shared across persistence units (see future section below).

Core/Runtime Exceptions

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

Properties

The public default context property (eclipselink.tenant-id) definition will be available from:

  • org.eclipse.persistence.config.EntityManagerProperties.MULTITENANT_PROPERTY_DEFAULT
  • org.eclipse.persistence.config.PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT

Java example

EntityManager em = createEntityManager(MULTI_TENANT_PU);
em.setProperty("tenant.id", "707");
em.setProperty(EntityManagerProperties.MULTITENANT_PROPERTY_DEFAULT, "707");

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
  • update all
  • delete all

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.

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 (hasTenantDiscriminatorFields()) {
    for (DatabaseField discriminatorField : tenantDiscriminatorFields.keySet()) {
        getFields().add(buildField(discriminatorField));
    }
}

DefaultTableGenerator

Only a small change will be required to the DDL generator to ensure we capture a primary key setting for a tenant discriminator column. The following line will be added to the initTableSchema(ClassDescriptor) method:

isPKField = isPKField || dbField.isPrimaryKey();

Open/Future items

  1. Bug 355458: Provide 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 to achieve cache isolation.

Back to the top