6.6.0.15.5: Implementing custom finder helpers for CMP entity beansTo work with custom finder helpers for CMP entity beans, there are two tasks that you need to complete:
The task of adding custom finder helpers to the home interface is found in the WebSphere Studio Application Developer help topic "Adding methods to the home and remote interfaces." The task of implementing custom finder helpers is described in the remainder of this topic in the following sections:
Introduction to working with custom finder helpersTo implement custom finder helpers, you can use one of two supported query languages:
EJB query language is recommended for implementing custom finder helpers, but the use of SQL is still supported. To implement custom finder helpers, you should use a deployment descriptor extension document rather than a helper finder interface. Both EJB query language strings and SQL query strings and method declarations can be used in a deployment descriptor extension document. The use of helper finder interfaces has been deprecated but is still supported to some extent. If you are working with existing EJB 1.0 JAR files, you can continue to define SQL query strings or method declarations in the helper finder interface. (The SQL query string is actually a field on the interface that describes the WHERE clause.) However, for any new development work that requires you to work with EJB JAR files at a level higher than 1.0, it is required that you use a deployment descriptor extension document rather than the finder helper interface to define your queries or method declarations. Implementing custom finders using EJB query language and SQLThe EJB query language defines finder methods for entity beans with container-managed persistence. The definition uses a language based on SQL that allows searches on the persistent attributes of an enterprise bean and associated bean attributes. The query language is independent of the bean's mapping to a relational datastore and is portable. The query language is compiled into SQL at deployment time based on the schema mapping for the bean. An EJB query is a string that contains
An EJB query also contains input parameters that correspond to the arguments of the finder method. Additional information is found in the WebSphere Studio Application Developer help topic "EJB query language - overview." The following types of custom finders are currently supported for enterprise beans and can be used in combination:
The WHERE and method custom finders are standard SQL techniques used to query a database column. For each finder method that is defined in the EJB home interface (other than findByPrimaryKey and those finder methods generated to support associations), one of the following queries or declarations must be defined in the deployment descriptor extension document or in the finder helper interface (in file beanClassNameFinderHelper.java):
The return type java.util.Enumeration or java.util.Collection on the finder in the home interface indicates that this finder may return more than one bean. Using the remote interface as the return type indicates that a single bean is returned. This is true for all of the supported types of custom finders. The code generated into the persister handles this distinction. For SQL WHERE and method custom finders, there may be situations where the finder methods access different databases. In this case, it is necessary to ensure that SQL compatibility is maintained across the different databases. For instance, it is possible that the SQL syntax used by each database is different. In these situations, use the SQL extensions defined by JDBC to resolve the database differences. For example, assume that you are developing a CMP entity bean that requires a finder method that involves a timestamp/date field. Also assume that this bean will be deployed to DB2 and Oracle databases. The problem is that the format of the timestamp/date fields in DB2 and Oracle are different, which causes difficulties in defining one WHERE clause for use with both DB2 and Oracle. The solution to this particular problem is to use the SQL Escape sequence. Defining custom finder helpers in deployment descriptor extension documentsIf you are working with existing EJB 1.0 JAR files, you can continue to define SQL query strings, method declarations, or EJB query language strings in the helper finder interface. However, this mechanism for defining queries has several restrictions, such as the inability to create polymorphic finders. Therefore, for any new development work, it is recommended that you use a deployment descriptor extension document rather than the finder helper interface to define your queries and method declarations. The extension document you need to use is saved in the JAR file and is named: META-INF/ibm-ejb-jar-ext.xmi The extension document contains query tags rather than query strings. You can edit the extension document much like a deployment descriptor file. The following example uses standard SQL and EJB query language finder syntax and will help you understand the format of the extensions document. You can use the example as a template and copy/paste the tagging for your own use. <ejbext:EJBJarExtension xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:ejbext="ejbext.xmi" xmlns:ejb="ejb.xmi" xmlns:ecore="ecore.xmi" xmi:id="ejb-jar_ID_Ext"> <ejbJar href="META-INF/ejb-jar.xml#ejb-jar_ID"/> <ejbExtensions xsi:type="ejbext:ContainerManagedEntityExtension" xmi:id="Department_Ext" name="Department"> <enterpriseBean xsi:type="ejb:ContainerManagedEntity" href="META-INF/ejb-jar.xml#Department"/> <finderDescriptors xsi:type="ejbext:WhereClauseFinderDescriptor" xmi:id="Department_findByName_WhereClause_ID" whereClause="T1.NAME LIKE ?"> <finderMethodElements xmi:id="MethodElement_11" name="findByName" parms="java.lang.String" type="Home"> <enterpriseBean xsi:type="ejb:ContainerManagedEntity" href="META-INF/ejb-jar.xml#Department"/> </finderMethodElements> </finderDescriptors> <finderDescriptors xsi:type="ejbext:EjbqlFinderDescriptor" xmi:id="Department_findByName_ejbql_ID" ejbqlQueryString="select e from DepartmentBean name like ?"> <finderMethodElements xmi:id="MethodElement_12" name="findByEjbName" parms="java.lang.String" type="Home"> <enterpriseBean xsi:type="ejb:ContainerManagedEntity" href="META-INF/ejb-jar.xml#Department"/> </finderMethodElements> </finderDescriptors> <finderDescriptors xsi:type="ejbext:UserFinderDescriptor" xmi:id="Department_findSpecial_User_ID"> <finderMethodElements xmi:id="MethodElement_13" name="findSpecial"type="Home"> <enterpriseBean xsi:type="ejb:ContainerManagedEntity" href="META-INF/ejb-jar.xml#Department"/> </finderMethodElements> </finderDescriptors> </ejbExtensions> </ejbJar> For each finder method in the extension document, you need to provide a finderDescriptor element that consists of two parts: the finderMethodElement, which identifies the method to which the finder descriptor applies, and the query string, which can be specified as one of the following types:
These three types of query string have the following unique xsi:type:
The name of the class that contains the custom finder method is beanClassNameFinderObject, where beanClassName is the name of the implementation class for the bean. The FinderObject class must be in the same package as the bean implementation class. In the extensions document, you can also employ a user finder descriptor, which the deployment tool uses to determine the finder implementation method provided by the user. In the above example, the last finderDescriptors tag specifies a user finder descriptor. This descriptor simply serves as a placeholder, because it contains no "string" like the other descriptors. The name of the custom finder is there, however. It is named findSpecial. As a result, in the beanClassNameFinderObject class provided by the bean developer, there must be a corresponding method called findSpecial. Since two separate findSpecial methods are used, care must be taken to understand the difference between the two. The essential difference is that the bean developer defines one findSpecial method in the home interface and the other findSpecial method in the FinderObject class. Both take the same arguments, but they return different types and serve different purposes. They are given the same name as a convenient way to tie them together. When a bean developer needs a custom finder, the developer defines it in the home interface. For CMP entity beans, the EJB Deploy Tool generates the code that implements the custom finder. For all but the custom method finder, all that is needed is a string. For the custom method finder, the bean developer needs to actually implement a method in the FinderObject. The deploy tool still generates the code that implements the findSpecial custom finder, but that generated code will call the findSpecial method in the FinderObject. During deployment, if a JAR file contains finder helper interfaces, the EJB Deploy Tool will migrate the appropriate SQL strings into a field contained within the extensions document (ibm-ejb-jar-ext.xmi). This string is what is used during deployment to emit code contained within the emitted JDBC persister. In WebSphere Application Server, Version 3.5, the finder helper interface was used directly. Once the information exists in the extensions document, that information will be used and any subsequent changes to the finder helper interface will be ignored. This field can be modified by hand or by using the Application Assembly tool. Note that when enterprise beans are exported from VisualAge for Java, Enterprise Edition, Version 4.0 using the Export Tool for Enterprise Java Beans 1.1, or when 1.0 EJB JAR files are deployed with the deployment tool, the finder meta data is migrated to the new form from the finder helper interfaces. If the enterprise beans are exported using the Export Tool for Enterprise Java Beans 1.1, the redundant classes are filtered from the exported JAR. If the enterprise beans are not exported using the Export Tool for Enterprise Java Beans 1.1 and are imported along with the redundant classes, the classes are simply ignored. The AutoWorld exampleTo introduce this AutoWorld example, consider the following hierarchy of CMP entity beans: Outside of this CMP bean hierarchy, there are two other existing CMP entity beans: VapGarage and VapMotorVehiclePart. An association exists between VapGarage and VapVehicle, in which a garage can hold many vehicles. Another association exists between VapMotorVehiclePart and VapVehicle, in which a vehicle can contain many parts. In the remainder of this topic, the AutoWorld example is used to show you how to work with queries defined in the finder helper interface.
InfoCenter Home > Using the finder helper interfaceIn this section, the AutoWorld example is used to show you how the following custom finders are defined in the finder helper interface: SQL WHERE custom findersA custom finder in which you enter only the filtering WHERE clause into the finder helper interface is called a WHERE custom finder. Using the garage example, the finder helper interface would look like this: public interface VapGarageBeanFinderHelper { public final static String findCapacityGreaterThanWhereClause = "T1.CAPACITY > ?"; } Notice that any dependency on the shape of the results is removed from the string. Two dependencies still exist:
The name of the column would change only if you took action to change it. The alias for the table will be the same from one generation to the next unless tables are added to or removed from the mapping. In single-table cases, this may not seem significant (the alias is always T1). When multiple tables are used, this is very important. For example, consider the VapVehicle hierarchy. When it is mapped using root/leaf inheritance mapping, there is at least one table for each class in the hierarchy, and the query into which the WHERE clause would be inserted would have multiple subselects. The WHERE clause would be inserted into each subselect, and it would be valid SQL syntax for each subselect to use a different alias for the same table. However, our query generation ensures that the same table has the same alias across the entire query. If it did not, this WHERE clause substitution technique could not work. In these cases, the code generated into the persister knows to inject any finder parameters into the query multiple times. There is one very important restriction when using this form of custom finder in a mapped hierarchy: The WHERE clause can only reference tables that map the bean in which the finder is defined or tables that map one of bean's parent beans. For example, a WHERE clause in the VapVehicleBeanFinderHelper can only reference columns in the table used to map the VapVehicle bean. However, a WHERE in the VapAutomobileBeanFinderHelper can reference columns from either the table that maps VapAutomobile or VapVehicle (its superclass). Any table references in your handwritten SQL code must match the table aliases set up in the genericFindSqlString field. This is declared in the enterprise bean's generated persister. The number of finder parameters must match
the number of injection points (the For example, the home interface may contain the following method: public java.util.Enumeration findGreaterThan (int threshold) throws java.rmi.RemoteException, javax.ejb.FinderException; In this finder helper interface, the WHERE custom finder is one of the forms that you can provide. For example (line broken for publication): public static final String findGreaterThanWhereClause = "T1.VALUE > ?"; Method custom findersA custom finder in which you enter a method signature into the finder helper interface is called a method custom finder. It is the most flexible type of custom finder, but it requires more work on your part. Using the same garage example as before, the finder helper interface would look like this: public interface VapGarageBeanFinderHelper { public java.sql.PreparedStatement findCapacityGreaterThan(int threshold) throws Exception; } Unlike the WHERE form, however, this is not enough for method custom finders. An implementation of this method is needed. You provide your implementation of the method in a class that follows these rules:
To finish off our example, the finder object would look like this. /** * Implementation class for methods in * VapGarageBeanFinderHelper. */ public class VapGarageBeanFinderObject extends com.ibm.vap.finders.VapEJSJDBCFinderObject implements VapGarageBeanFinderHelper { public java.sql.PreparedStatement findCapacityGreaterThan(int threshold) throws Exception { PreparedStatement ps = null; int mergeCount = getMergedWhereCount(); int columnCount = 1; ps = getMergedPreparedStatement("T1.CAPACITY > ?"); for (int i=0; i<(columnCount*mergeCount); i=i+columnCount) { ps.setInt(i+1, threshold); } return ps; } } In the case of any method custom finder, the generated persister uses your implementation to create the PreparedStatement to be executed. The persister will execute the PreparedStatement and handle the results. The implementation needs help from the persister to make sure the result set for the query has the correct shape. The com.ibm.vap.finders.VapEJSJDBCFinderObject base class provides several important helper methods, some of which are shown in the above example. In the following table, the complete set of helper methods are listed and described:
This is a rather simple case that can better be handled by a WHERE custom finder. More complex examples are possible that a WHERE custom finder simply could not handle. For example, suppose you wanted a finder that took a more complex object and injected it into multiple columns in a WHERE clause. You could end up with a finder method that looked like this: public java.sql.PreparedStatement findWithComplexObject(BigObject big) throws Exception { PreparedStatement ps = null; int mergeCount = getMergedWhereCount(); int columnCount = 3; int anInt = big.getAnInt(); String aString = big.getAString(); String aLongAsString = com.ibm.vap.converters.VapStringToLongConverter. singleton().dataFrom(big.getLongObject()); ps = getMergedPreparedStatement("(T1.ANINT > ?) AND (T1.ASTRING = ?) AND (T2.ALONGSTR < ?)"); for (int i=0; i<(columnCount*mergeCount); i=i+columnCount) { ps.setInt(i+1, anInt); if (aString == null) ps.setNull(1, java.sql.Types.VARCHAR); else ps.setString(i+2, aString); if (aLongAsString == null) ps.setNull(1, java.sql.Types.VARCHAR); else ps.setString(i+3, aLongAsString); } return ps; } Even more complex examples are possible. For instance, an object could be passed that contains the WHERE clause (or instructions on how to create it) in addition to the data. Or, there could be multiple parameters, each representing different conditions in the WHERE clause. The following example is a logical representation of how a many-to-many association could be accomplished using a complex method custom finder. The example involves a many-to-many association between Product and Customer beans, using a intermediary bean (ProdCustLink) and two 1:m associations: You can write method custom finders to span the relationship in either direction with just one method call. For this example, consider one direction only: a finder in Customer that retrieves all Customer instances that are associated with a given product key. Customer's home interface contains the appropriate method signature, as follows: java.util.Enumeration findCustomersByProduct(prod.cust.code.ProductKey inKey) throws java.rmi.RemoteException, javax.ejb.FinderException; Customer's finder helper interface contains the signature for the corresponding finder method: public java.sql.PreparedStatement findCustomersByProduct(prod.cust.code.ProductKey inKey) throws Exception; The finder object (CustomerBeanFinderObject) builds and caches the query string for the finder as well as implements the finder method. public class CustomerBeanFinderObject extends com.ibm.vap.finders.VapEJSJDBCFinderObject implements CustomerBeanFinderHelper { private String cachedFindCustomersByProductQueryString = null; . . . } Through lazy initialization in the finder object, the accessor method for the query-string field builds up the query string by first merging the WHERE condition into the query template and then adding a reference to the intermediate table into the FROM clause. The first half of the accessor method uses a genericFindInsertPoints array to locate and update each WHERE clause. Then, the second half of the method counts forward from the beginning of each FROM clause, inserts the reference to the intermediate table into the query string as needed, and updates the query-string field. protected String getFindCustomersByProductQueryString() { if (cachedFindCustomersByProductQueryString == null) { // Do the WHERE first // so that the genericFindInsertPoints are correct. int i; int[] genericFindInsertPoints = getGenericFindInsertPoints(); StringBuffer sb = new StringBuffer(getGenericFindSqlString()); for (i = 0; i < genericFindInsertPoints.length; i++) { sb.insert(genericFindInsertPoints[i], "(T1.id = T2.Customer_id) AND (T2.Product_id = ?)"); } // Make sure to update every FROM clause. String soFar = sb.toString(); int fromOffset = soFar.06060015_indexOf(" FROM "); while (fromOffset != -1) { sb.insert((fromOffset+5)," ProdCustLink T2, "); soFar = sb.toString(); fromOffset = soFar.06060015_indexOf(" FROM ", (fromOffset+5)); } cachedFindCustomersByProductQueryString = sb.toString(); } return cachedFindCustomersByProductQueryString; } After this method call, the query string looks something like the following: SELECT <columns> FROM ProdCustLink T2, CUSTOMER T1 WHERE((T1.id = T2.Customer_id) AND (T2.Product_id = ?)) Also in the finder object, the implemented finder uses the query string to create a PreparedStatement. Last but not least, the product ID value is added into each WHERE clause by using the superclass method getMergedWhereCount() in the iteration loop. public java.sql.PreparedStatement findCustomersByProduct(ProductKey inKey) throws java.lang.Exception { // Get the full query string and make a PreparedStatement. java.sql.PreparedStatement ps = getPreparedStatement(getFindCustomersByProductQueryString()); // Inject the product id parameter into each merged WHERE clause. for (int i = 0; i < getMergedWhereCount(); i++) { if (inKey != null) ps.setInt(i+1, inKey.id); else ps.setNull(i+1, 4); } return ps; } |
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|