Cmobilecom JPA 2.0.RC1 Developer Guide

23. Multitenant

Multitenancy allows multiple tenants to have physically or logically separated data in a system.

Separated Data Sources

Each tenant has its own persistence unit mapped to a separated data source. For example,
Tenant_1                       Tenant_2
-----------------------------------------------------
PU1: Database Schema_1         PU2: Database Schema_2
META-INF/Persistence.xml:
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
		version="2.2">

	<persistence-unit name="tenant_1" transaction-type="RESOURCE_LOCAL">
		<non-jta-datasource>jndiDataSourceName_schema_1</non-jta-datasource>
	</persistence-unit>

	<persistence-unit name="tenant_2" transaction-type="RESOURCE_LOCAL">
		<non-jta-datasource>jndiDataSourceName_schema_2</non-jta-datasource>
	</persistence-unit>
</persistence>
Create EntityManagerFactory and EntityManager for a tenant:
	// tenant_1
	EntityManagerFactory emf = Persistence.createEntityManagerFactory("tenant_1");
	EntityManager em = emf.createEntityManager();
	
	// tenant_2
	EntityManagerFactory emf2 = Persistence.createEntityManagerFactory("tenant_2");
	EntityManager em2 = emf2.createEntityManager();
Adding a new tenant requires adding a persistence unit in META-INF/persistence.xml.

Shared Table

All tenants share tables using discriminator columns. Tenant discriminator columns are not entity attributes, but they will be automatically added in SQL DDL and DML statements.

If a mapped superclass or entity class is configured as multitenant-enabled in an XML mapping file, its subclass entities will inherit the configuration as multitenant-enabled entities unless they are overridden.

For example, class hierarchy:

               AbstractEntity
          ------------------------------------------
           |          |          |                 |
         User        Role      Permission       Employee
                                                /      \
                                    FullTimeEmployee  PartTimeEmployee
orm.xml
<mapped-superclass class="com.cmobilecom.jpa.example.managed_classes.AbstractEntity">
	<multitenant>
		<discriminator-column name="tenant_id" type="java.lang.Long" 
			value-mapping-property="tenant.id"/>
	</multitenant>
</mapped-superclass>

<entity class="com.cmobilecom.jpa.example.managed_classes.Permission">
	<multitenant>
		<discriminator-column name="permission_group" type="java.lang.String" 
			length="30" value-mapping-property="permission.group"/>
	</multitenant>
</entity>

<entity class="com.cmobilecom.jpa.example.managed_classes.User">
	<multitenant enabled="false"/>
</entity>

The class hierarchy is configured as multitenant-enabled except the User entity. Permission entity uses a different tenant discriminator column than other entities of the class hierarchy.

The <discriminator-column> element specifies tenant discriminator column name, type, length and value mapping property whose value must be specified in the EntityManager, EntityManagerFactory or META-INF/persistence.xml. Discriminator column type should be number or string type such as java.lang.Long and java.lang.String. The length attribute is applicable to string type for schema generation.

Separated Persistence Units

Define a persistence-unit for each tenant in META-INF/persistence.xml and specifies the tenant discriminator column values. For example,
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
		version="2.2">

	<persistence-unit name="tenant_1" transaction-type="RESOURCE_LOCAL">
		<non-jta-datasource>jndiDataSourceName</non-jta-datasource>
	
		<properties>
			<property name="tenant.id" value="100"/>
			<property name="permission.group" value="group1"/>
		</properties>		
	</persistence-unit>

	<persistence-unit name="tenant_2" transaction-type="RESOURCE_LOCAL">
		<non-jta-datasource>jndiDataSourceName</non-jta-datasource>
	
		<properties>
			<property name="tenant.id" value="101"/>
			<property name="permission.group" value="group2"/>
		</properties>		
	</persistence-unit>
</persistence>
Create EntityManagerFactory and EntityManager for a tenant:
	// tenant_1
	EntityManagerFactory emf = Persistence.createEntityManagerFactory("tenant_1");
	EntityManager em = emf.createEntityManager();
	
	// tenant_2
	EntityManagerFactory emf2 = Persistence.createEntityManagerFactory("tenant_2");
	EntityManager em2 = emf2.createEntityManager();

Separated EntityManagerFactory(s)

All tenants share the same persistence-unit in META-INF/persistence.xml, but a separated EntityManagerFactory is created for each tenant. Tenant discriminator column values must be passed when creating a EntityManagerFactory for a tenant. For example,
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
		version="2.2">

	<persistence-unit name="all_tenants" transaction-type="RESOURCE_LOCAL">
		<non-jta-datasource>jndiDataSourceName</non-jta-datasource>
	</persistence-unit>
	
</persistence>
Create EntityManagerFactory and EntityManager for a tenant:
	// tenant_1
	Map<String, Object> tenant1_properties = new HashMap<>();
	tenant1_properties.put("tenant.id", 100);
	tenant1_properties.put("permission.group", "group1");
	EntityManagerFactory emf = Persistence.createEntityManagerFactory("all_tenants", tenant1_properties);
	EntityManager em = emf.createEntityManager();
	
	// tenant_2
	Map<String, Object> tenant2_properties = new HashMap<>();
	tenant2_properties.put("tenant.id", 101);
	tenant2_properties.put("permission.group", "group2");
	EntityManagerFactory emf2 = Persistence.createEntityManagerFactory("all_tenants", tenant2_properties);
	EntityManager em2 = emf2.createEntityManager();
Tenant discriminator column values set in an EntityManagerFactory must not be changed.

Separated EntityManager(s)

All tenants share the same persistence-unit in META-INF/persistence.xml and the same EntityManagerFactory, but a separated EntityManager is created for each tenant. Tenant discriminator column values must be passed when creating a EntityManager for a tenant. For example,
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
		version="2.2">

	<persistence-unit name="all_tenants" transaction-type="RESOURCE_LOCAL">
		<non-jta-datasource>jndiDataSourceName</non-jta-datasource>
	</persistence-unit>
	
</persistence>
Create one EntityManagerFactory shared by all tenants, and create one EntityManager for each tenant:
	EntityManagerFactory emf = Persistence.createEntityManagerFactory("all_tenants");
	
	// tenant_1
	Map<String, Object> tenant1_properties = new HashMap<>();
	tenant1_properties.put("tenant.id", 100);
	tenant1_properties.put("permission.group", "group1");
	EntityManager em = emf.createEntityManager(tenant1_properties);
	
	// tenant_2
	Map<String, Object> tenant2_properties = new HashMap<>();
	tenant2_properties.put("tenant.id", 101);
	tenant2_properties.put("permission.group", "group2");
	EntityManager em = emf.createEntityManager(tenant2_properties);
Tenant discriminator column values set in an EntityManager must not be changed. An EntityManager for one tenant must not be reused for a different tenant because

Primary Key

If an entity is multitenant enabled, but it does not have auto id generator, its identifier may not be unique among all tenants. So its tenant discriminator column must be a primary key column. Tenant discriminator column must be in all unique keys of primary and secondary tables. Schema generation will generate primary and unique keys, adding tenant discriminator column in the keys if necessary.

Caching

For separated-dataSource multitenancy or shared-table multitenancy with tenant discriminator column values specified in META-INF/persistence.xml or EntityManagerFactory, entities will be normally cached in 2nd-level cache according to caching configuration and hints since each tenant has its own cache.

For shared-table multitenancy with a shared EntityManagerFactory, the 2nd-level cache is shared among all tenants. If an entity is multitenant enabled, but it does not have auto id generator, the entity will not be cached since its identifier may not be unique among all tenants.

Fetch Tenant

When a query is to retrieve entities from all tenants, it is necessary to know which tenant a retrieved entity belongs to. To enable "fetch tenant" for an entity type, define a <fetch-tenant> element under <multitenant> element in ORM mapping file. For example,
<entity-mappings
		xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm cmobilecom-jpa-orm_1_0.xsd"
		version="2.2">
	<mapped-superclass class="com.cmobilecom.jpa.example.managed_classes.AbstractEntity" >
		<multitenant>
			<discriminator-column name="tenant_id" type="java.lang.Long"
					value-mapping-property="tenant.id"/>
			
			<fetch-tenant>
				<property name="tenantInfo" />
				<tenant entity="com.cmobilecom.jpa.example.managed_classes.Tenant">
					<fetch-attribute>name</fetch-attribute>
				</tenant>
			</fetch-tenant>
	
		</multitenant>
	</mapped-superclass>
</entity-mappings>
Refer to CmobilecomJPA ORM Extension Schema.

The mapped-superclass AbstractEntity defines a property "tenantInfo" whose value will be set to tenant discriminator value(Tenant id) followed by a separator(dot) and tenant name (e.g., 100.Tenant_1). The property is not an entity attribute. If there is no tenant entity defined, the property will be set to tenant discriminator value only. For example,

@MappedSuperclass
abstract public class AbstractEntity {
	@Transient
	private String tenantInfo;
	
	public String getTenantInfo() {
		return this.tenantInfo;
	}
	
	public void setTenantInfo(String tenantInfo) {
		this.tenantInfo = tenantInfo;
	}
}
	
@Entity
public class Employee extends AbstractEntity {
	@Id
	private String id;
	
	@Column(nullable=false, length=20)
	private String name;
}
	
@Entity
public class Tenant {
	@Id
	private Long id;
	
	@Column(nullable=false, length=30)
	private String name;
}
Example query: select all employees whose names match "John%" from all tenants.
	EntityManager em = emf.createEntityManager();
	TypedQuery<Employee> query = em.createQuery(
		"select e from Employee e where e.name like 'John%'", Employee.class);
	query.setHint(Constants.MULTITENANT_FETCH_TENANT, true);
	List<Employee> employees = query.getResultList();
The query hint Constants.MULTITENANT_FETCH_TENANT (true) tells CmobilecomJPA to retrieve Employee entities from all tenants, and fetch tenant information according to the <fetch-tenant> configuration.

Retrieved employees will contain tenant information. e.g.,

	Employee            Tenant Info
	--------------------------------
	123001, John A      100.Tenant_1
	000010, John B      101.Tenant_2
ValidationLicense InstallationFrames / No Frames