Programming Model
OpenJPA is a persistence framework based around persisting common POJOs. It relies on Java Annotations and/or XML to add the persistence behavior. Both options are available. OpenJPA also provides a very rich Object Query Language, batch statement features, and other useful features. As a last resort, OpenJPA allows a developer to drop down to Native SQL.
The basic programming model is relatively straightforward, with an EntityManager (created using an EntityManagerFactory or injected by the environment such as an EJB 3 container) being used to establish a persistent session context in which Plain Old Java Objects (POJOs) can be moved in and out of a persistent context.
It is worth mentioning here that an object association with an EntityManager defines the state in which it is managed. Figure 8.1 illustrates the life cycle of a Java object with respect to persistence in OpenJPA.
Figure 8.1 OpenJPA life cycle management.
This life cycle is interesting because it brings together the notion of an EntityManager and a POJO, resulting in four states to consider in the programming model:
- New/Transient—The Object is instantiated, but there is no relational data in the database.
- Managed—The Object is associated with the persistent manager, and therefore the instance has a database record and the Java instance is connected to its record. Executing getters() and setters() imply database operations.
- Detached—The EntityManager is closed but the instance is still around. It is just a Value Object at this point. Executing getters() and setters() do not imply database updates. You can move a detached instance back to a Managed state by calling merge on the EntityManager.
- Removed—This is an instance that is no longer in the database because it has been deleted. After the transaction is committed, the object is just like any other transient Java Object.
Within this context, we will consider the specific details of how to do initialization, make connections, create transactions, invoke CRUD methods, and so on.
Initialization
The heart of persisting POJOs relies on a special object called an EntityManager. The goal for a developer is to initialize an EntityManager with the proper mappings and database information necessary. JPA provides several ways to load an EntityManager, depending on the environment.
To initialize the framework, you need to create a file called persistence.xml, as shown in Figure 8.1, and define a persistence unit, as shown in Listing 8.1. The listing contains the information necessary for a Java SE Application to configure a persistence unit.
Listing 8.1. Java SE Persistence Unit
<persistence-unit name="pie-db-JAVA-SE"> <provider> org.apache.openjpa.persistence.PersistenceProviderImpl </provider> <properties> <property name="openjpa.ConnectionURL" value="jdbc:derby://localhost:1527/PWTE"/> <property name="openjpa.ConnectionDriverName" value="org.apache.derby.jdbc.ClientDriver"/> <property name="openjpa.jdbc.DBDictionary" value="derby"/> <property name="openjpa.Log" value="DefaultLevel=WARN, Runtime=INFO, Tool=INFO, SQL=TRACE"/> <property name="openjpa.jdbc.Schema" value="APP"/> </properties> </persistence-unit>
The first thing you do is define your provider, which in this case is OpenJPA. Then you use the properties to set up the name of the Driver or DataSource implementation and any additional properties. A full list of properties can be found in the Open JPA Manual referenced earlier [OpenJPA 2l].
In a Java EE environment, the configuration will be slightly different. Application Servers, like WebSphere Application Server, will often provide a default JPA implementation, and it is therefore not necessary to provide a JPA provider. Listing 8.2 shows an example of a persistence.xml file in a Java EE environment. In it, you can provide the JNDI name of a configured DataSource. The DataSource implementation is vendor specific. Some Application Server vendors allow for swapping the default implementation.
Listing 8.2. Java EE Persistence Unit
<persistence-unit name="pie-db-JAVA-EE"> <jta-data-source>jdbc/orderds</jta-data-source> <properties> <property name="openjpa.jdbc.DBDictionary" value="derby"/> <property name="openjpa.jdbc.Schema" value="APP"/> </properties> </persistence-unit>
Connections
After you configure the persistence unit, JPA will have the necessary information to instantiate a persistence context. A persistence context is a set of managed entity instances in which, for any persistent entity identity, there is a unique entity instance. Within the persistence context, the entity's association with the underlying persistence store is managed by the EntityManager (EM). If you are familiar with Hibernate, the EntityManager is similar to the Hibernate Session.
All the connections to the underlying database are encapsulated within the EM. So getting an instance of the EntityManager will get a connection for you as needed without any explicit coding on your part. However, getting an instance of the EntityManager varies between a Java SE and Java EE environment. In a Java SE environment, an application-managed EM instance is created by calling the EntityManagerFactory, and the lifetime of that instance is controlled by the application. For each application-managed EM instance, there are one or more corresponding application-managed persistence contexts, which are not linked with any transaction and are not propagated to other components. It is important to realize that you must open and close the Entity Manager yourself in an application coded to run in a Java SE environment.
The Persistence class is used as a bootstrap to get access to an EntityManagerFactory for a particular persistence unit configured in a Java SE environment. After you get access to the EntityManagerFactory, you can use that to get an instance of an EntityManager that can be used throughout your code to implement the persistence code. Listing 8.3 shows an example of this process. In this code, we illustrate the three steps just discussed. Notice that in this example, the EntityManager is scoped to each business method. This is one common pattern in a Java SE environment.
Listing 8.3. Look Up Entity Manager Factory
public class CustomerOrderServicesJavaSEImpl{ protected EntityManagerFactory emf = Persistence.createEntityManagerFactory( "pie-db-JAVA-SE" ); public Order openOrder(int customerId)throws Exception { EntityManager em = emf.createEntityManager(); em = emf.createEntityManager(); //Code em.close(); } ... }
It is worth noting that opening and closing an Entity Manager may be slower than keeping an instance around in some scenarios.
In a Java EE environment, the container can "inject" an EntityManagerFactory into a Java EE artifact, such as a Stateless Session Bean or an HttpServlet. Listing 8.4 shows an example of injecting an EntityManagerFactory into a Stateless Session Bean. Once injected, it is used the same way we illustrated earlier. Notice that you still must programmatically close the EntityManager because you used a factory to create the EntityManager. This is because you are still using an application-managed EntityManager.
Listing 8.4. Inject Entity Manager Factory
@Stateless public class CustomerOrderServices { @PersistenceUnit (unitName = "pie-db-JAVA-EE") protected EntityManagerFactory emf; public Order openOrder(int customerId)throws Exception { EntityManager em = emf.createEntityManager(); //Code em.close(); } ... }
In an EJB 3 environment, a container-managed EM instance is created by directing the container to inject one instance (either through direct injection or through JNDI lookup). The lifetime of that EM instance is controlled by the container; the instance matches the lifetime of the component into which it was injected. Container-managed Entity Managers will also provide automatic propagation of transactions, connections, and other services. We will discuss this further after transactions are discussed.
An EntityManager can be injected directly into an EJB, using a Java annotation called PersistenceContext. Listing 8.5 shows an example of this. In this case, the developer does not have to worry about a factory. Furthermore, you delegate to the container the management of the EntityManager and the propagation of the proper persistence context from EJB component to EJB component. This is a more common pattern in Java EE. If you are using the PersistenceContext in an EJB 3 component, the EJB 3 container will automatically propagate the persistence context for a particular request across components. It is important to keep in mind that an EJB 3 Session Bean is a thread-safe component, and therefore, only one client request is being serviced by the EJB component at a time. This means the EntityManager can safely be used by the instance without fear of another thread accessing it. The container can also take advantage of this and pass the current persistence context which can contain an active transaction.
Listing 8.5. Inject EntityManager
@Stateless public class CustomerOrderServices { @PersistenceContext (unitName = "pie-db-JAVA-EE") //Step 1 protected EntityManager em;
To illustrate the object states shown in Figure 8.1 in context of the code to get access to the EntityManager, see Listing 8.6. This listing shows objects in various states and how they move from one state to another. The comments within the listing describe the action.
Listing 8.6. Object States
//Set up EM and Transient POJO instance em = emf.createEntityManager(); Order newOrder = new Order(); newOrder.setStatus(Order.Status.OPEN); newOrder.setTotal(new BigDecimal(0)); //Make POJO Managed by persisting it em.persist(newOrder); newOrder.setTotal(new BigDecimal(1)); //Make POJO Detached by closing em.close(); newOrder.setTotal(new BigDecimal(2)); //Make POJO Managed by merging detached instance em2 = emf.createEntityManager(); em2.merge(newOrder;)
As long as a POJO is associated with an EntityManager, updates to the database are implied and the instance is managed. Managed Entities are either (a) loaded by the EntityManager via a find method or query, or (b) associated with the EntityManager with a persist or merge operation. We discuss this more in later sections on the Create, Retrieve, Update, and Destroy operations.
Transactions
You can demarcate transactions in OpenJPA in the following ways:
- Using a standard programmatic API such as the JTA interfaces
- Using the special javax.persistence.EntityTransaction interface provided by JPA
- Using declarative transactions within an EJB 3 Container
We have covered JDBC and JTA transactions in previous chapters. Listing 8.7 shows an example of using the EntityTransaction interface. A developer can get an instance of an EntityTransaction by calling getTransaction() on the EntityManager. After they have an instance, you can call begin, commit, or rollback. This is often the norm when using OpenJPA in a Java SE environment or in the web container.
Listing 8.7. Entity Manager Transaction Demarcation
public Order openOrder(int customerId) throws CustomerDoesNotExistException, OrderAlreadyOpenException, GeneralPersistenceException { EntityManager em = null; try { em = emf.createEntityManager(); em.getTransaction().begin(); //use em to manage objects em.getTransaction().commit(); return newOrder; } catch(CustomerDoesNotExistException e){ em.getTransaction().rollback(); throw e; } //Handle other Exceptions not listed finally { if (em != null) em.close(); } }
If a developer uses an external API like JTA to demarcate transactions and you are using an application-managed EntityManager, you need to have your EntityManager instance "join" the transaction. Listing 8.8 illustrates this situation and shows the code needed to cause the join.
Listing 8.8. Join Transaction
@PersistenceUnit (unitName = "pie-db-JAVA-EE") protected EntityManagerFactory emf; public Order openOrder(int customerId)throws Exception { javax.transaction.UserTransaction tran = //code to lookup UserTransaction in JNDI EntityManager em = emf.createEntityManager(); try { // Start JTA transaction tran.begin(); // Have em explicitly join it em = emf.createEntityManager(); em.joinTransaction(); //Code in transaction scope ready to commit tran.commit(); //Code outside of transaction scope } catch(Exception e) { tran.rollback(); throw e; } finally { em.close(); } }
When OpenJPA is used in an EJB 3 container, OpenJPA will allow for transactions to be controlled by EJB transaction demarcation. Listing 8.9 shows an example of JPA being used within an EJB 3 Session Bean. The EJB 3 method is marked with a Required transaction. In addition, the EntityManager is injected into the EJB 3 POJO. In this scenario, you get the benefit of having the container manage the EntityManager for you using the container-managed Entity Manager discussed in the preceding section.
Listing 8.9. EJB 3 Transaction Demarcation
@Stateless public class CustomerOrderServicesImpl implements CustomerOrderServices { @PersistenceContext(unitName="pie-db-JAVA-EE") protected EntityManager em; @TransactionAttribute(value=TransactionAttributeType.REQUIRED) public Order openOrder(int customerId) throws CustomerDoesNotExistException, OrderAlreadyOpenException, GeneralPersistenceException { // use em }
For each container-managed EM instance, there are one or more corresponding container-managed persistence contexts (PCs). At the time the PC is created, it is linked with the transaction currently in effect and propagated by the container, along with the transaction context to other called components within the same JVM.
The container-managed usage scenario is further subcategorized into transaction-scoped (lifetime is controlled by the transaction) and extended (lifetime is controlled by one or more stateful session bean instances). This means that in the transaction case, inside an EJB 3 container, the persistence context life cycle is governed by the transaction. You get automatic flushing of cache at the end of the transaction. JPA also provides an extended PersistenceContext that will be managed by some greater scope, such as a Stateful Session Bean or perhaps an Http Session. Listing 8.10 shows how you can inject a longer-lived Persistence Context.
Listing 8.10. Extended Persistence Context
@PersistenceContext( unitName="pie-db-JAVAEE", type=PersistenceContextType.EXTENDED ) protected EntityManager em;
In Chapter 5, "JDBC," we discussed savepoints. Savepoints allow for fine-grained control over the transactional behavior of your application. The JPA specification does not allow for savepoints; however, some vendors, like OpenJPA, may have extensions. OpenJPA's savepoint API allows you to set intermediate rollback points in your transaction. You can then choose to roll back changes made only after a specific savepoint, then commit or continue making new changes in the transaction. OpenJPA's OpenJPAEntityManager (subtype of JPA's EntityManager) supports these savepoint operations:
- void setSavepoint(String name);
- void releaseSavepoint(String name);
- void rollbackToSavepoint(String name);
Savepoints require some configuration, so refer to the OpenJPA documentation for more details.
Create
The EntityManager has most of the methods needed to persist Java objects, or entities. Persistence actions usually occur by passing instances of entities to and from the EntityManager. So for creating data, you would just create an instance of an entity and persist it using the persist method of the EntityManager. Listing 8.11 shows an example of this.
Listing 8.11. Persist Data
Order newOrder = new Order(); newOrder.setCustomer(customer); newOrder.setStatus(Order.Status.OPEN); newOrder.setTotal(new BigDecimal(0)); em.persist(newOrder);
This will correspond to an INSERT into the database. The Order object in the example is mapped to a table in the database. A POJO mapped to a database is called an Entity in JPA. We will discuss mappings in the "ORM Features Supported" section.
Retrieve
Reading data can be done several ways. The simplest read is to read an object by primary key. The EntityManager find method provides an easy way to do this. Listing 8.12 illustrates this. The find method takes the name of the class and a primary key value as a parameter. We discuss mapping primary keys in the later section, "ORM Features Supported."
Listing 8.12. Finding Entity Instances
AbstractCustomer customer = em.find(AbstractCustomer.class, customerId);
Using the find methods, OpenJPA can load a whole Object Graph. Listing 8.13 shows an example of accessing the Order Object after loading the customer. If the Order Object is mapped as a relationship and the proper fetching strategies are set, OpenJPA will load more than the root object with the find method. Later in the chapter, fetching is discussed.
Listing 8.13. Related Entity Instances
AbstractCustomer customer = em.find(AbstractCustomer.class, customerId); Order existingOpenOrder = customer.getOpenOrder();
JPA also comes with a rich query language called EJB-QL (sometimes called JPQL). You can issue queries against the object model. We will not provide a detailed tutorial on the query languages, and instead recommend that you read the reference guide [JPQL]; but the query language provides syntax to execute complex queries against related objects. Listing 8.14 shows an example of executing a query. As a developer, you can create a query using the createQuery against the EntityManager. Notice you can use :name to mark places where you want to use parameters. OpenJPA also supports using the ? approach used by JDBC Prepared Statements. However, both approaches will translate to Prepared Statements.
Listing 8.14. Executing JPA Queries
Query query = em.createQuery( "select l from LineItem l where l.productId = :productId and l.orderId = :orderId " ); query.setParameter("productId", productId); query.setParameter("orderId", existingOpenOrder.getOrderId()); LineItem item = (LineItem) query.getSingleResult();
OpenJPA also supports the capability to externalize queries from the code using the "named query" concept, which allows you to associate a query to a name using an annotation or the XML mapping file. Listing 8.13 shows how you annotate a POJO with the NamedQuery. The rest of the annotations in the listing are explained in the later section, "ORM Features Supported." After you define the NamedQuery, you can execute somewhere else in your code, as shown in the second part of Listing 8.15. It is worth mentioning that if you want to truly externalize the queries, you should use the XML deployment descriptor.
Listing 8.15. Executing JPA Queries
@Entity @Table(name="LINE_ITEM") @IdClass(LineItemId.class) @NamedQuery( name="existing.lineitem.forproduct", query=" select l from LineItem l where l.productId = :productId and l.orderId = :orderId" ) public class LineItem { ... Query query = em.createNamedQuery("existing.lineitem.forproduct"); query.setParameter("productId", productId); query.setParameter("orderId", existingOpenOrder.getOrderId()); LineItem item = (LineItem) query.getSingleResult();
With EJB-QL, when you are querying for Objects, you can load related objects as well, depending on the mapping. You can also load objects using joins. OpenJPA also extends EJB-QL with some value adds.
For the majority of the cases, you should be able to get data you need. There are cases when you need to drop down to native SQL. This could be to call a Stored Procedure, to get an optimized SQL, or because you cannot get OpenJPA to generate the correct SQL needed for the use case. OpenJPA supports the notion of native queries. You can execute SQL and project onto a POJO. An example is shown in Listing 8.16.
Listing 8.16. Native SQL
Query query = em.createNativeQuery( "SELECT * FROM LINE_ITEM", LineItem.class ); List<LineItem> items = query.getResultList());
You can also have Native Named Queries if you want to externalize the SQL.
Update
OpenJPA supports updating existing data in a few ways. Figure 8.1 showed you the life cycle of a POJO with respect to the EntityManager. Any field updated on a POJO that is associated with the EntityManager implies a database update. Listing 8.17 shows an example of code finding an instance of LineItem and executing an update.
Listing 8.17. Updating Persistent Entities
LineItem existingLineItem = em.find(LineItem.class,lineItemId); existingLineItem.setQuantity(existingLineItem.getQuantity() + quantity); existingLineItem.setAmount(existingLineItem.getAmount().add(amount));
Any update to related objects also implies an update. In Listing 8.18, we show that after finding the customer Entity, you can traverse to the Order. Because the customer is still being managed by the EntityManager, so is the Order.
Listing 8.18. Updating Related Entities
AbstractCustomer customer = em.find(AbstractCustomer.class, customerId); Order existingOpenOrder = customer.getOpenOrder(); BigDecimal amount = product.getPrice().multiply(new BigDecimal(quantity)); existingOpenOrder.setTotal(amount.add(existingOpenOrder.getTotal()));
OpenJPA also supports updating of detached Entities using the merge method. Listing 8.19 shows an example of a detached case. In the first part of the listing, you can see a fragment of Servlet code calling a service. The Service implementation is shown in the second part of the listing. A web request first reads the data using an HTTP GET, which gets access to the data and stores it in sessions. The service implementation uses a find to access the data and finish the request. Then an HTTP POST comes in to update the data in session. The Servlet doPost passes the detached instance into the updateLineItem method. The implementation of updateLineItem will attempt to merge the instance to the EntityManager.
Listing 8.19. Updating via merging
public void doGet( HttpServletRequest request, HttpServletResponse response) { String customerId = populateFromRequest(request); LineItem li = customerService.getLineItem(li); writeToSession(li); } public void doPost( HttpServletRequest request, HttpServletResponse response ) { LineItem li = populateFromSession(request); customerService.updateLineItem(li); } ... public LineItem getLineItem(int liId) throws GeneralPersistenceException { EntityManager em = //Get Entity Manager LineItem li = emf.find(LineItem.class,liId); return li; } public void updateLineItem(LineItem li) throws GeneralPersistenceException { EntityManager em = //Get Entity Manager em.merge(li); }
OpenJPA allows you to use EJB-QL to issue updates as well, such as shown in Listing 8.20. This feature enables you to update many instances with one statement. You also avoid hydrating objects in certain scenarios where performance is important.
Listing 8.20. Updating via Query Language
Query query = em.createQuery( "UPDATE CUSTOMER c SET o.discount = :discount WHERE c.type = 'RESIDENTIAL'" ); query.setParameter("discount", discount); query.executeUpdate();
Delete
You can delete data several ways using OpenJPA. A managed instance can be removed by calling remove on the EntityManager, as shown in Listing 8.21. (Listing 8.23 shows an even better option.)
Listing 8.21. Deleting via EntityManager Remove
LineItem existingLineItem = em.find(LineItem.class,lineItemId); if(existingLineItem != null){ em.remove(existingLineItem); }
You can configure OpenJPA to propagate deletes along an object graph. We will show you mapping relationships later; however, in Listing 8.22, you can see that Order has a relationship to a Set of LineItem instances. On the relationship, you can see that we have set the cascade to REMOVE. This means that when you delete an Order, all associated Lineitem instances will be deleted as well.
Listing 8.22. Deleting via Cascading
@Entity @Table(name="ORDERS") public class Order implements Serializable { ... @OneToMany(cascade=CascadeType.REMOVE, fetch=FetchType.EAGER ) @ElementJoinColumn( name="ORDER_ID",referencedColumnName="ORDER_ID" ) protected Set<LineItem> lineitems;
Finally, much as with Updates, OpenJPA allows you to delete, using EJB-QL Queries. Listing 8.23 shows an example of deleting via a query. This approach is useful if you want to delete many rows with one network call.
Listing 8.23. Deleting via a Query
Query query = em.createQuery( "DELETE FROM LineItem l WHERE l.productId = :productId and l.orderId = :orderId" ); query.setParameter("productId", productId); query.setParameter("orderId", existingOpenOrder.getOrderId()); query.executeUpdate();
Stored Procedures
We already showed how you can use native queries in OpenJPA. Native queries can be used to call stored procedures as shown in Listing 8.24.
Listing 8.24. Using a Native Query to Call a Stored Procedure
Query query = em.createNativeQuery("CALL SHIP_ORDER(?)"); query.setParameter(1, orderId); query.executeUpdate());
Batch Operations
As we showed in the Update and Delete sections, OpenJPA supports batching updates and deletes using EJB-QL. The Apache version of OpenJPA (1.0.0) currently does not support automatic statement batching for persistent operations. However, vendors that build on top of OpenJPA sometimes provide this function. The EJB 3 implementation of WebSphere Application Server provides an enhanced version of OpenJPA. They provide a configuration option for deferring update operations to commit time and batching them together. Other vendors may provide similar optimizations. The optimizations can make mass updates several orders of magnitude faster; see also the "Batch Operations" section in Chapter 5 for details of how this approach works.
Extending the Framework
When writing OpenJPA plug-ins or otherwise extending the OpenJPA runtime, however, you will use OpenJPA's native APIs. OpenJPA allows you to extend the framework in various ways, including these:
- You can extend the default EntityManager or EntityManagerFactory. This is often done by Vendors offering enhanced JPA implementations on top of OpenJPA.
- You can extend the query engine. This is usually done to provide optimized solutions, such as integrating with cache technologies.
- Data Caches can be added to back the OpenJPA cache.
- Other areas support extending behavior, such as fetch and primary key generation strategies.
The OpenJPA implementation shows the specific interfaces and classes that need to be extended to provide your own extensions.
Error Handling
JPA Exceptions are unchecked. Figure 8.2 shows the JPA Exception Architecture. JPA uses standard exceptions where appropriate, most notably IllegalArgumentExceptions and IllegalStateExceptions. These exceptions can occur when you perform persistence actions without the proper setup—for example, sending an Entity to an EntityManager that is not managing it.
Figure 8.2 JPA exception class diagram.
The specification also provides a few JPA-specific exceptions in the javax.persistence package. Listing 8.25 shows an example of catching an EntityNotFoundException. Alternatively, because the exceptions are unchecked, you can choose to not catch it and handle it at a higher level.
Listing 8.25. Exception Example
{ try AbstractCustomer customer = em.find( AbstractCustomer.class, customerId ); ... } catch(javax.persistence.EntityNotFoundException e) { throw new GeneralPersistenceException(e); }
All exceptions thrown by OpenJPA implement org.apache.openjpa.util.Exception Info to provide you with additional error information. Figure 8.3 shows the class diagram of the ExceptionInfo interface.
Figure 8.3 ExceptionInfo interface.