NetBeans & JPA with Multiple Persistence.xml Files

If you are developing a large, desktop application which needs to keep a persistent state one of a complex data structure, where perhaps you have also to perform queries, the use of an embedded relational database with an ORM is a good solution. My experience with blueMarine has been quite positive so far with Derby as the embedded database and Hibernate as ORM. Since a few months I'm slowly replacing all the Hibernate-specific code with JPA, to take advantage of annotations and to have more reusable code (for instance, it could be used in other projects with different ORMs such as TopLink, OpenJPA, etc).

As a side note, this code at the moment doesn't use any of the NetBeans RCP specific APIs, so it can be used if you have to deal with multiple JPA configuration files even in other environments.

Now, blueMarine is highly modular; I like modularity in projects for a number of (obvious) reasons and I have more than one hundred modules in it, developed around the NetBeans RCP APIs. Some of these modules contain their own persistence objects (entities) and they should have their own, independent ORM mappings. With Hibernate, this was not a problem: each module had a set of *.hbm.xml files containing the class-table mappings and you could manually load them one by one with this approach:

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

...

SessionFactory sessionFactory;

...

Configuration configuration = new Configuration();
InputStream is = ... get resource input stream for the *.hbm.xml file
configuration.addInputStream(is);
sessionFactory = configuration.buildSessionFactory()

 

The important point is to re-create the sessionFactory every time you modify the configuration, which could be a bit annoying, but in the end it happens only a few times during the initialization of the application (or the first time you dynamically activate a module with a persistent entity inside).

With JPA things change. The ORM mappings are loaded by default from annotations, so there's no need for the *.hbm.xml files, and this is a good simplification; but there is no API for programmatically register persistent entities, that must be declared in a specific persistence.xml file such in this example:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="BLUEMARINE_PU" transaction-type="RESOURCE_LOCAL">
<class>it.tidalwave.catalog.persistence.AttributePB</class>
<class>it.tidalwave.catalog.persistence.CategoryPB</class>
<class>it.tidalwave.catalog.persistence.TagBindingPB</class>
<class>it.tidalwave.catalog.persistence.TagPB</class>
...
</persistence-unit>
</persistence>

 

If you are used to JPA and J2EE this might sound as a little surprise: in J2EE environments you don't need to explicitly list all the entities, they are dynamically discovered by classpath inspection; outside J2EE you need to explicitly list them (I don't know the reason of this difference, perhaps the point is that classpath inspection takes a bit of time and you can afford to pay it when you re-deploy a J2EE application).

As a second note, JPA indeed allows multiple persistence.xml files, but only if they are related to different persistence units (PUs) - a PU is the container of all the configuration, caches and such for a group of persistent objects, and I don't want to have multiple ones, but a single one for the whole application ("BLUEMARINE_PU").

So, what to do? Thanks to the suggestions of Andrei Badea, I've found a solution, with just two minor issues to fix. The steps to perform are:

  1. Scan through the classpath and find all the persistence.xml files;
  2. Manipulate them by means of XML APIs and merge into a single file;
  3. Feed the resulting file to JPA
The complete code is in the ClassLoaderProxy class, that is available through subversion here (use -r 5502, which is the current while I'm typing, as it will be surely refactored in future).

First, our class extends a ClassLoader

public class ClassLoaderProxy extends ClassLoader 
{
...
public ClassLoaderProxy (final ClassLoader parent)
{
super(parent);
}
...
}

The first method we deal with is able to scan the classpath for the JPA configuration files:

    private static final String PERSISTENCE_XML = "META-INF/persistence.xml";

...

private Collection<URL> findPersistenceXMLs (final Filter filter)
throws IOException
{
final Collection<URL> result = new ArrayList<URL>();

for (final Enumeration<URL> e = super.getResources(PERSISTENCE_XML); e.hasMoreElements(); )
{
final URL url = e.nextElement();

if (filter.filter(url))
{
result.add(url);
}
}

return result;
}

The filter parameter is needed because my approach is to have a "master" persistence.xml file, with all the generic configuration, and several "secondary" files that just list the persistent entities inside each module:

    private static final String MASTER_URL_SUFFIX = "it-tidalwave-bluemarine-persistence.jar!/" + PERSISTENCE_XML;

enum Filter
{
MASTER
{
public boolean filter (URL url)
{
return url.toExternalForm().endsWith(MASTER_URL_SUFFIX);
}
},

OTHERS
{
public boolean filter (URL url)
{
return !url.toExternalForm().endsWith(MASTER_URL_SUFFIX);
}
};

public abstract boolean filter (URL url);
}

The MASTER_URL_SUFFIX, as you can see, contains the jar name of the module where the master persistence.xml file is.

Now, the code that gets all the pieces and merge them. Basically, it extracts all the "classes" declarations from the secondary files and inserts them in the master file. I'm not giving details on the implementation, as it is just a JAXP / XPATH exercise:

    private static final DocumentBuilderFactory DOC_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();

private static final XPathFactory XPATH_FACTORY = XPathFactory.newInstance();

private static final XPath XPATH = XPATH_FACTORY.newXPath();

private static final XPathExpression XPATH_ENTITY_PU_NODE;
private static final XPathExpression XPATH_ENTITY_CLASS_TEXT;

...

static
{
try
{
XPATH_ENTITY_PU_NODE = XPATH.compile("//persistence/persistence-unit");
XPATH_ENTITY_CLASS_TEXT = XPATH.compile("//persistence/persistence-unit/class/text()");
}
catch (XPathExpressionException e)
{
throw new ExceptionInInitializerError(e);
}
}

...

private String scanPersistenceXML()
throws IOException,
ParserConfigurationException,
SAXException,
XPathExpressionException,
TransformerConfigurationException,
TransformerException
{
logger.info("scanPersistenceXML()");
final DocumentBuilder builder = DOC_BUILDER_FACTORY.newDocumentBuilder();
DOC_BUILDER_FACTORY.setNamespaceAware(true);

final URL masterURL = findPersistenceXMLs(Filter.MASTER).iterator().next();
logger.fine(String.format(">>>> master persistence.xml: %s", masterURL));
final Document masterDocument = builder.parse(masterURL.toExternalForm());
final Node puNode = (Node)XPATH_ENTITY_PU_NODE.evaluate(masterDocument, XPathConstants.NODE);

for (final URL url : findPersistenceXMLs(Filter.OTHERS))
{
logger.info(String.format(">>>> other persistence.xml: %s", url));
final Document document = builder.parse(url.toExternalForm());
final NodeList nodes = (NodeList)XPATH_ENTITY_CLASS_TEXT.evaluate(document, XPathConstants.NODESET);

for (int i = 0; i < nodes.getLength(); i++)
{
final String entityClassName = nodes.item(i).getNodeValue();
logger.info(String.format(">>>>>>>> entity class: %s", entityClassName));

if (i == 0)
{
puNode.appendChild(masterDocument.createTextNode("\n"));
puNode.appendChild(masterDocument.createComment(" from " + url.toExternalForm().replaceAll(".*/cluster/modules/", "") + " "));
puNode.appendChild(masterDocument.createTextNode("\n"));
}

final Node child = masterDocument.createElement("class");
child.appendChild(masterDocument.createTextNode(entityClassName));
puNode.appendChild(child);
puNode.appendChild(masterDocument.createTextNode("\n"));
}
}

return toString(masterDocument);
}

Now, where should we trigger this processing? In the getResource() method of the ClassLoader, which is precisely the one called by JPA to retrieve the persistence.xml file. This method must return the URL of the requested resource and the idea is that we will not return the master file URL, rather the URL of our synthetised file.

private static final String PERSISTENCE_XML = "META-INF/persistence.xml";

private URL persistenceXMLurl;

...

@Override
public Enumeration<URL> getResources (final String name)
throws IOException
{
if (PERSISTENCE_XML.equals(name))
{
if (persistenceXMLurl == null)
{
try
{
final String persistenceXml = scanPersistenceXML();
logger.fine("persistence.xml " + persistenceXml);

// The base directory must be empty since Hibernate will scan it searching for classes.
final File file = new File(System.getProperty("java.io.tmpdir") + "/blueMarinePU/" + PERSISTENCE_XML);
file.getParentFile().mkdirs();
final PrintWriter pw = new PrintWriter(new FileWriter(file));
pw.print(persistenceXml);
pw.close();
persistenceXMLurl = new URL("file://" + file.getAbsolutePath());
logger.info("URL: " + persistenceXMLurl);
}
catch (ParserConfigurationException e)
{
throw new IOException(e.toString());
}
catch (SAXException e)
{
throw new IOException(e.toString());
}
catch (XPathExpressionException e)
{
throw new IOException(e.toString());
}
catch (TransformerConfigurationException e)
{
throw new IOException(e.toString());
}
catch (TransformerException e)
{
throw new IOException(e.toString());
}
}

return new Enumeration<URL>()
{
URL url = persistenceXMLurl;

public boolean hasMoreElements()
{
return url != null;
}

public URL nextElement()
{
final URL url2 = url;
url = null;
return url2;
}
};
}

return super.getResources(name);
}

 

Here you can see the first rough corner. Since I must return an URL (that will be later opened by JPA by opening a connection to it), I must create a physical file to refer it (indeed NetBeans RCP allows to create a "virtual" filesystem which is mapped into memory and it has its own URLs - I attempted to use it, but I've been told that those URLs can't be connected to, so this approach won't work. I think it can be fixed, but it's a matter of a future post).

Well, we're at the end of the job. Now we just have to force JPA to use our ClassLoader, instead of the default one. In blueMarine, I have a specific method of a specific service that is used to access JPA. In this method, I'm just overriding the "system ClassLoader" across the call to JPA, as in the following code:

    public synchronized EntityManagerFactory getEntityManagerFactory()
{
if (entityManagerFactory == null)
{
final Properties properties = configuration.getProperties();
final Thread currentThread = Thread.currentThread();
final ClassLoader saveClassLoader = currentThread.getContextClassLoader();
currentThread.setContextClassLoader(new ClassLoaderProxy(saveClassLoader));
entityManagerFactory = javax.persistence.Persistence.createEntityManagerFactory("BLUEMARINE_PU", configuration.getProperties());
currentThread.setContextClassLoader(saveClassLoader);
}

return entityManagerFactory;
}

This is the second rough corner: while it fits my needs, a better solution would be to try to replace the system class loader globally; in this way, you could call JPA without any specific care. At the moment I don't know whether it's possible to replace the system ClassLoader in NetBeans RCP - matter for another post!

0
Average: 4 (4 votes)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

David Chamberlin replied on Thu, 2008/05/29 - 6:40am

One potential gotcha about supporting this model is that depending on which modules are loaded, you end up with a different database schema.  For example, if you add a module that contains a model object with a to-one relationship from one of the core modules this would end up requiring an additional foreign key in the core object's table.  This may mean a data migration exercise is needed when you upgrade or add new features that were not part of an initial deployment.

Nevertheless, I think this looks like something that ought to be supported by the specification.  Perhaps you could look to getting it added into JPA 2.0 - you should run it by Mike Keith as a potential enhancement.

Fabrizio Giudici replied on Thu, 2008/05/29 - 6:52am

Excellent remark, David, and I've actually forgotten to say that in my application, in fact, I don't use JPA relationships at all. I can live with it since indeed I have a very simple schema of relationships, in which a single entity is related with entities provided by other modules; thus is not difficult to handle them manually. Of course, in other scenarios this could be a problem.

BTW, managing the database schema upgrades needed by new versions of the software is another problem - in a desktop application, you can't afford to do that with an administration task as you'd probably do in a regular enterprise application, since the upgrade must be performed automatically and in unattended mode when the user installs and runs a new version of the application. I've done some preliminary work in this area that I should be able to post on my blog in a few days.

piterson replied on Wed, 2009/07/29 - 8:46am

You can't afford to do that with an administration task as you'd probably do in a regular enterprise application, since the upgrade must be performed automatically and in unattended mode when the user installs and runs a new version of the application.

 Elizabeth Bay

 

frankmarsh replied on Sat, 2009/08/08 - 2:52am

Unfortunately the JPA specification doesn't say anything about how to handle multiple persistence.xml files.

teeth whitening products

 

seymon replied on Tue, 2009/08/25 - 1:43pm

I have a very simple schema of relationships, in which a single entity is related with entities provided by other modules; thus is not difficult to handle them manually. Of course, in other scenarios this could be a problem. Personal Injury Claims

gandaria7001 replied on Sun, 2009/09/06 - 12:35pm

 

I like modularity in projects for a number of (obvious) reasons and I have more than one hundred modules in it, developed around the NetBeans RCP APIs

 

 

 

<a herf=”http://www.propertyinvestor.com.au/”> Investment Property </a>

gandaria7001 replied on Sun, 2009/09/06 - 12:36pm


 

Jessi replied on Thu, 2009/09/10 - 1:03am

I have reviewd modularity in projects and I have more than one hundred modules in it, developed around the NetBeans RCP APIs.     http://www.titlewebsolutions.com/

danhawley replied on Tue, 2009/09/15 - 10:49am

Hi, I am unable to follow the link to the classloader proxy class. I am doing something wrong or is the link out of date? Thanks, Dan

pp456 replied on Tue, 2009/10/13 - 7:51am

I want to give thanks for the informative post. I really appriciate sharing this great post.I have a very simple schema of relationships, in which a single entity is related with entities provided by other modules;

 adwords management

india news replied on Fri, 2009/10/23 - 6:36am

This module really works good.

http://www.theinfosage.com

reinsurance company replied on Sun, 2009/10/25 - 9:11pm

Net beans and JPA are a fabulous combination. This post was really helpful... Great way to go...

  Normal 0 false false false EN-US X-NONE X-NONE MicrosoftInternetExplorer4 /* Style Definitions */ table.MsoNormalTable {mso-style-name:"Table Normal"; mso-tstyle-rowband-size:0; mso-tstyle-colband-size:0; mso-style-noshow:yes; mso-style-priority:99; mso-style-qformat:yes; mso-style-parent:""; mso-padding-alt:0in 5.4pt 0in 5.4pt; mso-para-margin:0in; mso-para-margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:11.0pt; font-family:"Calibri","sans-serif"; mso-ascii-font-family:Calibri; mso-ascii-theme-font:minor-latin; mso-fareast-font-family:"Times New Roman"; mso-fareast-theme-font:minor-fareast; mso-hansi-font-family:Calibri; mso-hansi-theme-font:minor-latin; mso-bidi-font-family:"Times New Roman"; mso-bidi-theme-font:minor-bidi;} <a href="http://www.serveamericaltd.com"> reinsurance company </a>

baloch replied on Mon, 2009/10/26 - 8:29pm

If you are used to JPA and J2EE this might sound as a little surprise: in J2EE environments you don't need to explicitly list all the entities, they are dynamically discovered by classpath inspection; outside J2EE you need to explicitly list them (I don't know the reason of this difference, perhaps the point is that classpath inspection takes a bit of time and you can afford to pay it when you re-deploy a J2EE application).

You are right on his point. It takes the classpath and redeploys it everytime it is run.

 deals random

pexiq

 

 

kpmkumar replied on Thu, 2009/10/29 - 8:31am

good to watch and good to over look the codings that are used Limited Benefit Plans

seanyboyyo replied on Tue, 2009/11/03 - 12:02pm

Nice post here! good detail and well written. thanks Cheap Directory Submissions

aclazaro replied on Wed, 2009/11/04 - 12:17pm

I may not be so much of a techie person but as one of the prime movers of our own seo based company, I have been discovering all the useful data I found here in this site very useful to our business. Hence, I am pointing my IT team towards this direction. Thanks and regards! web hosting

nahid5692001 replied on Tue, 2009/11/24 - 8:48pm

hmm JPA with Netbeans, ive never tried that before. I will implement this in my next project. cheapest voip calls

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.