The 11g release of the Oracle Database gave us the PL/SQL Function Result Cache. This feature can help speed up performance in calling PL/SQL functions by caching function When you invoke a function has been set up to use a result cache, the PL/SQL engine will check whether the cache already contains a result for the set of input parameter values for this current call. If so, the result is retrieved from the cache and returned to the invoker. If the result is not yet in the cache, it will be put there as the function completes execution. This of course means that for frequently called functions that take some time to execute, substantial performance gains can be achieved.
I was thinking about this feature while working with ADF and WebServices. I have been using with this nice temperature conversion service – Fahrenheit to Celsius for example – and it did the job very well. However, the call to web service took anywhere between 2 and 6 seconds – way too long for an interesting user experience. And given the fact that this web service is a deterministic one – same input will always return the same response, if I would have something like a client side result cache that I could first inspect for a non-expired result for this current input, I may be able to at least prevent some slow service calls and replace them with very fast cache accesses.
This article describes how to set up WebService calls from ADF with a crude attempt at ResultCache implementation.
I will first create an ADF Data Control for a WebService (based on the WSDL). Using items bound to this data control, I will create a JSF page that allows users to convert temperatures. Unfortunately , we will discover that while calling the service works fine, it is way too slow. And given the fact that 80 degrees Fahrenheit today will convert to the same temperature expressed in degrees Celsius as it will be tomorrow. So calling the (slow) WebService over and over again with input parameters we already know the outcome for, we will attempt to find a smarter way of only calling the service if we really have to.
We will first implement this cache in the View layer using a managed bean between the page and the operation binding for the WebService call. Then I will try to add this Result Cache functionality at the Model level.
1. Create a WebService Data Control
Go to the new gallery and in the Web Services catagory,pick Web Service Data Control.
Fill in the WSDL Url in the wizard and tab out of the field. The wizard will download the WSDL and extract the Service Name.
Select the ConvertTemperature Operation on the second wizard step:
and press next until the last step is reached. An overview of the selected options is shown:
Now press finish. The Data Control is created and published on the Data Control palet.
Now create a new JSF page. Drag and drop the Operation from the Data Control Palette, as an ADF Parameter Form:
The Operation Binding is created in the Page Definition file and a Form with Input Fields for the Parameters as well as a submit button to invoke the service is added to the page:
Now again drag and drop, this time the Result attribute, drop it as an output text:
The Output Text item that will display the result of calling this ConvertTemperature service is added to the page, bound to the appropriate attribute in the Page Definition file:
Now it is time to run the page and see whether we can successfully invoke the web service:
and press OK:
It takes a while, but we get the result we were after. (it may not seem that warm, but add the humidity…)
Now the page is doing what we wanted it to do, but the conversion takes quite long. And doing it the second time round for 80 degrees Fahrenheit takes just as long as the first attempt. No smart result cache around at this point.
2/ View Level implementation of Operation Result Cache
The quick and dirty way of implement a result cache could be at the view-level, as we will see next.
In this implementation, I will insert a managed bean between between the Page and the OperationBinding. The bean will only invoke the OperationBinding that then leverages the WebService Data Control to make the actual call to the Web Service if it cannot retrieve the value for the given input from the application scope result cache (another managed bean). The input fields are value bound to the managed bean, as is the button and the output text field. The managed bean will provide the input parameters when invoking the OperationBinding.
a. Create Class TemperatureConverterProxyBean
package temperatureconverter;
import java.util.Map;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;
import oracle.adf.model.binding.DCBindingContainer;
import oracle.adfinternal.view.faces.model.binding.FacesCtrlActionBinding;
public class TemperatureConverterProxyBean {
String fromUnit ="degreeFahrenheit";
String toUnit = "degreeCelsius";
Double temperature;
Double convertedTemperature;
public TemperatureConverterProxyBean() {
}
public void setFromUnit(String fromUnit) {
this.fromUnit = fromUnit;
}
public String getFromUnit() {
return fromUnit;
}
public void setToUnit(String toUnit) {
this.toUnit = toUnit;
}
public String getToUnit() {
return toUnit;
}
public void setTemperature(Double temperature) {
this.temperature = temperature;
}
public Double getTemperature() {
return temperature;
}
private String getCacheKey() {
StringBuffer cacheKey = new StringBuffer("convertTemperatue");
cacheKey.append(";*;").append(this.fromUnit);
cacheKey.append(";*;").append(this.toUnit);
cacheKey.append(";*;").append(this.temperature.toString());
return cacheKey.toString();
}
private Object findInCache() {
Map functionResultCache = (Map)getExpressionValue("#{FunctionR esultCache}");
Object cachedValue = functio nResultCache.get(getCacheKey());
return cachedValue;
}
public String convertTemperature() {
// check if result exists in cache
//note: this cache implementation is extremely simplistic!
Object cachedValue = findInCache();
if (cachedValue!=null) {
this.convertedTemperature = (Double)cachedValue;
} else {
DCBindingContainer bindings = (DCBindingContainer)getExpressionValue("#{bindings}");
FacesCtrlActionBinding convertTemperatureOperation = (FacesCtrlActionBinding)bindings.findCtrlBinding("ConvertTemp");
convertTemperatureOperation.getParamsMap().put("Temperature", this.temperature);
convertTemperatureOperation.getParamsMap().put("FromUnit", this.fromUnit);
convertTemperatureOperation.getParamsMap().put("ToUnit", this.toUnit);
convertTemperatureOperation.invoke();
Double result = (Double)convertTemperatureOperation.getResult();
storeInCache(result);
// store result in the cache
this.convertedTemperature = result;
}
return null;
}
public Object getExpressionValue( String el) {
ValueBinding vb = FacesContext.getCurrentInstance().getApplication().createValueBinding(el);
return vb.getValue(FacesContext.getCurrentInstance());
}
public void setConvertedTemperature(Double convertedTemperature) {
this.convertedTemperature = convertedTemperature;
}
public Double getConvertedTemperature() {
return convertedTemperature;
}
private void storeInCache(Object result) {
Map functionResultCache = (Map)getExpressionValue("#{FunctionResultCache}");
functionResultCache.put(getCacheKey(), result);
}
}
The interesting bit is the convertTemperature method. This method will be invoked when the Convert button is pressed in the web page. It will then first try to find a result in the FunctionResultCache. If it finds the value, the value is returned. If it does not, it will retrieve the OperationBinding ConvertTemp from the BindingContainer, set the parameters based on the bean properties and invoke the OperationBinding. Finally it will store the result in the FunctionResultCache (call to storeInCache) and put the result on the bean property convertedTemperature.
b. Configure Managed Bean temperatureConverterProxyBean
<managed-bean>
<managed-bean-name>temperatureConverterProxyBean</managed-bean-name>
<managed-bean-class>temperatureconverter.TemperatureConverterProxyBean</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
c. Bind form elements to managed bean
All page items that before were directly bound to elements in the Page Definition, are now associated with the temperatureConverterProxyBean. For example:
<af:inputText value="#{temperatureConverterProxyBean.temperature}" ...
<af:inputText value="#{temperatureConverterProxyBean.fromUnit}" ...
<af:inputText value="#{temperatureConverterProxyBean.toUnit}" ...
<af:commandButton action="#{temperatureConverterProxyBean.convertTemperature}"
text="ConvertTemp"
disabled="#{!bindings.ConvertTemp.enabled}"/>
<af:inputText value="#{temperatureConverterProxyBean.convertedTemperature}" ...
d. Configure the Application Scope Result Cache
<managed-bean>
<managed-bean-name>FunctionResultCache</managed-bean-name>
<managed-bean-class>java.util.HashMap</managed-bean-class>
<managed-bean-scope>application</managed-bean-scope>
</managed-bean>
When we now run the page, it is simple to see what happens: the first conversion of a specific Fahrenheit temperature value is just as slow as it was before. However, when we now perform the conversion for a temperature we have converted before, it appears blazingly fast in the page, because in that case it can be taken from the cache.
So here we have demonstrated a simple implementation of a Service Result Cache. Simple because for example it does not have an expiration time set on cached values – now they will remain valid for as long as the application instance is running – which for the Temperature converter may be appropriate. Also simple because it is implemented very explicitly in the View – in consumer of the Service based Data Control. That is not an ideal place – we do not want to burden the View with such implementation details, especially because now we would have to implement cache support everywhere in any View project that calls the service.
3. Model based Service Result Cache
My first attempt at model based implementation was to implement an extension of the SOAPProvider class that is configured as provider in the Data Control that was created for the WebService. This sub class would have to be configured in the DataControls.jspx file, something like this:
<?xml version="1.0" encoding="UTF-8" ?>
<DataControlConfigs xmlns="http://xmlns.oracle.com/adfm/configuration"
version="10.1.3.41.57" Package="temperatureconverter"
id="DataControls">
<AdapterDataControl id="TemperatueConversionService"
FactoryClass="oracle.adf.model.adapter.DataControlFactoryImpl"
ImplDef="oracle.adfinternal.model.adapter.webservice.WSDefinition"
SupportsTransactions="false"
SupportsSortCollection="false" SupportsResetState="false"
SupportsRangesize="false" SupportsFindMode="false"
SupportsUpdates="false"
Definition="TemperatueConversionService"
BeanClass="TemperatueConversionService"
xmlns="http://xmlns.oracle.com/adfm/datacontrol">
<Source>
<!-- replaced oracle.adfinternal.model.adapter.webservice.provider.soap.SOAPProvider with temperatureconverter.CachedResultsSOAPProvider -->
<definition xmlns="http://xmlns.oracle.com/adfm/adapter/webservice"
name="TemperatueConversionService" version="1.0"
provider="temperatureconverter.CachedResultsSOAPProvider"
wsdl="http://www.webservicex.net/ConvertTemperature.asmx?wsdl">
<service name="ConvertTemperature"
namespace="http://www.webserviceX.NET/"
connection="TemperatueConversionService">
<port name="ConvertTemperatureSoap">
<operation name="ConvertTemp"/>
</port>
</service>
</definition>
</Source>
</AdapterDataControl>
</DataControlConfigs>
It was my intention to override the execute() method in this class, making it first check the cache before calling the super.execute() – and in the latter case storing the result in the cache before returning it to the invoker. However, with
this change and a bare bones implementation of the CachedResultsSOAPProvider:
public class CachedResultsSOAPProvider extends SOAPProvider{
public CachedResultsSOAPProvider() {
}
public Object execute(String operationName,
Map params) throws AdapterException {
return super.execute(operationName, params);
}
}
I ran into an exception that seems to be caused by a bug. The classloader is unable to load the class: SEVERE: Failed to load the provider: temperatureconverter.CachedResultsSOAPProvider.
It may be that the class should be in a specific jar-file (as is suggested by some comments in the Java sources). Indeed, it turns out that when I add the CachedResultsSOAPProvider class to the jar dc-adapters.jar in JDEV_HOME\BC4J\jlib, it does get picked up.
I now further flesh out this class:
public class CachedResultsSOAPProvider extends SOAPProvider {
public CachedResultsSOAPProvider() {
}
private String getCacheKey(String operation, Map parameters) {
StringBuffer cacheKey = new StringBuffer(operation);
for (Object o:parameters.entrySet()) {
cacheKey.append(";*;").append(o.toString());
}
return cacheKey.toString();
}
public Object execute(String operationName,
Map params) throws AdapterException {
Map cache =
(Map)ADFContext.getCurrent().getExpressionEvaluator().evaluate("#{FunctionResultCache}");
Object result = cache.get(getCacheKey(operationName, params));
if (result==null) { // not found in cache, we need to make the web service call after all
result = super.execute(operationName, params);
cache.put(getCacheKey(operationName, params), result);
}
return result;
}
}
When we now construct a page based on this Data Control, it is completely transparent to the page that the WebService is actually provided with a Service Result Cache that will prevent unnecessary, time consuming service calls being made.
Conclusion
It turns out to be fairly simple with ADF to implement the concept of a Service Result Cache for Data Controls based on deterministic WebServices. By adding a special subclass of SOAPProvider to the dc-adapters.jar file and changing the provider in the DataControl definition to CachedResultsSOAPProvider, we turn a WebService Data Control into a Service Result Cache enabled Data Control. Using this Data Control is not any different from using a not-ResultCache-enabled Data Control. It can be a useful concept in managing expensive service calls, also for example when the call is not necessarily deterministic, but we can live with data that is from within a certain time window ("data less than 2 hours old is fine").