Chapter 5. Behavioral Analysis

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 tutorial.jdo

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

    jdoc tutorial.jdo (or jdoc Snake.class)

  4. Refresh the database

    As we have created a new persistence-capable class, we must change the database schema to match. So run schematool -action refresh tutorial.jdo (or schematool -action refresh Snake.class) to propagate the changes to the database.

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. Run java tutorial.AnimalMaintenance list Snake to see the new snakes in the database.

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)
      {
        PersistenceManager pm = JDOFactory.getPersistenceManager ();
    
        // 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");
    
          Transaction transaction = pm.currentTransaction ();
          transaction.begin ();
    
          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.");
            }
    
            // 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);
          }
    
          transaction.commit ();
        }
        else
        {
          System.out.println ("no snakes matching '" + filter
            + "' found in persistence manager");
        }
        pm.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 tutorial.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 tutorial.jdo file.

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

    Note that we specified an inverse 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 many-to-many 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 the metadata section of the Kodo JDO Reference Guide.

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

    javac Snake.java Rabbit.java

    jdoc tutorial.jdo (or jdoc Snake.class Rabbit.class)

  5. Refresh the database

    schematool -action refresh tutorial.jdo

Now, experiment with the following classes: java tutorial.Snake eat and java tutorial.AnimalMaintenance details Snake.

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:

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 field 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:

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", which prints details about all snakes that have eaten male rabbits.