- Background
- Architectural Overview
- Programming Model
- ORM Features Supported
- Tuning Options
- Development Process of the Common Example
- Summary
- Links to developerWorks
- References
Development Process of the Common Example
Now that we have gone through OpenJPA in detail, we will show you the steps required to develop a complete application. To run it yourself, follow the directions in Appendix A, "Setting Up the Common Example." Keep in mind that Chapter 2, "High-Level Requirements and Persistence," and Chapter 3, "Designing Persistent Object Services," describe the requirements and design of our example. This section focuses on the details related to developing OpenJPA applications after you understand the requirements and settle on a design.
Defining the Object
When developing an OpenJPA application, you define your objects by coding Java Classes with annotations, and then coding XML mapping files. The following listings show our domain model implemented in Java with OpenJPA annotations being used to specify the mapping metadata. We only show subsets of the classes to illustrate the mapping. You can examine all the code by downloading the sample as shown in Appendix A. Listing 8.68 lists the AbstractCustomer superclass. It is mapped using a Single Table Strategy. Besides the defaulted primitive value fields, we explicitly map the open order field as a one-to-one relationship to the ORDERS table because a Customer object may have at most one open order, and we map the orders field as a one-to-many relationship with respect to the ORDERS table because a Customer might refer to more than one in any state, including open. The orders field is declared a bidirectional relationship, and therefore additional details are defined on the Order side. We also define an Eager fetching strategy, because we want to fetch the open order record whenever the customer is accessed. We use a Lazy option to load the orders collection only when we specifically access the history.
Listing 8.68. Abstract Customer
@Entity @Inheritance(strategy=SINGLE_TABLE) @Table(name = "CUSTOMER") @DiscriminatorColumn(name="TYPE", discriminatorType = STRING) public abstract class AbstractCustomer implements Serializable { @Id @Column(name="CUSTOMER_ID") protected int customerId; protected String name; protected String type; @OneToOne( fetch=FetchType.EAGER, cascade = {CascadeType.MERGE,CascadeType.REFRESH}, optional=true ) @JoinColumn(name="OPEN_ORDER", referencedColumnName = "ORDER_ID") protected Order openOrder; @OneToMany(mappedBy="customer",fetch=FetchType.LAZY) protected Set<Order> orders; ... //Gettters and Setters... }
Listing 8.69 shows the ResidentialCustomer subclass. Notice we used the DiscriminatorValue to determine the type. We described this in the Inheritance section of the template.
Listing 8.69. Residential Customer
@Entity @DiscriminatorValue("RESIDENTAL") public class ResidentialCustomer extends AbstractCustomer implements Serializable { @Column(name="RESIDENTIAL_HOUSEHOLD_SIZE") protected short householdSize; @Column(name="RESIDENTIAL_FREQUENT_CUSTOMER") protected boolean frequentCustomer; //Getters and Setters... }
Listing 8.70 shows the BusinessCustomer subclass, which is similar to the ResidentialCustomer subclass.
Listing 8.70. Business Customer
@Entity @DiscriminatorValue("BUSINESS") public class BusinessCustomer extends AbstractCustomer implements Serializable { @Column(name="BUSINESS_VOLUME_DISCOUNT") protected boolean volumeDiscount; @Column(name="BUSINESS_PARTNER") protected boolean businessPartner; @Column(name="BUSINESS_DESCRIPTION") protected String description; //Getters and Setters... }
Listing 8.71 shows the Order object. We elected to generate the Order ID using the Identity Strategy because this primary key is bound to a single table. We also map a relationship back to the AbstractCustomer using ManyToOne. The detail of this bidirectional relationship is defined on the Order object.
The Order object also has a Set of LineItems for the Order that implements a unidirectional relationship between the Order and LineItem classes. We have no requirement to navigate from a LineItem to an Order; defining a single-sided relationship will make the underlying SQL optimal.
Listing 8.71. Order Object
@Entity @Table(name="ORDERS") public class Order implements Serializable { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) @Column(name="ORDER_ID") protected int orderId; protected BigDecimal total; public static enum Status { OPEN, SUBMITTED, CLOSED } @Enumerated(EnumType.STRING) protected Status status; @ManyToOne @JoinColumn( name="CUSTOMER_ID", referencedColumnName = "CUSTOMER_ID" ) protected AbstractCustomer customer; @OneToMany(cascade=CascadeType.REMOVE,fetch=FetchType.EAGER ) @ElementJoinColumn(name="ORDER_ID",referencedColumnName="ORDER_ID" ) protected Set<LineItem> lineitems; //getters and setters }
Listing 8.72 shows the LineItem class. You will notice that the LineItem class is a composite key. This choice was made to illustrate how to map to certain legacy schemas. ORM technologies tend to work better with generated keys. The LineItem also has a one-to-one relationship to the Product. This is also a unidirectional relationship because there is no requirement to navigate from a Product instance to the LineItem objects that reference it. Product instances also usually are cached because the product catalog changes infrequently.
Listing 8.72. LineItem
@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 implements Serializable { @Id @Column(name="ORDER_ID") private int orderId; @Id @Column(name="PRODUCT_ID") private int productId; protected long quantity; protected BigDecimal amount; @ManyToOne(fetch = FetchType.EAGER) @JoinColumns({ @JoinColumn(name="PRODUCT_ID", referencedColumnName = "PRODUCT_ID" )} ) protected Product product; //getters and setters }
Listing 8.73 shows the composite primary key for the LineItem Entity.
Listing 8.73. Line Item ID
@Embeddable public class LineItemId implements Serializable{ private int orderId; private int productId; //getters and setters @Override public int hashCode() { //unique hashcode } @Override public boolean equals(Object obj) { //equals } }
Listing 8.74 shows the Product. The Product is a simple class that is mapped to the PRODUCT table. It has no relationships. (Real products usually have many more details and belong to categories and such.) We have added a NamedQuery to the Product annotations to specify a query that retrieves the list of products. As noted previously, we also cache the Product. This example should not be considered complete and is for demonstration purposes only; products in real enterprise systems are usually indexed, categorized, and related to extensive catalog and inventory management systems.
Listing 8.74. Product
@Entity @NamedQuery(name="product.all",query="select p from Product p") @DataCache(timeout=6000000) public class Product implements Serializable { @Id @Column(name="PRODUCT_ID") protected int productId; protected BigDecimal price; protected String description; //getters and setters }
We did not employ the option to provide XML mapping files. OpenJPA is meant to reduce the number of artifacts one has to develop. We could have chosen to minimize the annotations and specify the mapping inside XML descriptors. For example, in a real application, we recommend externalizing the cache period and the named query so that the code need not change to modify these parameters.
Implementing the Services
This section shows the implementation of the Service. For OpenJPA, we provided both an EJB 3 version of the service and a Java SE version. Because EJB 3 Session Beans are Java classes, the Java SE version just extends the proper EJB 3 class and bootstraps the EntityManager as described earlier. Figure 8.6 shows all the exceptions for our services. See the downloadable sample or refer to Chapter 3 for details.
Figure 8.6 Exceptions from the common example.
We have two implementations of our service: one for Java SE using OpenJPA and another using IBM JPA (built on OpenJPA) for EJB 3. The interface for the service was introduced in Chapter 3, so refer to the details there. For the EJB 3 version, the service interface has an extra @Local annotation to denote a Local EJB. The service implementations vary slightly from a bootstrapping standpoint with Java SE.
Specifically, the Java SE version of the application looks up the EntityManager using the EntityManagerFactory, as illustrated earlier in Figure 8.3. The EJB 3 version is illustrated earlier in Figure 8.5.
The loadCustomer operation uses the EntityManager find method to look up the customer. Because the mapping file defined the proper eager loading, it will load the customer record, an open order if it exists, and any line items for that order. The Inheritance is also automatic; based on the type, it will return the correct subclass. All this happens with one call. Listing 8.75 shows the loadCustomer method as implemented in the EJB 3 version. The Java SE version uses transaction demarcation. You can examine the downloadable source to see the difference. Appendix A shows how to load the code into your Eclipse-based development environment.
Listing 8.75. The loadCustomer Implementation
public AbstractCustomer loadCustomer(int customerId) throws ExistException,GeneralPersistenceException { AbstractCustomer customer = em.find( AbstractCustomer.class, customerId ); return customer; }
The openOrder operation creates an order Java Object and persists it. It then sets the new order onto the customer instance. The transaction is implied because of the nature of EJBs. The openOrder routine will check if an order is open and throw an exception if it is. Listing 8.76 shows the implementation.
Listing 8.76. The openOrder Implementation
public Order openOrder(int customerId) throws CustomerDoesNotExistException, OrderAlreadyOpenException, GeneralPersistenceException{ AbstractCustomer customer = loadCustomer(customerId); Order existingOpenOrder = customer.getOpenOrder(); if(existingOpenOrder != null) { throw new OrderAlreadyOpenException(); } Order newOrder = new Order(); newOrder.setCustomer(customer); newOrder.setStatus(Order.Status.OPEN); newOrder.setTotal(new BigDecimal(0)); em.persist(newOrder); customer.setOpenOrder(newOrder); return newOrder; }
The implementation for the addLineItem operation first checks to see whether the Product exists using the EntityManager find method as seen in the other examples. It then queries to check whether a Line Item already exists, and updates the quantity if it does. Otherwise, it creates a new instance. Again, the transaction is implied due to the EJB 3 method. Listing 8.77 shows the implementation of addLineItem.
Listing 8.77. The addLineItem Implementation
public LineItem addLineItem( int customerId, int productId, long quantity) throws CustomerDoesNotExistException, OrderNotOpenException, ProductDoesNotExistException, GeneralPersistenceException, InvalidQuantityException { Product product = em.find(Product.class,productId); if(quantity <= 0 ) throw new InvalidQuantityException(); if(product == null) throw new ProductDoesNotExistException(); AbstractCustomer customer = loadCustomer(customerId); Order existingOpenOrder = customer.getOpenOrder(); if(existingOpenOrder == null) { throw new OrderNotOpenException(); } BigDecimal amount = product.getPrice().multiply( new BigDecimal(quantity) ); existingOpenOrder.setTotal( amount.add(existingOpenOrder.getTotal()) ); LineItemId lineItemId = new LineItemId(); lineItemId.setProductId(productId); lineItemId.setOrderId(existingOpenOrder.getOrderId()); LineItem existingLineItem = em.find(LineItem.class,lineItemId); if(existingLineItem == null) { LineItem lineItem = new LineItem(); lineItem.setOrderId(existingOpenOrder.getOrderId()); lineItem.setProductId(product.getProductId()); lineItem.setAmount(amount); lineItem.setProduct(product); lineItem.setQuantity(quantity); em.persist(lineItem); return lineItem; } else { existingLineItem.setQuantity( existingLineItem.getQuantity() + quantity ); existingLineItem.setAmount( existingLineItem.getAmount().add(amount) ); return existingLineItem; } }
The removeLineItem operation simply deletes the LineItem by finding the record and removing it. Listing 8.78 shows the implementation.
Listing 8.78. The removeLineItem Implementation
public void removeLineItem( int customerId, int productId ) throws CustomerDoesNotExistException, OrderNotOpenException, ProductDoesNotExistException, NoLineItemsException, GeneralPersistenceException { Product product = em.find(Product.class,productId); if(product == null) throw new ProductDoesNotExistException(); AbstractCustomer customer = loadCustomer(customerId); Order existingOpenOrder = customer.getOpenOrder(); if(existingOpenOrder == null || existingOpenOrder.getStatus() != Order.Status.OPEN) throw new OrderNotOpenException(); LineItemId lineItemId = new LineItemId(); lineItemId.setProductId(productId); lineItemId.setOrderId(existingOpenOrder.getOrderId()); LineItem existingLineItem = em.find(LineItem.class,lineItemId); if(existingLineItem != null) { em.remove(existingLineItem); } else { throw new NoLineItemsException(); } }
The submitOrder operation is almost as simple—it changes the status of the order and removes it from the openOrder property of the abstract customer class. Listing 8.79 shows the submitOrder implementation.
Listing 8.79. The submitOrder Implementation
public void submit(int customerId) throws CustomerDoesNotExistException, OrderNotOpenException, NoLineItemsException, GeneralPersistenceException { AbstractCustomer customer = loadCustomer(customerId); Order existingOpenOrder = customer.getOpenOrder(); if(existingOpenOrder == null || existingOpenOrder.getStatus() != Order.Status.OPEN) throw new OrderNotOpenException(); if(existingOpenOrder.getLineitems() == null || existingOpenOrder.getLineitems().size() <= 0 ) throw new NoLineItemsException(); existingOpenOrder.setStatus(Order.Status.SUBMITTED); customer.setOpenOrder(null); }
Packaging the Components
The environment you deploy to will affect how the application is packaged. A Java SE environment may require you to copy files, or package the code into a JAR. You most likely have to write some deployment scripts. Figure 8.7 shows the Java Project in Eclipse. It is a plain Java Project with the persistence.xml defined in the meta-inf directory. The persistence.xml is necessary to obtain the connection.
Figure 8.7 Packaging.
In a Java EE application, you usually have to package an application in an EAR file. Figure 8.8 shows the layout of the EAR file, which is made up of other files, such as EJB-JAR files for EJBs or WAR files for web applications. See the Java EE specification for details. Notice that we can use the same Java SE JAR as an EJB 3 module as well.
Figure 8.8 Java EE EAR.
The persistence.xml file is packaged in the meta-inf directory and contains our persistence units. We illustrated the format earlier. As with the operation implementations, you can examine the downloadable source for more details.
Unit Testing
Depending on the methodology, you may have coded your unit test before or after implementing the service operations. Agile methods usually push a test-driven approach and encourage coding test cases first. Regardless, our OpenJPA example contains the unit test to run the application. Figure 8.9 shows the Unit Test project for the common example.
Figure 8.9 Unit Test package.
Chapter 3 explains the aspects of the unit test that are the same regardless of the persistence mechanism used (as it should be). The only difference with OpenJPA is that we also provide the option to look up the Session Bean in the Java EE case and use JUnitEE to run it. There is no JPA-specific information within the unit test, but there is EJB information. As long as you have the Java EE API JARs, this unit test can run in both a Java SE environment and a standard Java EE environment. Examine the downloadable source for details and Appendix A for instructions on how to run them.
Deploying to Production
Deploying to production involves setting the proper configuration values for the target environment. Other than the common testing needed to move an application to production, there are no additional considerations other than those already specified in this section. In a Java SE environment, you have to run the bytecode enhancer yourself. In a Java EE container, like WebSphere Application Server, the bytecode enhancer is run automatically when the installation tools are run, so it will not be an explicit step.