JPA

Tools Location
Source Code https://bitbucket.org/amdatu/amdatu-jpa
Wiki https://amdatu.atlassian.net/wiki/display/AMDATUDEV/Amdatu+JPA
Issue Tracking https://amdatu.atlassian.net/browse/AMDATUJPA
Continuous Build https://amdatu.atlassian.net/builds/browse/JPA

Introduction

Amdatu JPA makes it easy to work with relational databases using JPA in OSGi. Amdatu JPA supports the following features:

  • Register data sources from Configuration Admin
  • Create Persistence Units for bundles containing a persistence.xml
  • Publish EntityManagers as services
  • Managed transactions based on annotations
  • Support for JTA
  • Schema migration

Currently Amdatu JPA supports Apache OpenJPA and EclipseLink.

Components

Amdatu JPA provides the following components:

Bundle Required Description
org.amdatu.persistence2_1 Only for EclipseLink The JPA 2.1 and Amdatu JPA APIs
org.amdatu.persistence2_0 Only for OpenJPA The JPA 2.0 and Amdatu JPA APIs
org.amdatu.jpa.datasourcefactory yes Register data sources as services.
org.amdatu.jpa.extender yes Integration of JPA in OSGi. Takes care of creating Entity Managers.
org.amdatu.jta.api yes Repackages javax.transaction and javax.transaction.xa
org.amdatu.jta.manager yes Managed transaction support based on an aspect
org.amdatu.jta.transactionmanager yes Repackages Apache Aries transaction manager for correct wiring
org.amdatu.jpa.adapter.eclipselink yes Contains the integration layer for EclipseLink
org.amdatu.dtabase.schemamigration no Optional schema migration tool based on Flyway.

Of course each individual component may be replaced by your own implementation based on the API if this suits your needs better.

Dependencies

The following tables provide an overview of the required and optional general dependencies as well as the specific dependencies for the different providers offered by Amdatu JPA:

Dependencies for all providers

Bundle Required Description
org.apache.felix.dependencymanager yes Apache Felix Dependency Manager is used internally by all Amdatu components
org.apache.commons.lang3 yes Used by org.amdatu.jpa.datasourcefactory
Implementation of Config Admin (e.g. org.apache.felix.configiadmin) yes Used to configure data sources
org.ops4j.pax.logging.pax-logging-api no Integration between logging providers. Alternatively custom SLF4j integration can be used
org.ops4j.pax.logging.pax-logging-service no See pax-logging-api

Dependencies for OpenJPA

org.apache.commons.collections yes Used by OpenJPA internally
org.apache.commons.lang yes Used by OpenJPA internally
org.apache.commons.pool yes Used by OpenJPA internally
org.apache.servicemix.bundles.commons-dbcp yes Used by OpenJPA internally
org.apache.servicemix.bundles.serp yes Used by OpenJPA internally
org.apache.xbean.asm5-shaded true Used by OpenJPA internally
org.apache.openjpa true OpenJPA itself

Dependencies for EclipseLink

org.eclipse.persistence.antlr yes Part of EclipseLink
org.eclipse.persistence.asm yes Part of EclipseLink
org.eclipse.persistence.core yes Part of EclipseLink
org.eclipse.persistence.jpa yes Part of EclipseLink
org.eclipse.persistence.jpa.jpql yes Part of EclipseLink

Configuring a data source

Note that as of the R6 release the datasourcefactory uses the OSGi Compendium JDBC™ Service Specification (chapter 125) to obtain the JDBC driver. Because of this a DataSourceFactory needs to be available.

The first step towards working with JPA is registering a data source. Amdatu JPA provides a Managed Service Factory to accomplish this.

The DataSourceFactory is configured by providing a configuration for the PID org.amdatu.jpa.datasourcefactory.

Property Type Required Description Default value
driverClassName String yes JDBC driver to load (Make sure there is a DataSourceFactory available for this driver)
jdbcUrl String yes JDBC connection url
username String yes Database username used to connect
password String yes Database password used to connect
managed Boolean yes Managed transactions enabled true/false

Additional configuration properties will be propagated as a service property of the DataSource service created by the datasourcefactory.

Drivers and the OSGi JDBC™ Service Specification

Most JDBC drivers currently don’t provide a DataSourceFactory service implementation, the Pax JDBC project provides driver adapters to fill this gap for some popular databases.

Creating a Persistence Unit

JPA requires a Persistence Unit to be configured. A Persistence Unit contains mapped entities and is used to create EntityManagers. It is common to use multiple Persistence Units within a single application, as described in the section about architecture and design. A Persistence Unit is created by adding a persistence.xml file to the META-INF directory of a bundle, and reference it with a special header:

Include-Resource: META-INF/persistence.xml=resources/persistence.xml
Meta-Persistence: META-INF/persistence.xml

The persistence.xml file is a standard JPA configuration file. In the persistence.xml file the data sources to use a referenced and the mapped entities are listed. The following is a complete persistence.xml that uses both a JTA and a standard data source, and maps three entities.

<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="OrdersPU" transaction-type="JTA">

    <provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider>
    <jta-data-source>
      <![CDATA[
      osgi:javax.sql.DataSource/(name=ManagedDemoDs)
      ]]>
    </jta-data-source>

    <non-jta-data-source>
      <![CDATA[
      osgi:javax.sql.DataSource/(name=DemoDs)
      ]]>
    </non-jta-data-source>

    <class>webshop.orders.jpa.JPAOrder</class>
    <class>webshop.orders.jpa.JPAOrderEvent</class>
    <class>webshop.orders.jpa.JPAOrderProduct</class>

    <exclude-unlisted-classes>true</exclude-unlisted-classes>

    <properties>
      <property name="openjpa.ManagedRuntime" value="org.apache.openjpa.ee.OSGiManagedRuntime"/>
      <property name="openjpa.Log" value="slf4j"/>
      <property name="openjpa.ConnectionFactoryProperties" value="PrintParameters=true" />

      <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(ForeignKeys=true)"/>
      <property name="openjpa.jdbc.SchemaFactory" value="native(ForeignKeys=true)"/>
      <property name="openjpa.jdbc.MappingDefaults" value="ForeignKeyDeleteAction=restrict,JoinForeignKeyDeleteAction=restrict"/>
    </properties>
  </persistence-unit>
</persistence>

The Persistence Unit has a name (line 4). This name is set as a service property osgi.unit.name on the published EntityManager service. The names of the data sources are the same as the serviceName property in the data source configuration. The provider property defines the JPA implementation to use, but currently only Apache OpenJPA is support by Amdatu JPA.

The properties at the end of the file are JPA implementation (in this case Apache OpenJPA) specific configuration properties. The listed configuration generates tables automatically based on the entity classes.

Use JPA with Managed Transactions

To avoid cluttering code with transaction management code Amdatu JPA supports managed transactions. Amdatu JPA will take care of opening, commiting or rolling back transactions, and synchronize EntityManagers with these transactions. The exact behaviour of transaction demarcation can be controlled by annotations.

To enable managed transactions there are to options to choose from. The first option is to use a service property transactional with a value of MyServiceInterface.class.getName(). Make sure that the interface in the setInterface method is set to Object.class! This is required to make sure that the service is not injected in consumers before it is properly proxied by the transaction manager component. After creation of the proxy the component will be registered with it’s correct interface (the interface specified in the service property).

public class Activator extends DependencyActivatorBase {
  @Override
  public void init(BundleContext bc, DependencyManager dm) throws Exception {

    Properties props = new Properties();
    props.put("transactional", OrderService.class.getName());

    //Note the use of Object.class as interface, while we are actually registering a OrderService.class.
    dm.add(createComponent()
        .setInterface(Object.class.getName(), props)
        .setImplementation(JPAOrderService.class)
        .add(createServiceDependency().setService(EntityManager.class, "(osgi.unit.name=OrdersPU)").setRequired(true)));
  }

  @Override
  public void destroy(BundleContext bc, DependencyManager dm) throws Exception {
  }
}

Alternatively we can do the same using the ManagedTransactional interface. The interface has a method getManagedService() that should return the actual service interface(s).

@Transactional
public class OrderServiceImpl implements ManagedTransactional, OrderService {
  private volatile EntityManager em;

  @Override
  public Class<?>[] getManagedInterfaces() {
    return Arrays.asList(OrderService.class);
  }

  //Service methods...
}

In this case the activator looks a little different as well. The interface being registered is now MangedTransactional.class. Note again that the component is not actually registered using it’s service interface, the transaction manager component will take care of this.

@Override
  public void init(BundleContext bc, DependencyManager dm) throws Exception {
    dm.add(createComponent().setInterface(ManagedTransactional.class.getName(), null)
        .setImplementation(OrderServiceImpl.class)
        .add(createServiceDependency().setService(EntityManager.class).setRequired(true)));
  }

The EntityManager dependency should contain a filter to make sure that the EntityManager representing the correct Persistence Unit is injected, since most applications will have multiple Persistence Units.

Next we can implement a service that works with an EntityManager.

@Transactional
public class JPAOrderService implements OrderService {

  private volatile EntityManager em;

  @Override
  public void placeOrder(Order order) {
    em.persist(new JPAOrder(order));

  }

  @Override
  @Transactional(TxType.SUPPORTS)
  public List<Order> listOrders() {
    List<JPAOrder> resultList = em.createQuery("select o from JPAOrder o", JPAOrder.class).getResultList();

    List<Order> orders = new ArrayList<>();
    for (JPAOrder jpaOrder : resultList) {
      orders.add(jpaOrder.getAsOrder());
    }
    return orders;
  }

  //...

Because Managed Transactions are used the class and methods can be annotated using @Transactional. This controls how the transaction manager should deal with (existing) transactions. The @Transactional on the class level sets the default for the class, which can be overridden by placing a @Transactional on a method.

In the example we have a @Transactional on top of the class without any parameters. This defaults to TxType.REQUIRED. This will either join an existing transaction when available, or create a new transaction. When a new transaction is created it will be committed when the method returns, or rolled back when an exception occurs. Because of this default the placeOrder method always runs in a transaction. The listOrders method is read-only and doesn’t necessarily require a transaction, so the default is overridden to TxType.SUPPORTS; when a transaction exists the method will run in that transaction, but if no transaction is active it runs without. The EntityManager is automatically synchronized with the transaction, and will be flushed when the transaction is committed.

An EntityManager is by default not thread safe, and an instance shouldn’t be used by multiple threads. When managed transactions are used, the transaction manager takes care of binding the EntityManager to a transaction, which in turn is bound to a thread. This means that it is safe to use an EntityManager as a service without any additional synchronization when using @TxType.REQUIRED, @TxType.REQUIRES_NEW or @TxType.MANDATORY. In other cases, you will need additonal synchronization in your code and enable non transactional access to the EntityManager by setting the value for the org.amdatu.jpa.allowNonTransactionalEntityManagerAccess property to true in your persistence.xml file.

Using JPA with manual transaction management

Although Managed Transactions are recommended it is also possible to manually control transactions. The service should be registered without the transactional=true property and @Transactional annotations can’t be used. From code we can control transactions directly on the EntityManager.

@Override
  public void placeOrder(Order order) {
    em.getTransaction().begin();

    em.persist(new JPAOrder(order));

    em.getTransaction().commit();
  }

Architecture and design

In a sound modular arechitecture a service should be responsible for it’s own data and the data should only be touched by that service. How does that translate to a relational model where many tables are related by foreign keys!? A large, fully normalized, model certainly doesn’t work in a modular environment. Each service should have it’s own model, with (in most cases) just a few entities. Entities within a model can have relations, but should never have relations with entities from another service’s model. To accomplish this it might be necessary to denormalize or use service calls instead of joins when collecting data. Practically these model boundaries require most applications to define multiple Persistence Units. Potentially these Persistence Units could even use separate data sources, but this is not necessary to achieve our modularity goals. The following diagram is an example from a webshop model with three different Persistence Units.

The Order entity needs to reference a Customer. A foreign key relationship is not possible because the two entities (should) live in their own isolated Persistence Unit. As a workaround an unique property of the Customer is stored for each order. The Order REST service will fetch orders from the OrderService and get more Customer information from the CustomerService. This is obviously less efficient than a single join query, but is necessary to pretect module boundaries; something that will pay of on the long term.

To use API classes as Entity or not

A design choice that must be made early on is whether API classes exported by a service bundle are used directly as entities. The first restriction is that the entity classes MUST be part of the bundle containing the persistence configuration. This is due to the class weaving which is required by OpenJPA. Next, from a pure modular view it would be bad practice to expose classes in an API which are annotated with implementation details (JPA annotations). This is a tradeoff between ease-of-use and preventing leaking of implementation details.

When keeping the annotated classes private there must be some mapping mechanism to map the entity classes to/from the API classes. There are several ways to accomplish this. The following example is one solution. Unfortunatly subclassing in JPA has some special rules, which practically forces you to repeat the properties that should be mapped to the database in the subclass. Alternatively a @Mappedsuperclass can be used, but that brings us back to leaking implementation details…

@Entity
public class JPAProduct extends Product {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;
  private String name;
  private String category;
  private double price;

  public JPAProduct() {
  }

  public JPAProduct(Product product) {
    name = product.getName();
    category = product.getCategory();
    price = product.getPrice();
  }

  //Getters and setters for all properties

Examples

The Amdatu JPA project on BitBucket contains a simple example of how to setup and use JPA. For a more complex example the Amdatu Showcase project contains a branch that uses JPA for persistence instead of Mongo.

Schema migration

When working with relational databases schema migration becomes an important topic. Although most JPA implementations can generate tables based on entities it’s unsafe to use this in production environments. It’s better to write migration scripts manually and carefully test those before upgrading to a new version of the software on production.

Flyway is an excellent tool to perform migration scripts. It keeps it’s own administration of applied migration scripts; all you have to do is provide the actual scripts. Amdatu JPA contains a service to use Flyway from within an OSGi application. A persistence bundle contains it’s own scripts and uses this service to perform the migration.

It would seem natural to use an extender mechanism for this, but this doesn’t work well. When using an extender you can’t be sure that the extender does it’s work before the bundle’s services are used, which is problematic when the new JPA code is incompatible with the old schema. Because of this problem the persistence bundle must be responsible for starting the migration itself; usally from it’s “start” method. An example of such a migration is shown below.

public class MigrationDemo {

  private volatile DataSource m_ds;
  private volatile SchemaMigrationService m_migrationService;
  private volatile BundleContext m_bundleContext;

  public void start() {

    m_migrationService.migrate(m_ds, m_bundleContext.getBundle(), "migrations");

    System.out.println("Migration completed!");

    //Now we can safely use our entities
  }
}

The migrations directory should contain .sql scripts with a version number in the file name such as “V1__create.sql”. More information about versioning and scripts in general can be found on the Flyway website.

Configure migrations in persistence.xml

With Amdatu JPA it’s possible to configure migrations in persistence.xml. When using this feature Amdatu JPA makes sure that the EntityManager and EntityManagerFactory services only become available after a successful schema migration.

For this feature the migration scripts need to be included in the persistence bundle and the following properties need to be added to the persistence.xml

  • org.amdatu.database.schemamigration.enabled – boolean, Set to true to enable Amdatu JPA schema management
  • org.amdatu.database.schemamigration.locations – String, Location in the persistence bundle where the migration scripts are located
  • org.amdatu.database.schemamigration.migrationServiceFilter – String, OSGi filter used to retrieve the SchemaMigrationService

Flyway configuration properties

  • org.amdatu.database.schemamigration.flyway.tableName – String (Flyway API docs)
  • org.amdatu.database.schemamigration.flyway.initOnMigrate – boolean (Flyway API docs)
  • org.amdatu.database.schemamigration.flyway.outOfOrder – boolean (Flyway API docs)
  • org.amdatu.database.schemamigration.flyway.validateOnMigrate – boolean (Flyway API docs)

Example persistence.xml using schema management

<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="OrdersPU" transaction-type="JTA">

    ...SNIP...
    <properties>
      ...SNIP...
      <property name="org.amdatu.database.schemamigration.enabled" value="true" />
      <property name="org.amdatu.database.schemamigration.locations" value="migrations" />

      <!-- Optional properties : -->
      <property name="org.amdatu.database.schemamigration.migrationServiceFilter" value="(migrator=custom-schema-migrator)" />
      <!-- -->

      <!-- Flyway configuration properties -->
      <property name="org.amdatu.database.schemamigration.flyway.tableName" value="my_custom_schema_version" />
      <property name="org.amdatu.database.schemamigration.flyway.initOnMigrate" value="false" />
      <property name="org.amdatu.database.schemamigration.flyway.outOfOrder" value="true" />
      <property name="org.amdatu.database.schemamigration.flyway.validateOnMigrate" value="true" />
      <!-- -->
    </properties>
  </persistence-unit>
</persistence>

Schema migration requirements

  • org.amdatu.database.schemamigration