|
Dear Won't Get Fooled,
Again, I am very impressed. Yes, I did laugh when I saw the quantity of the code, but on the other hand its quality is really great! Your code follows all the JDBC best practices I know about, and then some. Just to summarize what I see you doing:
- Create resources as late as possible and release them as early as possible.
- Handle errors properly, and make sure to use the finally clause to do actions that should occur whether there was an exception or not.
- You set access intents on the connection and use prepared statements to optimize the communications. (I especially liked your using the setFetchSize() on the statement in conjunction with the Vector initializer and increment!)
- And, of course, your SQL used specific column selects and joins where possible to minimize the number and size of statements sent to the database tier and the amount of data returned.
This code sets the bar pretty high with respect to comparisons we can make to the current code and any suggested improvements.
Believe it or not, though, I think you can do better that JDBC when using CMPs with CMRs. Just by looking at the session bean code, and without asking to look at your entity EJB code or deployment descriptors, I had suspected that you did not use CMRs in your entity EJB components. Why? Your session bean was procedural rather than object-oriented. That is, it procedurally handled all the logic of "navigating" the relationships between the four entities involved instead of delegating to the entity. If it were object-oriented, I would have expected to see the following code in your session:
public CustomerData getOpenOrderForCustomer(int id)
throws OrderNotOpenException
{
// Get the Customer DTO
CustomerKey key = CustomerKey new(id)
Customer ref = cHome.findByPrimaryKey(key);
return ref.getDataWithOpenOrder();
}
|
As a brief aside, this is a true session facade pattern: the session bean delegates the business logic to another class - in this case the Customer entity, which represents the top of an object graph. The session bean merely adds transactions, security, and distribution as qualities of service.
That said, I appreciate the fact that you coded your CMP and JDBC access logic directly in the session bean rather than delegate to a helper POJO. You must have read my first article, too. But whether the logic calling the CMPs is in the session bean or a POJO, it is still procedural and not object-oriented in nature.
To get back to the main point, if I had seen the true session facade pattern passing through to an entity above, I still would not have known you were using CMRs until I looked at the Customer. Its "guts" might have looked like:
// CMP fields
public abstract String getName();
public abstract void setName(String value);
public abstract int getOpenOrderId();
public abstract void setOpenOrderId(int value);
// Custom getters
public CustomerData getData() {
CustomerData data = new CustomerData();
data.setKey(getPrimaryKey());
data.setOpenOrderId(getOpenOrderId());
return data;
}
public CustomerData getDataWithOpenOrder(int cId) {
// Check to see if there is an open order
int oId = getOpenOrderId();
if (oId == 0) {
throw new OrderNotOpenException(cId);
}
// Get the Order entity and the DTO object
Order oRef = oHome.findByOrderId(oId);
OrderData oData = oRef.getDataWithLineItems();
CustomerData data = getData();
data.setOrder(oData);
return data;
}
|
This code would have indicated that you were using object oriented delegation, but not using CMRs. If you were using CMRs, the code would have looked something like this:
// CMP fields
public abstract String getName();
public abstract void setName(String value);
public abstract int getOpenOrderId();
public abstract void setOpenOrderId(int value);
// CMR fields
public abstract Order getOpenOrder();
public abstract void setOpenOrder(Order value);
public abstract Collection getOrders();
public abstract void setOrders(Collection value);
// Custom getters
public CustomerData getData() {
CustomerData data = new CustomerData();
data.setKey(getPrimaryKey());
data.setOpenOrderId(getOpenOrderId());
return data;
}
public CustomerData getDataWithOpenOrder() {
// Check to see if there is an open order REFERENCE
Order oRef = getOpenOrder ();
if (oRef == null) {
throw new OrderNotOpenException(cId);
}
// Get the Open Order data
OrderData oData = oRef.getDataWithLineItems();
CustomerData data = getData();
data.setOrder(oData);
return data;
}
|
I am sure you can generalize either the procedural or object-oriented pattern working its way through the graph, and that using CMRs is independent of this delegation. I will provide the guts of the Order, Line Item, and Product entities for the object-oriented CMR case, starting with the Order, since we ultimately want to compare to the procedural JDBC code above:
// CMP fields
public abstract String getStatus();
public abstract void setStatus(String value);
// CMR fields
public abstract Customer getCustomer();
public abstract void setCustomer(Customer value);
public abstract Collection getLineItems();
public abstract void setLineItems(Collection value);
// Custom getters
public OrderData getData() {
OrderData data = new OrderData();
cData.setKey(getPrimaryKey());
data.setStatus(getStatus());
return data;
}
public OrderData getDataWithLineItems() {
// Use CMR to get the line items into an array
Collection liList = getLineItems();
int liSize = liList.size();
LineItemData[] liArray = new LineItemData[liSize];
Iterator liIterator = liList.iterator();
LineItem liRef = null;
for (int i = 0; i < liSize; i++) {
// Get the Line Item DTO
liRef = (LineItem) liIterator.next();
liArray[i] = liRef.getDataWithProduct();
}
// Create the object and return
OrderData data = getData();
data.setLineItems(liArray);
return data;
}
|
Here is the Line Item:
// CMP fields
public abstract int getQuantity();
public abstract void setQuantity(int value);
public abstract int getAmount();
public abstract void setAmount(int value);
// CMR fields
public abstract Order getOrder();
public abstract void setOrder(Order value);
public abstract Product getProduct();
public abstract void setProduct(Product value);
// Custom getters
public LineItemData getData() {
LineItemData data = new LineItemData ();
data.setKey(getPrimaryKey());
data.setQuantity(getQuantity());
data.setAmount(setAmount());
return data;
}
public CustomerData getDataWithProduct() {
// Get the Product from the CMR
Product pRef = getProduct();
ProductData pData = pRef.getData ();
LineItemData data = getData();
data.setProduct(pData);
return data;
}
|
And, finally, here is the Product:
// CMP fields
public abstract String getDescription();
public abstract void setDescription(String value);
public abstract int getPrice();
public abstract void setPrice(int value);
// CMR fields
public abstract Collection getLineItems();
public abstract void setLineItems(Collection value);
// Custom getters
public ProductData getData() {
ProductData data = new ProductData();
data.setKey(getPrimaryKey());
data.setDescription(getDescription());
data.setPrice(getPrice());
return data;
}
|
So now we can truly compare apples, the variety of "apples" being:
- Procedural/JDBC
- Procedural/CMP
- Procedural/CMR
- OO/JDBC
- OO/CMP
- OO/CMR.
I include the OO/JDBC case just to be complete - which is basically any approach where "custom" objects are developed one-to-one with your business objects (four of them in your case). Entity EJBs with bean managed persistence (BMPs) would be included in this list, as would "JDO-like" objects without the tool or JVM support. In other words, if you custom code JDBC associated with a single business object with the expectation that it can call others within its own methods, and be called in the context of others, then it is considered OO/JDBC.
Now, on to the evaluation.
First off, and as you realized I would point out, any individual method in any CMP/CMR case is far, far simpler than any method with directly-coded JDBC because of the complexity of the SQL and framework. I also mention an "obvious" point that needs making: if you suddenly switch databases, the JDBC implementations no longer work (unless you modify the code, or use data sources or property files or environment variables as an approach to getting the connection URL). And even then, if you move away from relational database as your underlying datastore, then you are totally dead.
Next, and as you also pointed out, there are way more methods involved in developing a given unit of work when using any CMP/CMR approach (or even the OO/JDBC approach) than when using procedural/JDBC - in part because of all the custom gets and sets involved. But these custom object-oriented methods of any type are reusable, while procedural code, especially that using JDBC, has to be hand-crafted to optimize it to the scenario.
For an example of reuse, you can easily expose the various getData methods from any entity bean (CMP or CMR) up to the session with the appropriate key fields being passed in. This session method would be a true facade, as mentioned above. For example, say we wanted to get just the "standalone" DTO associated with a given entity type. We could write four very simple session facade methods:
public CustomerData getCustomerData(int id)
throws FinderException
{
// Get the Customer DTO
CustomerKey key = CustomerKey new(id)
Customer ref = cHome.findByPrimaryKey(key);
return ref.getData ();
}
public OrderData getOrderData(int id)
throws FinderException
{
// Get the Order DTO
OrderKey key = OrderKey new(id)
Order ref = oHome.findByPrimaryKey(key);
return ref.getData ();
}
public LineItemData getLineItemData(int orderId, int productId)
throws FinderException
{
// Get the LineItem DTO
LineItemKey key = LineItemKey new(orderId, productId)
LineItem ref = liHome.findByPrimaryKey(key);
return ref.getData ();
}
public ProductData getProductData(int id)
throws FinderException
{
// Get the Product DTO
ProductKey key = ProductKey new(id)
Product ref = pHome.findByPrimaryKey(key);
return ref.getData ();
}
|
The true benefit of reusability, though, is maintainability; by using the custom methods associated with the DTO derived from your data model, you only need change the various entity bean getData() and setData() methods when you add or remove an attribute (whether it is related to a CMP field or a CMR). Every procedural or OO/JDBC method related to those fields would need to change if you modified the schema.
When using CMRs, it is rarely necessary to get the home and then use a custom finder to retrieve one or more references to entities; a CMR does that automatically behind the scenes when one is declared. This simplification results in a lot less parameters being passed around when compared to procedural and CMP-only approaches. In fact, when using CMRs, even if procedural, only one finder needs to be explicitly called in a unit of work. And this count includes that for the stateless session bean reference, since it can be cached in the client! Also, in many cases with OO/CMRs, the entity lookup is using a findByPrimaryKey() method. This means that you have to specify fewer custom finders in the deployment descriptors. Less is more, when it comes to maintainability and usability.
Related to this point about the key, when using CMRs, often only the session bean (or "ultimate client", like the servlet or JSP) needs to know what the key fields of an entity are, even with respect to the custom methods of the entity itself. This makes entities even simpler and easier to maintain, and even more so if CMRs are consistently used. You could modify the key fields and not have to do more than redeploy. When not using CMRs, your code has to know the key fields to find a related object, and you are more likely to use custom finders in your code, whether procedural or OO style.
Now we will address your last and, presumably, most important point. It is clear from your load tests that the procedural/JDBC performs with better throughput than the procedural/CMP case. And there is no reason to think that an OO/CMP case would run any better or worse than a procedural CMP (meaning 2X worse in your case than the procedural JDBC), since the only thing that changes significantly is the delegation.
What is not clear is whether the CMR case would run as well as the hand-crafted JDBC code, regardless of whether either technology uses a procedural or OO approach. We submit that when using CMRs, the container can be tuned in some application servers, like IBM WebSphere Application Server, to optimize the SQL in ways similar to that which you hand coded. For example, "access intents" can be specified that enable you to set the read ahead size (similar to setFetchSize), read limit (similar to setMaxRows), and preload caching (similar to joins). You can also "tweak" the columns that are loaded in the selects. (See the IBM WebSphere Application Information Center document on application profiling.)
If you would be inclined to argue that dealing with CMRs and profiles and access intents makes the entities as complicated as directly coding optimized JDBC, I would offer a number of points:
- CMRs are model information similar to foreign keys in the database. Unlike foreign keys, however, they also make the application code simpler, regardless of whether they are OO or procedural in nature.
- Explicitly-specified access intents should only be used when the out-of-the-box performance does not meet specified goals; for example, the case we discussed last month, when one or more unrelated entities are the target of the unit of work, you discovered that performance is within just a few percent and well worth the simpler programming model.
- EJB containers are getting better at mapping CMPs with CMRs to the underlying data stores all the time, making it possible to redeploy later and get performance benefits that are impossible if you hand code the JDBC.
- Where access intents do need to be specified for performance reasons, the code of the EJBs does not need to change, only the tuning parameters do. Thus, the code will always function, even if it does not perform as well as it could. This guarantee has a huge impact on the development and testing cycle.
- Where a number of units of work share the same basic set of access intents, such as all those that need the order header with the customer (like a submit, cancel, and show orders method), you can create a common application profile and tune these functions together.
Without CMRs, your Procedural or OO/CMP code would implicitly issue a number of separate SQL statements: one for the customer, one for the order, one for the list of line items, and one for each product. No wonder CMPs don't perform as well as hand-coded JDBC.
But with full exploitation of CMRs, the application can sometimes be tuned with simple object navigation path expressions to issue just one SQL statement (with zero or more inner joins to handle multi-cardinality navigations; my friend Stacy calls these "honking big" joins). In other words, it may be possible that applications fully exploiting CMRs could perform better than the code your average JDBC programmer is willing to write (I hate to even look at SQL with inner joins - especially when someone else wrote it!).
So, hopefully this evaluation has convinced you to use CMRs. And, interestingly enough, even when your code does not use them explicitly, you still can add CMRs to the entities and redeploy without changing any methods. Then, as long as your code always uses the findByPrimaryKey() method, the container can try to exploit the CMRs in the generated SQL. This point is key - if you will pardon the pun. The EJB 2.0 specification makes it clear that calling a custom finder method cannot be circumvented since there may be essential logic hidden within. See point #3, above, to see that this is a no-risk option, even if you do not change one line of code.
But, it would not be that hard for you to go back and change your procedural/CMP method such that it is compatible with CMRs, like so:
public CustomerData getOpenOrderForCustomer(int cId) {
// Get the Customer DTO
// Using PK here to discourage use of custom finders
// But since this is the "root" of the call, it is optional
Customer cRef = cHome.findByPrimaryKey(new CustomerKey(cId));
CustomerData cData = cRef.getData();
// Check to see if there is an open order
int oId = cData.getOpenOrderId();
if (oId == 0) {
throw new OrderNotOpenException(cId);
}
// Get the Order entity and the DTO object -fBPK is mandatory
Order oRef = oHome.findByPrimaryKey(new OrderKey(oId));
OrderData oData = oRef.getData();
cData.setOrder(oData);
// Get the array of Line Items DTOs set up
// When not using CMRs, this custom finder is mandatory!
Collection liList = liHome.findAllItemsForOrderId(oId);
int liSize = liList.size();
LineItemData[] liArray = new LineItemData[liSize];
oData.setLineItems(liArray);
Iterator liIterator = liList.iterator();
LineItem liRef = null;
LineItemData liData = null;
Product pRef = null;
for (int i = 0; i < liSize; i++) {
// Get the Line Item DTO
liRef = (LineItem) liIterator.next();
liData = liRef.getData();
// Get the Product DTO -fBPK is mandatory to support join
pRef = pHome.findByPrimaryKey(new ProductKey(pId));
liData.setProduct(pRef.getData());
liArray[i] = liData;
}
return cData;
}
|
This code could be tuned to issue as few as two SQL statements, because the custom findAllItemsForOrderId() method is called with a custom finder, forcing a "reset". With this level of tuning, the procedural CMP-CMR (meaning CMRs are defined but not explicitly used in the code) should perform within a few percent of your hand coded JDBC, which also issues two SQL statements.
But you will actually find it simpler to use the CMRs once you have gone to the trouble to specify them -- even if the code is procedural - and basically the same lines of code as above must change:
public CustomerData getOpenOrderForCustomer(int cId) {
// Get the Customer DTO
// Using PK here to discourage use of custom finders
// But since this is the "root" of the call, it is optional
Customer cRef = cHome.findByPrimaryKey(new CustomerKey(cId));
CustomerData cData = cRef.getData();
// Check to see if there is an open order
// Note that this check is now much more "meaningful" since
// the code does not have to interpret '0' as no open order
Order oRef = cData.getOpenOrder();
if (oRef == null) {
throw new OrderNotOpenException(cId);
}
// Get the Order DTO object, since we already have the ref
OrderData oData = oRef.getData();
cData.setOrder(oData);
// Get the array of Line Items DTOs using the CMR
Collection liList = oRef.getLineItems();
int liSize = liList.size();
LineItemData[] liArray = new LineItemData[liSize];
oData.setLineItems(liArray);
Iterator liIterator = liList.iterator();
LineItem liRef = null;
LineItemData liData = null;
Product pRef = null;
for (int i = 0; i < liSize; i++) {
// Get the Line Item DTO
liRef = (LineItem) liIterator.next();
liData = liRef.getData();
// Get the Product DTO using the CMR
liData.setProduct(liRef.getProduct().getData());
liArray[i] = liData;
}
return cData;
}
|
Don't forget that you may get to throw away most of those pesky homes and IC lookups in your session ejbCreate() methods by building (or better yet, generating) a session facade per entity. What a win-win! After this, I hope you will start signing yourself as:
Led to Water
I also hope you drink it, and not the stronger stuff that gets passed around.
OK then, Your EJB Advocate
|