MySQL & ADF Business Components - Enum types

Using ADF BC against MySQL is, I feel, a fairly under-explored area, and although there  are several articles which will help you with the basic setup, things start to fade out when you get into it.  I think that the key missing link is that of data type mapping so I'm intending to put together a series of articles that will explore some of these topics. 

Here, I'll start with a pretty fundamental one. If you explore the MySQL World database (or indeed the Sakila database) you'll come across enum types. (If you want to follow along here you can head over to the MySQL Other Documentation page to get the World database to play with.)

The World database contains a table called Country:

+-----------------+
| Tables_in_world |
+-----------------+
| City            |
| Country         |
| CountryLanguage |
+-----------------+

And if we describe that, here is the definition:

+----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+
| Field          | Type                                                                                  | Null | Key | Default | Extra |
+----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+
| Code           | char(3)                                                                               | NO   | PRI |         |       |
| Name           | char(52)                                                                              | NO   |     |         |       |
| Continent      | enum('Asia','Europe','North America','Africa','Oceania','Antarctica','South America') | NO   |     | Asia    |       |
| Region         | char(26)                                                                              | NO   |     |         |       |
| SurfaceArea    | float(10,2)                                                                           | NO   |     | 0.00    |       |
| IndepYear      | smallint(6)                                                                           | YES  |     | NULL    |       |
| Population     | int(11)                                                                               | NO   |     | 0       |       |
| LifeExpectancy | float(3,1)                                                                            | YES  |     | NULL    |       |
| GNP            | float(10,2)                                                                           | YES  |     | NULL    |       |
| GNPOld         | float(10,2)                                                                           | YES  |     | NULL    |       |
| LocalName      | char(45)                                                                              | NO   |     |         |       |
| GovernmentForm | char(45)                                                                              | NO   |     |         |       |
| HeadOfState    | char(60)                                                                              | YES  |     | NULL    |       |
| Capital        | int(11)                                                                               | YES  |     | NULL    |       |
| Code2          | char(2)                                                                               | NO   |     |         |       |
+----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+

For now, let's concentrate on the Continent field, defined as enum('Asia','Europe',...) . Functionally this is similar, I guess to a varchar2 column in Oracle, with an  check constraint using an "in" with a static list. 

So if we generate a BC Entity object from that table, what do we get for the Continent field, (this is using SQL92 mode and the Java type-map in 11.1.2.3 of JDeveloper):

  <Attribute
    Name="Continent"
    IsNotNull="true"
    ColumnName="Continent"
    SQLType="$none$"
    Domain="oracle.demo.mysql.model.eo.Enum"
    Type="oracle.demo.mysql.model.eo.common.Enum"
    ColumnType="ENUM"
    TableName="world.Country"/>

So you can see here that the Type seems to be something specific to my project, and indeed, the generation of the entity has not only generated the XML for the EO but also the XML and Java class for a new domain type called  oracle.demo.mysql.model.eo.common.Enum.  This generated class implements the oracle.jbo.domain.DomainInterface, and we'll have a look at it in a bit more detail in a second.

Does it Work Though?

So what if we stop right there and just run the ADF BC  tester, does the default EO/VO combo actually function? Well the answer to that is Yes (which is a relief!), however, if you change the value of the Continent field to an invalid value (i.e. not one of the values listed in the enum) then the default error message is a little sparse on the actual reason for the problem:

JBO-26041: Failed to post data to database during "Update": 
      SQL Statement "UPDATE world.Country Country SET Continent=? WHERE Code=?".

What's more, you don't see this error until the data is actually posted to the database and it would be nice to do this kind of validation up-front. A second, related problem is down to the way that enums are handled in MySQL.  If I update the field to a valid value, but use a different case from that declared in the enum() e.g. SOUTH America, then MySQL will nicely match this and convert it to "South America" in the database. However, that of course mutates the record as far as ADFBC is concerned and further updates will result in:

JBO-25014: Another user has changed the row with primary key oracle.jbo.Key[... ].

So how can we improve things?

So first of all, it's trivial to fix the locking problem (JBO-25014 error). For that we just need to update the properties of the attribute in the EO to refresh after insert and update:

Setting the refresh properties for Continents

Next, how can we improve both the timing and the error message provided by the validation? Well in this case we need to fill in some blanks in the generated domain class.  If you have a look at it you'll see that there is a validate() method stub generated into the class.

    /**
     * Implements domain validation logic and throws a JboException on error.
     */
    protected void validate() {
        //  ### Implement custom domain validation logic here. ###
    } 

Not surprisingly all we need to do there is implement some logic in that and throw a JboException with a more informative error message.  This will ensure that not only is the message better, but also validation will happen as soon as the field value is set in the EO, rather than being deferred and waiting for the database post.

Before you implement the method though I'd recommend that you do some refactoring to change both the name of the Domain and Java class to  something a little more specific than "Enum".  To do this, simple select the top level Enum node in the navigator and use Refactor | Rename  from the menu bar. This will rename the Java class, the XML file and of course fix up the Country EO itself correctly. I've renamed mine to CountryContinentsEnum.

Implement the Validation Logic  

The logic we need in this case is to inspect the mData value inside of the validate() method and compare it to a list of the valid Continents (as defined by the MySQL column definition). The twist here is to remember that MySQL will in fact be happy with any case combination for the Enum value, so we need to carry out a case insensitive compare.  In my example this consists of three steps:

1) Define the list of Valid Values

I actually use a HashMap here where the key of the map is the uppercase version of the Continent name and the value is the lower case value.  For the validation below we only need the uppercase version for the actual check, but it's nice to have the mixed case version for any exception messages that we raise.

This list is stored in a static class variable:

public static final Map<String, String> CONTINENT_ENUM = new HashMap<String, String>(7);

2) Populate the Valid Values Map

I populate the Map in a static block in the class.  These values can be shared across all instances.

static {
   CONTINENT_ENUM.put("ASIA", "Asia");
   CONTINENT_ENUM.put("EUROPE", "Europe");
   CONTINENT_ENUM.put("NORTH AMERICA", "North America");
   CONTINENT_ENUM.put("AFRICA", "Africa");
   CONTINENT_ENUM.put("OCEANIA", "Oceania");
   CONTINENT_ENUM.put("ANTARCTICA", "Antarctica");
   CONTINENT_ENUM.put("SOUTH AMERICA", "South America");
}

Hint: Why not map the To-Uppercase keyboard shortcut in Tools | Preferences, it's not mapped by default but the function is there in the IDE! That will make it easier to create the uppercase version of the value. 

3) Implement the validate() Method

Finally the actual validate method.  Most of this is the error handling as the actual validation check itself is a simple (and quick) containsKey() call on the map.

protected void validate() {
  // MySQL will convert the value to the expected case  
  if (!CONTINENT_ENUM.containsKey(mData.toUpperCase())) {

    //Construct a nice error to send to the client
    boolean firstLoop = true;
    StringBuilder errorMsg = new StringBuilder("Incorrect continent value suppied. Pick one from ");
    for (Map.Entry<String, String> entry : CONTINENT_ENUM.entrySet() ){
      if (!firstLoop){
        errorMsg.append(", ");
      }
      errorMsg.append(entry.getValue());
      firstLoop = false;
    }
    throw new JboException(errorMsg.toString());
  }
}

Now to test that, here's the result when we put an invalid value into the BC Tester:

Great, but look what happens when we have a Web UI bound to the same component and attempt to put an invalid value in:

Whoops! So here's a slight problem, when run through the binding layer we're not seeing that well crafted error message, instead there is some data conversion error. What's more if I change to a valid value such as "Asia" I get the same error, so it's not my validate() method that's barfing here.  (in fact if you remove the validate method all together then you'll still get the error).

What to Do?

What's happening here is that the binding layer itself is sensibly doing a check on datatype and is not seeing how to do the conversion (even though ADF BC itself will handle an incoming string). So we need to give JSF a little help and specify a Converter.  A basic converter in JSF is very simple, it just has to implement two methods getAsString() and getAsObject() which convert from the Object Type to a String for HTTP to use and visa-versa.

Here's the simple implementation in this case:

package oracle.demo.mysql.view;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import oracle.adf.share.logging.ADFLogger;
import oracle.demo.mysql.model.eo.common.CountryContinentsEnum;
import oracle.jbo.JboException;

/**
 * Generic converter for MySQL the Continent Enum type that is just an enummeration of Strings
 * This simply wraps the real conversion which will happen at the ADF BC layer
 */
public class ContinentEnumConverter implements Converter {
    
    private static ADFLogger _logger = ADFLogger.createADFLogger(ContinentEnumConverter.class);
    
    public ContinentEnumConverter() {
        super();
    }

    /**
     * Standard converter class method that converts the String form of the object sent with the HTTP request 
     * into the real object type that needs to be handed off to the model
     * @param facesContext
     * @param uIComponent
     * @param stringValue 
     * @return CountryContinentsEnum created from stringValue
     */
    @Override
    public Object getAsObject(FacesContext facesContext, UIComponent uIComponent, String stringValue) {
        CountryContinentsEnum continent = null;
        try {
            continent = new CountryContinentsEnum(stringValue);
    
        }
        catch (JboException jboex){
            //If the validate method is failed then this is the exception that we will get 
            reportConversionProblem(jboex.getMessage(),false);            
        }
        catch (Exception ex) {
            //Others: just in case
            reportConversionProblem("Error: Can't create instance of Continent " + ex,true);            
        }
        
        return continent;
    }
    /**
     * Standard converter method that converts from the Object type to a String form suitable for HTTP 
     * @param facesContext
     * @param uIComponent
     * @param objectValue
     * @return String value of objectValue
     */
    @Override
    public String getAsString(FacesContext facesContext, UIComponent uIComponent, Object objectValue) {
        return objectValue.toString();
    }
    
    
    /*
     * Just queues up the conversion problem and optionally logs it as well  
     */
    private void reportConversionProblem(String message, boolean logit){
        if (logit){
            _logger.severe("Error: " + message); 
        }
        FacesMessage fmsg = new FacesMessage(null,message);
        fmsg.setSeverity(FacesMessage.SEVERITY_ERROR);
        throw new ConverterException(fmsg);
    }
} 

Once you have a converter class it then has to be associated with the components that it will need to operate on in some way.  There are two options, firstly you can associate the converter with a specific class so that whenever JSF encounters a conversion using that class it will use the associated converter for that, or secondly you can create the converter with a specific ID and then associate that converter ID with individual components using the <f:converter converterId="..."> tag.

In this case I'm using the former method so that I won't have to make any changes to the page, I just need this entry in my faces-config.xml file:

<converter>
  <converter-for-class>
    oracle.demo.mysql.model.eo.common.CountryContinentsEnum
  </converter-for-class>
  <converter-class>
    oracle.demo.mysql.view.ContinentEnumConverter
  </converter-class>
</converter>

 

Now when we induce an error, here's the result:

An Easier Way?

In this example, I've taken a very strictly typed approach to MySQL Enums where we're able to take advantage of the ADF BC domain capabilities to handle the type conversion. However, the fact remains that enums in MySQL are always lists of Strings, so another valid approach to the problem is to simply change the generated mapping from the enum type to String and then add on a validation in the ADF BC layer to restrict the valid values that can be squirted into MySQL.  You'll have to handle the fact that the case does not have to match exactly as part of this validation though.

My preference though would be to stick to the explicit type conversion method I'm using here, but pair that with a list of values definition on the attribute so that the UI always presents the enumeration as a list in any case.

Comments:

Post a Comment:
Comments are closed for this entry.
About

Hawaii, Yes! Duncan has been around Oracle technology way too long but occasionally has interesting things to say. He works in the Development Tools Division at Oracle, but you guessed that right? In his spare time he contributes to the Hudson CI Server Project at Eclipse
Follow DuncanMills on Twitter

Note that comments on this blog are moderated so (1) There may be a delay before it gets published (2) I reserve the right to ignore silly questions and comment spam is not tolerated - it gets deleted so don't even bother, we all have better things to do with our lives.
However, don't be put off, I want to hear what you have to say!

Search

Archives
« April 2014
MonTueWedThuFriSatSun
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    
       
Today