iDempiere implements true multi-tenancy at the database level, enabling multiple independent tenants (clients) to operate within a single system instance with complete data isolation and security.
Every table in iDempiere includes two mandatory columns for tenant isolation:
Standard Multi-Tenancy Columns
public interface TenantColumns { /** * Client/Tenant ID * Isolates data between different business entities */ String COLUMNNAME_AD_Client_ID = "AD_Client_ID"; /** * Organization ID * Subdivides data within a client */ String COLUMNNAME_AD_Org_ID = "AD_Org_ID";}
package org.compiere.model;public abstract class PO implements Serializable { /** * Constructor with source PO and tenant override * @param ctx context * @param source source PO to copy from * @param AD_Client_ID client ID * @param AD_Org_ID organization ID */ public PO(Properties ctx, PO source, int AD_Client_ID, int AD_Org_ID) { this(ctx, 0, null); // Copy values from source copyValues(source, this); // Override tenant fields setAD_Client_ID(AD_Client_ID); setAD_Org_ID(AD_Org_ID); } /** * Set AD_Client_ID - called automatically for new records * @param AD_Client_ID client ID */ final protected void setAD_Client_ID(int AD_Client_ID) { set_ValueNoCheck("AD_Client_ID", Integer.valueOf(AD_Client_ID)); } /** * Get AD_Client_ID * @return client ID */ public final int getAD_Client_ID() { Integer ii = (Integer)get_Value("AD_Client_ID"); if (ii == null) return 0; return ii.intValue(); } /** * Set AD_Org_ID - called automatically for new records * @param AD_Org_ID organization ID */ final protected void setAD_Org_ID(int AD_Org_ID) { set_ValueNoCheck("AD_Org_ID", Integer.valueOf(AD_Org_ID)); } /** * Get AD_Org_ID * @return organization ID */ public final int getAD_Org_ID() { Integer ii = (Integer)get_Value("AD_Org_ID"); if (ii == null) return 0; return ii.intValue(); }}
When creating new records, iDempiere automatically sets AD_Client_ID and AD_Org_ID from the context (session), ensuring records are created in the correct tenant.
public abstract class PO implements Serializable { /** * Validate cross-tenant foreign key references * Called automatically before save * @throws CrossTenantException if violation detected */ private void validateCrossTenant() { // Check if writing to different tenant if (!is_new()) { int sessionClientId = Env.getAD_Client_ID(p_ctx); if (sessionClientId != getAD_Client_ID()) { throw new CrossTenantException(true, get_TableName(), get_ID()); } } // Validate foreign key references for (int i = 0; i < get_ColumnCount(); i++) { if (is_ValueChanged(i)) { Object fkval = get_Value(i); String fkcol = get_ColumnName(i); // Check if this is a foreign key column if (fkcol.endsWith("_ID") && fkval instanceof Integer) { int fkId = (Integer)fkval; if (fkId > 0) { // Validate FK references same tenant String fkTable = fkcol.substring(0, fkcol.length() - 3); if (isTenantValidated(fkTable)) { int fkClientId = getClientIdForRecord(fkTable, fkId); if (fkClientId != getAD_Client_ID()) { throw new CrossTenantException(fkval, fkcol); } } } } } } }}
CrossTenantException is thrown automatically when code attempts to:
Save a record belonging to a different client
Set a foreign key referencing a record from another client
Read a record from another client (when security is enforced)
Organizations can have parent-child relationships:
import org.compiere.model.MOrg;// Get organizationMOrg org = new MOrg(ctx, orgId, null);// Check if organization is summary (has children)if (org.isSummary()) { // This org has child organizations log.info("Summary organization: " + org.getName());}// Get parent organizationif (org.getParent_Org_ID() > 0) { MOrg parent = new MOrg(ctx, org.getParent_Org_ID(), null); log.info("Parent org: " + parent.getName());}// Get all organizations accessible to current roleMRole role = MRole.get(ctx, roleId);List<Integer> accessibleOrgs = role.getOrgAccess();// Check if user can access specific organizationboolean canAccess = role.isOrgAccess(orgId, false);
// System records have AD_Client_ID=0MCountry country = MCountry.get(ctx, "US");log.info("Client ID: " + country.getAD_Client_ID()); // 0// Accessible from any clientint currentClient = Env.getAD_Client_ID(ctx); // e.g., 1000000// country is still accessible
Data isolated per tenant:
Business Partners - Customers, vendors, employees
Products - Items, services, resources
Transactions - Orders, invoices, payments
Accounting - Chart of accounts, journals
// Client-specific recordsMBPartner partner = new MBPartner(ctx, partnerId, null);log.info("Client ID: " + partner.getAD_Client_ID()); // 1000000// Only accessible to same client// CrossTenantException thrown if accessed from different client
Data accessible based on organization hierarchy:
Warehouses - Inventory locations
Locators - Storage bins
Cash Books - Cash management
Bank Accounts - Financial accounts
// Org-specific access controlled by roleMRole role = MRole.get(ctx, roleId);// User can access organizations based on role definitionif (role.isOrgAccess(warehouse.getAD_Org_ID(), false)) { // Warehouse is accessible}
Never hardcode client or organization IDs. Always retrieve from context:
// Badorder.setAD_Client_ID(1000000);order.setAD_Org_ID(1000000);// Goodorder.setAD_Client_ID(Env.getAD_Client_ID(ctx));order.setAD_Org_ID(Env.getAD_Org_ID(ctx));// Best - automatic from constructorMOrder order = new MOrder(ctx, 0, trxName);// Client and Org set automatically
Validate Foreign Keys
Ensure foreign key references are within the same tenant:
// Validate partner belongs to same clientMBPartner partner = new MBPartner(ctx, partnerId, null);if (partner.getAD_Client_ID() != order.getAD_Client_ID()) { throw new AdempiereException("Partner from different client");}
iDempiere does this automatically, but explicit checks add clarity.
Use Query Filters
Always apply client filtering in queries:
// Explicit client filterList<MProduct> products = new Query(ctx, MProduct.Table_Name, "AD_Client_ID=?", trxName) .setParameters(Env.getAD_Client_ID(ctx)) .list();// Or use built-in filterList<MProduct> products = new Query(ctx, MProduct.Table_Name, null, trxName) .setClient_ID() // Adds filter automatically .list();
Test with Multiple Tenants
Always test customizations with multiple clients to verify isolation:
// Test helperpublic void testMultiTenantIsolation() { // Create records in client 1 Env.setContext(ctx, "#AD_Client_ID", 1000000); MOrder order1 = createTestOrder(ctx); // Switch to client 2 Env.setContext(ctx, "#AD_Client_ID", 1000001); // Verify order1 is not accessible MOrder test = new MOrder(ctx, order1.getC_Order_ID(), null); assertNull(test, "Order from client 1 should not be accessible");}
When developing plugins for multi-tenant environments:
import org.compiere.model.ModelValidator;public class CustomValidator implements ModelValidator { private int m_AD_Client_ID = -1; @Override public void initialize(ModelValidationEngine engine, MClient client) { // Validator is initialized per client m_AD_Client_ID = client.getAD_Client_ID(); log.info("Initializing validator for client: " + client.getName()); // Register for events in this client only engine.addModelChange(MOrder.Table_Name, this); } @Override public int getAD_Client_ID() { return m_AD_Client_ID; } @Override public String modelChange(PO po, int type) { // This method is only called for records in this client // po.getAD_Client_ID() == m_AD_Client_ID return null; }}
Model validators are initialized once per client. Static data in validators may be shared across all clients - use instance variables for client-specific state.