This chapter provides instructions for making JPA entities and JAXB beans extensible. Mappings can be added or modified externally, without modifying the entity or bean source file and without redeploying the persistence unit. This feature is useful in a Software-as-a-Service environment where multiple clients can share applications and data sources. It is also useful for customizing an application during installation rather than during development.
This chapter contains the following sections:
Users want to establish a SaaS environment, where applications and data sources are shared by multiple clients.
Use the TopLink extensibility feature to extend JPA entities and JAXB beans by using external mappings.
TopLink 12c Release 1 (12.1.2) or later.
Note:
TopLink's core functionality is provided by EclipseLink, the open source persistence framework from the Eclipse Foundation. EclipseLink implements Java Persistence API (JPA), Java Architecture for XML Binding (JAXB), and other standards-based persistence technologies, plus extensions to those standards. TopLink includes all of EclipseLink, plus additional functionality from Oracle.
Use the @VirtualAccessMethods
annotation to specify that an entity is extensible. By using virtual properties in an extensible entity, you can specify mappings external to the entity. This allows you to modify the mappings without modifying the entity source file and without redeploying the entity's persistence unit.
Extensible entities are useful in a multi-tenant (or SaaS) architecture where a shared, generic application can be used by multiple clients (tenants). Tenants have private access to their own data, and to data shared with other tenants.
Using extensible entities, you can:
Build an application where some mappings are common to all users and some mappings are user-specific.
Add mappings to an application after it is made available to a customer (even post-deployment).
Use the same EntityManagerFactory
interface to work with data after mappings have changed.
Provide an additional source of metadata to be used by an application.
To create and support an extensible JPA entity:
Configure the entity by annotating the entity class with @VirtualAccessMethods
(or using the XML <access-methods>
), adding get
and set
methods for the property values, and adding a data structure to store the extended attributes and values, as described in the following sections:
Annotate the entity with @VirtualAccessMethods
to specify that it is extensible and to define virtual properties.
Table 12-1 describes the attributes available to the @VirtualAccessMethods
annotation.
Table 12-1 Attributes for the @VirtualAccessMethods Annotation
Attribute | Description |
---|---|
|
The name of the Default: Required? No |
|
The name of the Default: Required? No |
Add get(String)
and set(String, Object)
methods to the entity. The get()
method returns a value by property name and the set()
method stores a value by property name. The default names for these methods are get
and set
, and they can be overridden with the @VirtualAccessMethods
annotation.
EclipseLink weaves these methods if weaving is enabled, which provides support for lazy loading, change tracking, fetch groups, and internal optimizations.
Note:
Weaving is not supported when using virtual access methods with OneToOne
mappings. If attempted, an exception will be thrown.
Add a data structure to store the extended attributes and values, that is, the virtual mappings. These can then be mapped to the database. See Section 12.1.1.3, "Task 3: Provide Additional Mappings."
A common way to store the virtual mappings is in a Map
object (as shown in Example 12-1), but you can also use other strategies.
When using field-based access, annotate the data structure with @Transient
so the structure cannot be used for another mapping. When using property-based access, @Transient
is unnecessary.
Example 12-1 illustrates an entity class that uses property access.
Example 12-1 Entity Class that Uses Property Access
@Entity @VirtualAccessMethods public class Customer{ @Id private int id; ... @Transient private Map<String, Object> extensions; public <T> T get(String name) { return (T) extentions.get(name); } public Object set(String name, Object value) { return extensions.put(name, value); }
As an alternative to, or in addition to, using the @VirtualAccessMethods
annotation, you can use an access="VIRTUAL"
attribute on a mapping element (such as <basic>
), for example:
<basic name="idNumber" access="VIRTUAL" attribute-type="String"> <column name="FLEX_COL1"/> </basic>
To set virtual access methods as the defaults for the persistence unit, use the <access>
and <access-methods>
elements, for example:
<persistence-unit-metadata> <xml-mapping-metadata-complete/> <exclude-default-mappings/> <persistence-unit-defaults> <access>VIRTUAL</access> <access-methods set-method="get" get-method="set"/> </persistence-unit-defaults> </persistence-unit-metadata>
Provide database tables with extra columns to store virtual attribute values. For example, the following Customer
table includes two predefined columns, ID
and NAME
, and three columns for storing the attribute values, EXT_1
, EXT_2
, EXT_3
:
CUSTOMER
table
INTEGER
ID
VARCHAR
NAME
VARCHAR
EXT_1
VARCHAR
EXT_2
VARCHAR
EXT_3
You can then specify which of the FLEX
columns should be used to persist an extended attribute, as described in "Task 3: Provide Additional Mappings".
To provide additional mappings, add the mappings with the column
and access-methods
attributes to the eclipselink-orm.xml
file, for example:
<basic name="idNumber" access="VIRTUAL" attribute-type="String"> <column name="FLEX_COL1"/> </basic>
Configure persistence unit properties to indicate that the application should retrieve the flexible mappings from the eclipselink-orm.xml
file. You can set persistence unit properties using the persistence.xml
file or by setting properties on the EntityManagerFactory
interface, as described in the following sections.
For more information about external mappings, see Chapter 13, "Using an External MetaData Source."
In the persistence.xml
file, use the eclipselink.metadata-source
property to use the default eclipselink-orm.xml
file. Use the eclipselink.metadata-source.xml.url
property to use a different file at the specified location, for example:
<property name="eclipselink.metadata-source" value="XML"/> <property name="eclipselink.metadata-source.xml.url" value="foo://bar"/>
Extensions are added at bootstrap time through access to a metadata repository. The metadata repository is accessed through a class that provides methods to retrieve the metadata it holds. EclipseLink includes a metadata repository implementation that supports XML repositories.
Specify the class to use and any configuration information for the metadata repository through persistence unit properties. The EntityManagerFactory
interface integrates additional mapping information from the metadata repository into the metadata it uses to bootstrap.
You can provide your own implementation of the class to access the metadata repository. Each metadata repository access class must specify an individual set of properties to use to connect to the repository.
You can subclass either of the following classes:
org.eclipse.persistence.internal.jpa.extensions.MetadataRepository
org.eclipse.persistence.internal.jpa.extensions.XMLMetadataRepository
In the following example, the properties that begin with com.foo
are subclasses defined by the developer.
<property name="eclipselink.metadata-source" value="com.foo.MetadataRepository"/> <property name="com.foo.MetadataRepository.location" value="foo://bar"/> <property name="com.foo.MetadataRepository.extra-data" value="foo-bar"/>
If you change the metadata and you want an EntityManager
instance based on the new metadata, you must call the refreshMetadata()
method on the EntityManagerFactory
interface to refresh the data. The next EntityManager
instance will be based on the new metadata.
The refreshMetadata()
method takes a map of properties that can be used to override the properties previously defined for the metadata-source
element.
Example 12-2 illustrates the following:
Field access is used for non-extension fields.
Virtual access is used for extension fields, using defaults (get(String)
and set(String, Object)
).
The get(String)
and set(String, Object)
methods will be woven, even if no mappings use them, because of the presence of @VirtualAccessMethods
.
These items are illustrated in bold font.
Example 12-2 Virtual Access Using Default get and set Method Names
@Entity @VirtualAccessMethods public class Address { @Id private int id; @Transient private Map<String, Object> extensions; public int getId(){ return id; } public <T> T get(String name) { return (T) extentions.get(name); } public Object set(String name, Object value) { return extensions.put(name, value); } . . .
Example 12-3 illustrates the following:
Field access is used for non-extension fields.
The @VirtualAccessMethods
annotation overrides methods to be used for getting and setting.
The get(String)
and set(String, Object)
methods will be woven, even if no mappings use them, because of the presence of @VirtualAccessMethods
.
The XML for extended mapping indicates which get()
and set()
method to use.
These items are illustrated in bold font.
Example 12-3 Overriding get and set Methods
@Entity @VirtualAccessMethods(get="getExtension", set="setExtension") public class Address { @Id private int id; @Transient private Map<String, Object> extensions; public int getId(){ return id; } public <T> T getExtension(String name) { return (T) extensions.get(name); } public Object setExtension(String name, Object value) { return extensions.put(name, value); } ... <basic name="name" access="VIRTUAL" attribute-type="String"> <column name="FLEX_1"/> </basic>
Example 12-4 illustrates the following:
Property access is used for non-extension fields.
Virtual access is used for extension fields, using defaults (get(String)
and set(String, Object)
).
The extensions are mapped in a portable way. @Transient
is not required, because property access is used.
The get(String)
and set(String, Object)
methods will be woven, even if no mappings use them, because of the presence of @VirtualAccessMethods
.
These items are illustrated in bold font.
Example 12-4 Using Property Access
@Entity @VirtualAccessMethods public class Address { private int id; private Map<String, Object> extensions; @Id public int getId(){ return id; } public <T> T get(String name) { return (T) extensions.get(name); } public Object set(String name, Object value) { return extensions.put(name, value); } ...
Use the @XmlVirtualAccessMethods
annotation to specify that a JAXB bean is extensible. By using virtual properties in an extensible bean, you can specify mappings external to the bean. This allows you to modify the mappings without modifying the bean source file and without redeploying the bean's persistence unit.
In a multi-tenant (or SaaS) 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. JAXB is designed to work with domain models that have real fields and properties. EclipseLink extensions to JAXB introduce the concept of virtual properties which can easily handle this use case. Virtual properties are defined by the Object-XML metadata file, and provide a way to extend a class without modifying the source.
This section has the following subsections:
To create and support an extensible JAXB bean:
Configure the bean by annotating the bean class with the @XmlVirtualAccessMethods
, adding get
and set
methods for the property values, and adding a data structure to store the extended attributes and values. Alternatively, you can use the <xml-virtual-access-methods>
element in eclipselink-orm.xml
.
Annotate the bean with @XmlVirtualAccessMethods
to specify that it is extensible and to define virtual properties.
Table 12-2 describes the attributes available to the @XmlVirtualAccessMethods
annotation.
Table 12-2 Attributes for the @XmlVirtualAccessMethods Annotation
Attribute | Description |
---|---|
|
The name of the getter method to use for the virtual property. This method must take a single Default: Required? No |
|
The name of the setter method to use for the virtual property. This method must take a Default: Required? No |
Add get(String)
and set(String, Object)
methods to the bean. The get()
method returns a value by property name and the set()
method stores a value by property name. The default names for these methods are get
and set
, and they can be overridden with the @XmlVirtualAccessMethods
annotation.
Add a data structure to store the extended attributes and values, that is, the virtual mappings. These can then be mapped to the database. See "Task 2: Provide Additional Mappings".
A common way to store the virtual mappings is in a Map
, but you can use other ways, as well. For example you could store the virtual mappings in a directory system.
When using field-based access, annotate the data structure with @XmlTransient
so it cannot use it for another mapping. When using property-based access, @XmlTransient
is unnecessary.
As an alternative to, or in addition to, using @XmlVirtualAccessMethods
, you can use the XML equivalents, for example:
XML to enable virtual access methods using get
and set
:
<xml-virtual-access-methods/>
XML to enable virtual access methods using put
instead of set
(default):
<xml-virtual-access-methods set-method="put"/>
XML to enable virtual access methods using retrieve
instead of get
(default):
<xml-virtual-access-methods get-method="retrieve"/>
XML to enable virtual access methods using retrieve
and put
instead of get
and set
(default):
<xml-virtual-access-methods get-method="retrieve" set-method="put"/>
To provide additional mappings, add the mappings to the eclipselink-oxm.xml
file, for example:
<xml-element java-attribute="idNumber"/>
The examples in this section illustrate how to use extensible JAXB beans. The example begins with the creation of a base class that other classes can extend. In this case the extensible classes are for Customers
and PhoneNumbers
. Mapping files are created for two separate tenants. Even though both tenants share several real properties, they will define virtual properties that are unique to their requirements.
Example 12-5 illustrates a base class, ExtensibleBase
, which other extensible classes can extend. In the example, the use of the @XmlTransient
annotation prevents ExtensibleBase
from being mapped as an inheritance relationship. 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.
Example 12-5 A Base Class for Extensible Classes
package examples.virtual; 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); } }
Example 12-6 illustrates the definition of a Customer
class. The Customer
class is extensible because it inherits from a domain class that has been annotated with @XmlVirtualAccessMethods
.
Example 12-6 An Extensible Customer Class
package examples.virtual;
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;
}
}
Example 12-7 illustrates an Address
class. It is not necessary for every class in your model to be extensible. In this example, the Address
class does not have any virtual properties.
Example 12-7 A Nonextensible Address Class
package examples.virtual; public class Address { private String street; public String getStreet() { return street; } public void setStreet(String street) { this.street = street; } }
Example 12-8 illustrates a PhoneNumber
class. Like Customer
, PhoneNumber
will be an extensible class.
Example 12-8 An Extensible PhoneNumber Class
package examples.virtual;
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;
}
}
The examples in this section define two separate tenants. Even though both tenants share several real properties, the corresponding XML representation can be quite different due to virtual properties.
The first tenant is an online sporting goods store that requires the following extensions to its model:
Customer ID
Customer's middle name
Shipping address
A collection of contact phone numbers
Type of phone number (that is, home, work, or cell)
The metadata for the virtual properties is captured in the eclipselink-oxm.xml
mapping file or in files using the eclipselink-orm.xml
schema. Virtual properties are mapped in the same way as real properties. Some additional information is required, including type (since this cannot be determined through reflection), and for collection properties, a container type. The virtual properties defined below for Customer
are middleName
, shippingAddress
, and phoneNumbers
. For PhoneNumber
, the virtual property is the type
property.
Example 12-9 illustrates the binding-tenant1.xml
mapping file.
Example 12-9 Defining Virtual Properties for Tenant 1
<?xml version="1.0"?> <xml-bindings xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm" package-name="examples.virtual"> <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="examples.virtual.Address"/> <xml-element java-attribute="phoneNumbers" name="phoneNumber" type="examples.virtual.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>
The get
and 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. Example 12-10 illustrates the Customer
class code for tenant 1 to obtain the data associated with virtual properties.
Example 12-10 Tenant 1 Code to Provide the Data Associated with Virtual Properties
...
Customer customer = new Customer();
//Set Customer's real properties
customer.setFirstName("Jane");
customer.setLastName("Doe");
Address billingAddress = new Address();
billingAddress.setStreet("1 Billing Street");
customer.setBillingAddress(billingAddress);
//Set Customer's virtual 'middleName' property
customer.put("middleName", "Anne");
//Set Customer's virtual 'shippingAddress' property
Address shippingAddress = new Address();
shippingAddress.setStreet("2 Shipping Road");
customer.put("shippingAddress", shippingAddress);
List<PhoneNumber> phoneNumbers = new ArrayList<PhoneNumber>();
customer.put("phoneNumbers", phoneNumbers);
PhoneNumber workPhoneNumber = new PhoneNumber();
workPhoneNumber.setNumber("555-WORK");
//Set the PhoneNumber's virtual 'type' property
workPhoneNumber.put("type", "WORK");
phoneNumbers.add(workPhoneNumber);
PhoneNumber homePhoneNumber = new PhoneNumber();
homePhoneNumber.setNumber("555-HOME");
//Set the PhoneNumber's virtual 'type' property
homePhoneNumber.put("type", "HOME");
phoneNumbers.add(homePhoneNumber);
Map<String, Object> properties = new HashMap<String, Object>();
properties.put(JAXBContextFactory.ECLIPSELINK_OXM_XML_KEY, "examples/virtual/binding-tenant1.xml");
JAXBContext jc = JAXBContext.newInstance(new Class[] {Customer.class, Address.class}, properties);
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(customer, System.out);
...
Example 12-11 illustrates the XML output from the Customer
class for tenant 1.
Example 12-11 XML Output from the Customer Class for Tenant 1
<?xml version="1.0" encoding="UTF-8"?> <customer> <firstName>Jane</firstName> <middleName>Anne</middleName> <lastName>Doe</lastName> <billingAddress> <street>1 Billing Street</street> </billingAddress> <shippingAddress> <street>2 Shipping Road</street> </shippingAddress> <phoneNumber type="WORK">555-WORK</phoneNumber> <phoneNumber type="HOME">555-HOME</phoneNumber> </customer>
The second tenant is a streaming media provider that offers on-demand movies and music to its subscribers. It requires a different set of extensions to the core model:
A single contact phone number
For this tenant, the mapping file is also used to customize the mapping of the real properties.
Example 12-12 illustrates the binding-tenant2.xml
mapping file.
Example 12-12 Defining Virtual Properties for Tenant 2
<?xml version="1.0"?> <xml-bindings xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm" package-name="examples.virtual"> <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="examples.virtual.PhoneNumber"/> </java-attributes> </java-type> </java-types> </xml-bindings>
Example 12-13 illustrates the tenant 2 Customer
class code to obtain the data associated with virtual properties.
Example 12-13 Tenant 2 Code to Provide the Data Associated with Virtual 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);
Map<String, Object> properties = new HashMap<String, Object>();
properties.put(JAXBContextFactory.ECLIPSELINK_OXM_XML_KEY, "examples/virtual/binding-tenant2.xml");
JAXBContext jc = JAXBContext.newInstance(new Class[] {Customer.class, Address.class}, properties);
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(customer, System.out);
...
Example 12-14 illustrates the XML output from the Customer
class for tenant 2.
See the following resources for more information about the technologies and tools used to implement the solutions in this chapter:
Code Sample
"@VirtualAccessMethods
" in Java Persistence API (JPA) Extensions Reference for Oracle TopLink.)
"Configuring Virtual Access Methods" in Developing JAXB Applications Using Oracle TopLink