June 2, 2011

MOXy Extensible Models - Multi-Tenant Example

In a multi-tenant architecture a single application runs on a server, serving multiple client organizations (tenants).  Good multi-tenant applications allow per-tenant customizations.  When these customizations are made to data, it can be difficult for the binding layer to handle them.  In this post I'll demonstrate how to leverage EclipseLink JAXB (MOXy)'s support for extensible models to easily handle this use case.


Java Model

The following will be used as our domain model for this example.  The real properties represent the parts of the model that will be common to all tenants.  The per-tenant extensions will be represented as virtual properties.

ExtensibleBase

The @XmlVirtualAccessMethods annotation is used to specify that a class is extensible.  An extensible class is required to have a "get" method that returns a value by property name, and a "set" method that stores a value by property name.  The default names for these methods are "get" and "set", and can be overridden with the @XmlVirtualAccessMethods annotation.  Since we will have multiple extensible classes in this example we'll configure a base class for this behaviour that extensible classes can extend.  We will use the @XmlTransient annotation to prevent ExtensibleBase from being mapped as an inheritance relationship.

package blog.multitenant;

import java.util.HashMap;
import java.util.Map;

import javax.xml.bind.annotation.XmlTransient;

import org.eclipse.persistence.oxm.annotations.XmlVirtualAccessMethods;

@XmlTransient
@XmlVirtualAccessMethods(setMethod="put")
public class ExtensibleBase {

    private Map<String, Object> extensions = new HashMap<String, Object>();

    public <T> T get(String property) {
        return (T) extensions.get(property);
    }

    public void put(String property, Object value) {
        extensions.put(property, value);
    }

}

Customer

The Customer class will be extensible since it inherits from a domain class that has been annotated with @XmlVirtualAccessMethods.

package blog.multitenant;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Customer extends ExtensibleBase {

    private String firstName;
    private String lastName;
    private Address billingAddress;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Address getBillingAddress() {
        return billingAddress;
    }

    public void setBillingAddress(Address billingAddress) {
        this.billingAddress = billingAddress;
    }

}

Address

It is not necessary to have every class in your model be extensible.  In this example the Address class will not have any virtual properties.

package blog.multitenant;

public class Address {

    private String street;

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

}

PhoneNumber

PhoneNumber like Customer will be an extensible class.

package blog.multitenant;

import javax.xml.bind.annotation.XmlValue;

public class PhoneNumber extends ExtensibleBase {

    private String number;

    @XmlValue
    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

}

Tenant 1

Our first tenant is an online sporting goods store, that requires the following extensions to their model:
  • Customer ID
  • Customer's middle name
  • Shipping address
  • A collection of contact phone numbers
  • Type of phone number (i.e. home, work, or cell)

Mapping File (blog/multitenant/binding-tenant1.xml)

The metadata for the virtual properties is supplied through MOXy's XML mapping file.  Virtual properties are mapped in the same way as real properties.  Some additional information is required including type (since this can not be determined via reflection), and for collection properties a container type.

<?xml version="1.0"?>
<xml-bindings
    xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm"
    package-name="blog.multitenant">
    <java-types>
        <java-type name="Customer">
            <xml-type prop-order="firstName middleName lastName billingAddress shippingAddress phoneNumbers"/>
            <java-attributes>
                <xml-attribute 
                    java-attribute="id" 
                    type="java.lang.Integer"/>
                <xml-element 
                    java-attribute="middleName" 
                    type="java.lang.String"/>
                <xml-element 
                    java-attribute="shippingAddress" 
                    type="blog.multitenant.Address"/>
                <xml-element 
                    java-attribute="phoneNumbers" 
                    name="phoneNumber" 
                    type="blog.multitenant.PhoneNumber" 
                    container-type="java.util.List"/>
            </java-attributes>
        </java-type>
        <java-type name="PhoneNumber">
            <java-attributes>
                <xml-attribute 
                    java-attribute="type" 
                    type="java.lang.String"/>
            </java-attributes>
        </java-type>
    </java-types>
</xml-bindings>

Demo

The get/set methods are used on the domain model to interact with the real properties and the accessors defined on the @XmlVirtualAccessMethods annotation are used to interact with the virtual properties.  The normal  JAXB mechanisms are used for marshal and unmarshal operations:

package blog.multitenant;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

import org.eclipse.persistence.jaxb.JAXBContextFactory;

public class DemoTenant1 {

    public static void main(String[] args) throws Exception {
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put(JAXBContextFactory.ECLIPSELINK_OXM_XML_KEY, "blog/multitenant/binding-tenant1.xml");
        JAXBContext jc = JAXBContext.newInstance(new Class[] {Customer.class, Address.class}, properties);

        Customer customer = new Customer();
        customer.setFirstName("Jane");
        customer.put("middleName", "Anne");
        customer.setLastName("Doe");

        Address billingAddress = new Address();
        billingAddress.setStreet("1 Billing Street");
        customer.setBillingAddress(billingAddress);

        Address shippingAddress = new Address();
        shippingAddress.setStreet("2 Shipping Road");
        customer.put("shipping", shippingAddress);

        List<PhoneNumber> phoneNumbers = new ArrayList<PhoneNumber>();
        customer.put("phoneNumbers", phoneNumbers);

        PhoneNumber workPhoneNumber = new PhoneNumber();
        workPhoneNumber.setNumber("555-WORK");
        workPhoneNumber.put("type", "WORK");
        phoneNumbers.add(workPhoneNumber);

        PhoneNumber homePhoneNumber = new PhoneNumber();
        homePhoneNumber.setNumber("555-HOME");
        homePhoneNumber.put("type", "HOME");
        phoneNumbers.add(homePhoneNumber);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(customer, System.out);
    }

}

Output

As you can see below there is nothing special in XML related to real or virtual properties:

<?xml version="1.0" encoding="UTF-8"?>
<customer>
   <firstName>Jane</firstName>
   <middleName>Anne</middleName>
   <lastName>Doe</lastName>
   <billingAddress>
      <street>1 Billing Street</street>
   </billingAddress>
   <phoneNumber type="WORK">555-WORK</phoneNumber>
   <phoneNumber type="HOME">555-HOME</phoneNumber>
</customer>

Tenant 2

Our second tenant is streaming media provider that offers on-demand movies and music to it's subscribers.  It requires a different set of extensions to the core model:
  • A single contact phone number

Mapping File (blog/multitenant/binding-tenant2.xml)

Again the virtual properties are defined through MOXy's XML mapping file.  For this tenant we will also leverage the mapping file to customize the mapping of the real properties:

<?xml version="1.0"?>
<xml-bindings
    xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm"
    package-name="blog.multitenant">
    <xml-schema namespace="urn:tenant1" element-form-default="QUALIFIED"/>
    <java-types>
        <java-type name="Customer">
            <xml-type prop-order="firstName lastName billingAddress phoneNumber"/>
            <java-attributes>
                <xml-attribute java-attribute="firstName"/>
                <xml-attribute java-attribute="lastName"/>
                <xml-element java-attribute="billingAddress" name="address"/>
                <xml-element 
                    java-attribute="phoneNumber" 
                    type="blog.multitenant.PhoneNumber"/>
            </java-attributes>
        </java-type>
    </java-types>
</xml-bindings>

Demo

package blog.multitenant;

import java.util.HashMap;
import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

import org.eclipse.persistence.jaxb.JAXBContextFactory;

public class DemoTenant2 {

    public static void main(String[] args) throws Exception {
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put(JAXBContextFactory.ECLIPSELINK_OXM_XML_KEY, "blog/multitenant/binding-tenant2.xml");
        JAXBContext jc = JAXBContext.newInstance(new Class[] {Customer.class, Address.class}, properties);

        Customer customer = new Customer();
        customer.setFirstName("Jane");
        customer.setLastName("Doe");

        Address billingAddress = new Address();
        billingAddress.setStreet("1 Billing Street");
        customer.setBillingAddress(billingAddress);

        PhoneNumber phoneNumber = new PhoneNumber();
        phoneNumber.setNumber("555-WORK");
        customer.put("phoneNumber", phoneNumber);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(customer, System.out);
    }

}

Output

Note that even though both tenants share several real properties, the corresponding XML representation can be quite different:

<?xml version="1.0" encoding="UTF-8"?>
<customer xmlns="urn:tenant1" firstName="Jane" lastName="Doe">
   <address>
      <street>1 Billing Street</street>
   </address>
   <phoneNumber>555-WORK</phoneNumber>
</customer>

Further Reading

If you enjoyed this post you may also be interested in:
Other articles related to new MOXy features in EclipseLink 2.3:

9 comments:

  1. That's very well explained. In a multi-tenant application , can we use MOXy Extensible Models to save data for different clients in the same table. For example-tenant1 is interested in saving the middle name of the customer while tenant2 is not. Middle name in this case is a virtual field. How can we implement that?


    ----Rampal

    ReplyDelete
  2. Hi Rampal,

    EclipseLink JPA has a corresponding mechanism for persisting extensible models to the database. For an example see:
    - Example - EclipseLink Extensible Entities

    -Blaise

    ReplyDelete
  3. Thanks Blaise . It helped..

    -Rampal

    ReplyDelete
  4. Hi Blaise ,
    How can we develop a multi-tenant application using flexi-fields with spring and eclipse-link. If we are using flexifields and EMF per tenant, then how do we get EMF from spring . Do we have override org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean??? And even if we override it, how can we use spring's transactionManager on each EMF using @transactional annotation?

    ReplyDelete
  5. Hi,

    Would you mind asking this question on the EclipseLink forum or EclipseLink users mailing list (eclipselink-users@eclipse.org)? That way we can get the EclipseLink JPA folks in on the discussion too.

    -Blaise

    ReplyDelete
  6. Hi, very interesting article.

    I have been playing with extensible entities in EclipseLink 2.4 nightly, both for JPA and MOXy. While JPA support allows declaring a custom class that implements a custom metadata source (so that metadata for extension fields need not be in a XML file), I could not find anything similar in MOXy. Does MOXy provide a similar feature?

    I looked into the org.eclipse.persistence.jaxb.* classes but could not find anything like that. I suppose that the metadata could be provided as instances of XmlBindings but I could not find a simple way to pass XmlBindings to the JAXB context.

    ReplyDelete
  7. Hi Blaise Doughan,

    Hope you are doing well!

    We have application that used by different clients. Currently our application nature was like... each client shared same code base with same database table.

    Now we want to implement application like... each client shared same code but different databases. For example client1 have database1 with all tables. Client2 have database2 with all tables.

    We want to implement this type of concept using multi-tenant. Currently we used J2EE EclipseLink with JPA.

    Can you please suggest or explain with one small example it would be great help for us?

    Thanks a lot!

    ReplyDelete

Note: Only a member of this blog may post a comment.