2.5. Behavioral Analysis

2.5.1. Complex Queries

Often, pet stores sell snakes as well as rabbits and dogs. Pet stores are primarily concerned with a snake's length; much like rabbits, pet store operators usually sell them all for a flat rate.

This chapter demonstrates more queries, schema manipulation, one-to-many relations, and many-to-many relations.

Provided with this tutorial is a file called Snake.java which contains a sample Snake implementation. Let's get it compiled and loaded:

  1. Examine and compile Snake.java.

    javac Snake.java
    
  2. Add tutorial.Snake to package.jdo.

    <class name="Snake" persistence-capable-superclass="Animal"/>
    
  3. Enhance the class.

    jdoc Snake.java
    
  4. Refresh the mappings and database.

    As we have created a new persistence-capable class, we must map it to the database and change the schema to match. So run the mapping tool:

    mappingtool -action refresh Snake.java
    

Once you have compiled everything, add a few snakes to the database using:

java tutorial.AnimalMaintenance add Snake "name" <length>

Where <length> is the length in feet for the new snake. To see the new snakes in the database, run:

java tutorial.AnimalMaintenance list Snake

Unfortunately for the massively developing rabbit population, snakes often eat rabbits. Any good inventory system should be able to capture this behavior. So, let's add some code to Snake.java to support the snake's eating behavior.

First, let's modify Snake.java to contain a list of eaten rabbits.

  1. Add the following code snippet to Snake.java .

    This is the 'many' side of a one-to-many relation.

        // *** Add this member variable declaration. ***
        // This list will be persisted into the database as 
        // a one-to-many relation.
        private Set giTract = new HashSet ();
    
        ...
    
        // *** Modify toString (boolean) to output the giTract list. ***
        public String toString (boolean detailed)
        {
            StringBuffer buf = new StringBuffer (1024);
            buf.append ("Snake ").append (getName ());
    
            if (detailed)
            {
                buf.append (" (").append (length).append (" feet long) sells for ");
                buf.append (getPrice ()).append (" dollars.");
                buf.append ("  Its gastrointestinal tract contains:\n");
                for (Iterator iter = giTract.iterator (); iter.hasNext ();)
                    buf.append ("\t").append (iter.next ()).append ("\n");
            }
            else
                buf.append ("; ate " + giTract.size () + " rabbits.");
    
            return buf.toString ();
        }
    
        ...
    
        // *** Add these methods. ***
        /**
         *  Kills the specified rabbit and eats it.
         */
        public void eat (Rabbit dinner)
        {
            // Consume the rabbit.
            dinner.kill ();
            dinner.eater = this;
            giTract.add (dinner);
            System.out.println ("Snake " + getName () + " ate rabbit " 
                + dinner.getName () + ".");
        }
    
    
        /**
         *  Locates the specified snake and tells it to eat a rabbit.
         */
        public static void eat (String filter)
        {
            PersistenceManagerFactory pmf =
                KodoHelper.getPersistenceManagerFactory ("kodo.properties");
            PersistenceManager pm = pmf.getPersistenceManager ();
            Transaction transaction = pm.currentTransaction ();
            transaction.begin ();
    
            // Find the desired snake(s) in the data store.
            Query query = pm.newQuery (Snake.class, filter);
            Collection results = (Collection) query.execute ();
          
            if (results.size () > 0)
            {
                Iterator iter = results.iterator ();
                Query uneatenQuery = pm.newQuery (Rabbit.class, "isDead == false");
    
                while (iter.hasNext ())
                {
                    // Find a rabbit to eat.
                    Random random = new Random ();
    
                    // Run a query for a rabbit whose 'isDead' field indicates
                    // that it is alive.
                    List menu = new ArrayList ();
                    menu.addAll ((Collection) uneatenQuery.execute ());
                    if (menu.size () == 0)
                    {
                        System.out.println ("No live rabbits in DB.");
                        break;
                    }
                    else
                    {
                        // Select a random rabbit from the list.
                        Rabbit dinner = (Rabbit) menu.get (random.nextInt 
                            (menu.size ()));
        
                        // Perform the eating.
                        Snake snake = (Snake) iter.next ();
                        System.out.println (snake + " is eating:");
                        snake.eat (dinner);
                    }
                }
            }
            else
                System.out.println ("No snakes matching '" + filter
                    + "' found in persistence manager");
    
            transaction.commit ();
            pm.close ();
            pmf.close ();
        }
    
    
        public static void main (String [] args)
        {
            if (args.length == 2 && args[0].equals ("eat"))
            {
                eat (args[1]);
                return;
            }
    
            // If we get here, something went wrong.
            System.out.println ("Usage:");
            System.out.println ("  java tutorial.Snake eat \"snakequery\"");
        }
    
  2. Add an eater field to Rabbit.java.

    This is the 'one' side of a one-to-many relation. Notice that we are making this field protected. This demonstrates that it is possible to enhance any field; you need not provide getters and setters as we have done in previous examples. This is not recommended practice, because it means that all classes that access this field directly must be JDO enhanced, even if they are not persistence-capable.

    Add the following member variable to Rabbit.java :

        protected Snake eater;
    
  3. Add metadata to package.jdo.

    Notice the giTract declaration: it is a simple Java list declaration. As with the many-to-many declarations in Rabbit.java, we will add metadata to the package.jdo file.

    <class name="Snake" persistence-capable-superclass="Animal" >
        <field name="giTract">
            <collection element-type="Rabbit"/>
            <extension vendor-name="kodo" key="inverse-owner" value="eater"/>
        </field>
    </class>
    

    Note that we specified an inverse-owner attribute in this example. This is because the relation is a two-sided one; that is, the rabbit has knowledge of which snake ate it. We could have left out the eater field and instead created a one-sided relation. The metadata might have looked like this:

    <class name="Snake" persistence-capable-superclass="Animal" >
        <field name="giTract">
            <collection element-type="Rabbit"/>
        </field>
    </class>
    

    For more information on types of relations, see Chapter 7, Object-Relational Mapping of the Kodo JDO Reference Guide.

  4. Compile Snake.java and Rabbit.java and jdo-enhance the classes.

    javac Snake.java Rabbit.java
    jdoc Snake.java Rabbit.java
    
  5. Refresh the mappings and database.

    mappingtool -action refresh Snake.java Rabbit.java
    

Now, experiment with the following commands:

java tutorial.Snake eat
java tutorial.AnimalMaintenance details Snake ""

2.5.1. Complex Queries

Imagine that one of the snakes in the database was named Killer. To find out which rabbits Killer ate, we could run either of the following two queries:

java tutorial.AnimalMaintenance details Snake "name == \"Killer\""
java tutorial.AnimalMaintenance list Rabbit "eater.name == \"Killer\""

The first query is Snake-centric -- the query runs against the Snake class, looking for all snakes named Killer and providing a detailed listing of them. The second is Rabbit-centric -- it examines the rabbits in the database for instances whose eater is named Killer. This second query demonstrates the that simple java 'dot' syntax is used when traversing an Object field in a query.

It is also possible to traverse Collection fields. Imagine that there was a rabbit called Roger in the data store and that one of the snakes ate it. In order to determine who ate Roger Rabbit, you could run a query like this:

java tutorial.AnimalMaintenance details Snake "giTract.contains (animal) && animal.name == \"Roger\""

Note the use of the animal variable that was registered with the query. To add support for this variable, uncomment the commented-out declareVariables call in the getAnimals method in AnimalMaintenance.java:

  1. AnimalMaintenance.getAnimals should look like this:

        /**
         *  Return a collection of animals that match the specified query filter.
         *
         *  @param  filter      the JDO filter to apply to the query
         *  @param  cls         the class of animal to query on
         *  @param  pm          the PersistenceManager to obtain the query from
         */
        public static Collection getAnimals (String filter, Class cls, 
            PersistenceManager pm)
        {
            // Get a query for the specified class and filter.
            Query query = pm.newQuery (cls, filter);
    
            // Add a single variable of type 'Animal' to this query to allow
            // for some reasonably powerful queries. This will be uncommented
            // in Chapter V.
            query.declareVariables ("Animal animal;");
    
            // Execute the query.
            return (Collection) query.execute ();
        }
    
  2. Recompile AnimalMaintenance.java.

    javac AnimalMaintenance.java
    

The animal variable is now available to use to represent any Animal contained in a collection. As animal was declared as type Animal, we cannot directly use fields in our Animal subclasses. However, the JDO specification provides support for casting in queries, which lets us run queries like:

java tutorial.AnimalMaintenance details Snake "giTract.contains (animal) && ((Rabbit) animal).isFemale == false"

This prints details about all snakes that have eaten male rabbits.