Cmobilecom JPA 2.0.1 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 necessarily mapped to entity attributes, but they will be automatically added in SQL DDL and DML statements if needed.

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:

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

<entity class="com.cmobilecom.jpa.example.managed_classes.User">
	<multitenant enabled="false"/>
</entity>
	
<entity class="com.cmobilecom.jpa.example.managed_classes.Role">
	<multitenant>
		
		<discriminator-column nullable="true"/>
	</multitenant>
</entity>	
	
<entity class="com.cmobilecom.jpa.example.managed_classes.Permission">
	<multitenant enabled="false"/>
</entity>

The class hierarchy is configured as multitenant-enabled except the User and Permission entities.

The <multitenant> element defines multitenant configuration of a managed type hierarchy:

Refer to Cmobilecom-JPA ORM Extension Schema for detail.

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"/>
		</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"/>
		</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 in a property map 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);
	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);
	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);
	EntityManager em = emf.createEntityManager(tenant1_properties);
	
	// tenant_2
	Map<String, Object> tenant2_properties = new HashMap<>();
	tenant2_properties.put("tenant.id", 101);
	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

Application Managed Multitenancy

When an EntityManager is to manage data of all tenants in shared-table multitenant strategy, one option is to define a new persistence unit with the same metamodel except that: The advantage of this approach is that all entities from different tenants have unique identifiers. The disadvantage is that another persistence unit with minor difference has to be created, and its metamodel has to be built at runtime which will incurs CPU and memory overhead.

The other option is application-managed multitenancy. When all multitenant-enabled entity types(except dependent entities) have id generators to ensure that no two entities from different tenants have the same identifier in a persistence context, this option will be better since it uses the same persistence unit and will not build a new metamodel.

Define multitenant attribute in multitenant configuration and set the property Constants.MULTITENANT_APP_MANAGED to true in EntityManager to enable application-managed multitenancy. The property can also be set in EntityManagerFactory or persistence.xml if appropriate, and shared by all EntityManagers. The property must not be changed once set.

When the attribute of multitenant configuration is specified, the attribute will be managed as a regular entity attribute by Cmobilecom-JPA and be a part of metamodel.

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.AbstractEntityBase" >
		<multitenant>
			<discriminator-column name="tenant_id" type="java.lang.Long"
					value-mapping-property="tenant.id"
					tenant-entity="com.cmobilecom.jpa.example.managed_classes.TenantImpl"
					attribute="tenant"/>
		</multitenant>
	</mapped-superclass>
	
</entity-mappings>
Managed classes:
@MappedSuperclass
abstract public class AbstractEntityBase {
	@Transient
	private Tenant tenant;
	
	public Tenant getTenant() {
		return this.tenant;
	}
	
	public void setTenant(Tenant tenant) {
		this.tenant = tenant;
	}
	
	...
}

public interface Tenant {
	public Long getId();
	
	public String getName();
}

@Entity
public class Employee extends AbstractEntityBase {
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private String id;
	
	@Column(nullable=false, length=20)
	private String name;
}

@Entity
public class TenantImpl implements Tenant {
	@Id
	private Long id;
	
	@Column(nullable=false, length=30)
	private String name;

	// getters and setters
}
The classes above may be separated in different modules. The attribute "tenant" is defined as transient, but it will be managed as a regular attribute.

Example code for application-managed multitenancy:

	EntityManager em = emf.createEntityManager();
	em.setProperty(Constants.MULTITENANT_ATTRIBUTE_APP_MANAGED, true);
	
	// persist an entity of Tenant 1
	Employee employeeA = new Employee();
	employeeA.setName("John A");
	employeeA.setTenant(em.getReference(TenantImpl.class, 1L));
	em.persist(employeeA);
	
	// persist an entity of Tenant 2
	Employee employeeB = new Employee();
	employeeB.setName("John B");
	employeeB.setTenant(em.getReference(TenantImpl.class, 2L));
	em.persist(employeeB);
	
	// query entities from all tenants
	TypedQuery<Employee> query = em.createQuery(
		"select e from Employee e where e.name like 'John%'", Employee.class);
	List<Employee> employees = query.getResultList();
	
	// query entities from one tenant
	TypedQuery<Employee> query = em.createQuery(
		"select e from Employee e where e.tenant.id=1 and e.name like 'John%'", Employee.class);
	List<Employee> employees = query.getResultList();

Retrieved employees will have tenant attribute values fetched. e.g.,

	id      name        tenant
	------------------------------
	1       John A      Tenant 1
	2       John B      Tenant 2

Primary Key and Caching

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.

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.

ValidationLicense InstallationFrames / No Frames