As Published In
Oracle Magazine
March/April 2007

TECHNOLOGY: Berkeley DB


Embedded Java Persistence

By Jonathan Gennick

Oracle Berkeley DB Java Edition provides painless persistence to Java objects.

Oracle Berkeley DB Java Edition is a 100% Pure Java database you can embed within a Java application. It draws on the venerable heritage of Berkeley DB, implementing the same successful underlying principles and concepts in a way that takes full advantage of the Java platform.

This article introduces Oracle Berkeley DB Java Edition and shows how to build a prototype internet shopping cart application by using Oracle Berkeley DB Java Edition's Direct Persistence Layer (DPL) to provide a painless and elegant mechanism for storing and retrieving Java objects.

Introducing Oracle Berkeley DB Java Edition

Oracle Berkeley DB Java Edition is not a relational database. Instead, it's a database that stores values in a B-tree structure based on keys you provide. B-tree structures are very efficient, well-proven storage-and-retrieval structures.

The B-tree approach comes from Berkeley DB, a C-based database that, since its debut in the early 1990s, has become one of the most widely used databases in the world. Berkeley DB is included with every version of Linux and UNIX and is used in a wide range of applications, from cell phones and routers to major retail and search Web sites.

The B-tree key/value approach is not limited to simple scalar values such as integers and strings. A value can be any arbitrary string of bytes, such as a string of bytes representing a Java class. Likewise, a key can be any string of bytes that serves to uniquely identify objects of that class.

Oracle Berkeley DB Java Edition is designed to be directly embedded within your Java application, and it runs in the same Java virtual machine (JVM). There is no overhead from context switches; no remote database server to communicate with; and, because there is no textual query language (for example, no SQL), no overhead from parsing queries.

Direct Persistence Layer

Converting the Java objects to some sort of binary representation is key to using Oracle Berkeley DB Java Edition to store Java objects. And when you retrieve those objects, you must instantiate the objects based on the byte strings you have retrieved. Enter the DPL, which handles all that detail work for you. You don't need to worry about any object-relational mapping, because Oracle Berkeley DB Java Edition stores an object as a unit, not spread across columns and tables.

Using the DPL is as simple as specifying a few annotations when you define a class of objects you want to store in an Oracle Berkeley DB Java Edition database. (Annotations are a new feature in Java 1.5.) If you prefer not to use annotations, however, you can still use the DPL. To do that, you create an EntityModel subclass that provides the metadata that would otherwise be provided by the annotations. Details of this approach are not included in this article. Refer to the resources in the Next Steps box for information on using this approach.

Entity Classes

An internet shopping cart is one application to which you might apply the DPL. The shopping cart would be implemented on the application server, with Oracle Berkeley DB Java Edition being used as an embedded database. The DPL would handle all the details of marshaling your application classes to and from the data store, leaving you free to focus on the functionality you need to provide your customers.

Code Listing 1: Creating an entity class 

@Entity public class Product {

   @PrimaryKey(sequence="ID") private long id;

   @SecondaryKey(relate=ONE_TO_ONE) private String name;

    public Product(String name) {
       this.name = name;
    }
   
    /** A default constructor is needed by the DPL for deserialization. */
    private Product() {
    }
.
.
.    
}

To begin building the shopping cart application, start with the entity class . In DPL terminology, entity class refers to a class you want to store directly in an Oracle Berkeley DB Java Edition database. Listing 1 creates an entity class named Product as part of the shopping cart solution. There are four key points to note in the listing. The first is the use of the @Entity annotation in front of the class definition: 

@Entity public class Product {

The @Entity annotation becomes a property that is attached to the class by the Java compiler. DPL methods then query for such properties, using them to guide the behind-the-scenes database operations the DPL performs for you.

Next, note the specification of a primary key: 

@PrimaryKey(sequence="ID")
private long id; 

You use the @PrimaryKey annotation to identify the member variable that is to be the key used to place an instance of the class into Oracle Berkeley DB Java Edition's B-tree structure. In this case, the key is to be automatically generated by a sequence named ID. When you store a new Product object, the DPL will fill in the ID variable's value for you; you do not need to supply it beforehand. The DPL will also create the ID sequence for you the first time a Product object is stored. You do not need to create the sequence yourself.

Optionally, you can create secondary keys. The Product class defines a secondary key on the product name: 

@SecondaryKey(relate=ONE_TO_ONE)
private String name;

When you specify a secondary key, you must think loosely in terms of specifying what in a relational database would be a foreign key relationship. Oracle Berkeley DB Java Edition creates a second B-tree in support of the additional key, and the second B-tree will support duplicate entries, or not, depending on the type of relationship you specify. The way you read the ONE_TO_ONE relationship type is as follows: "There is exactly ONE product for ONE product name."

The last thing to note about Listing 1 is the default constructor: 

private Product() { }

The DPL requires a default constructor. When you retrieve an object, the DPL gets the object's byte stream from the database, deserializes that byte stream, and instantiates the object for you by invoking the default constructor.

The other entity classes, User and ShoppingCart, are defined in the same manner as Product. One additional specification you'll see in the ShoppingCart class is the specification of a secondary key to define a relationship between two entity classes: 

@Entity
public class ShoppingCart {

  @PrimaryKey(sequence="ID")
  private long id;

  @SecondaryKey(
             relate=MANY_TO_ONE,
             relatedEntity=User.class,
             onRelatedEntityDelete=CASCADE)
  private String userName;
.
.
.

Here you see the @SecondaryKey annotation applied to the userName variable. There are three parameters to this annotation. The relatedEntity parameter specifies the target entity. Thus, you can read MANY_TO_ONE as "Many ShoppingCarts for ONE user." Why would a user have more than one cart? One reason might be to give each user a wish-list cart in addition to the cart that holds immediate purchases.

The last parameter of the @SecondaryKey annotation specifies an "on delete" rule. CASCADE is used here to specify that the DPL should delete all of a user's shopping carts if you delete the user. Space prohibits fully showing all of the class definitions in this article. Please take the time to download the application from the URL in the Next Steps box and read the complete code.

Persistent Classes

Sometimes you have a class in which objects of that class contain, in turn, objects of other classes. In the DPL, a persistent class is a class of objects you embed within entity objects. If the highest-level class is an entity class, then the classes all the way down need to be annotated as persistent classes. In addition, subclasses and superclasses of an entity class must also be annotated with @Persistent. Although this is not shown in this article, the DPL supports subclassing and polymorphism that works as expected in Java.

The shopping cart application has one persistent class. Its definition is shown in Listing 2. (I've elided the getter and setter methods to save space.) You need to do only two things to make a class persistent: apply the @Persistent annotation, and define a default constructor. It's that simple.

Code Listing 2: Creating a persistent class 

@Persistent public class LineItem {

    private long productId;
    private int quantity;

    public LineItem(long productId, int quantity) {
          this.productId = productId;
          this.quantity = quantity;
    }

    /** A default constructor is needed by the DPL for deserialization. */
    private LineItem() {
    }
.
.
.
}

Database Environment

With entity and persistent classes defined, the next steps are to create a database environment and then to create and open an entity store . A database environment is an abstraction that encapsulates and specifies default properties for a collection of entity stores. An entity store is what you interact with to store and retrieve objects in your application.

Code Listing 3: Opening a database environment and an entity store 

/* Open a transactional Oracle Berkeley DB Environment. */
EnvironmentConfig envConfig = new EnvironmentConfig();
envConfig.setAllowCreate(true);
envConfig.setTransactional(true);
env = new Environment(dataDirectory, envConfig);

/* Open a transactional EntityStore. */
StoreConfig storeConfig = new StoreConfig();
storeConfig.setAllowCreate(true);
storeConfig.setTransactional(true);
store = new EntityStore(env, "MyStore", storeConfig);

The code in Listing 3 uses the following steps to open a database environment, creating a new one if necessary:

1. Instantiates an EnvironmentConfig object
2. Sets a flag so that opening the environment for the first time causes all the needed underlying files to be created
3. Enables transactions for the environment
4. Passes the EnvironmentConfig object to the Environment constructor, thus opening—and creating, if necessary—the environment

At the physical level, an environment correlates with a specific directory on disk. When you run the example code on your own system, be sure that the first parameter to the Environment constructor is a File object representing a valid file system path for a directory that already exists. The DPL will not create the environment directory for you. (However, the full example for this article uses other Java functionality to check that the directory exists, creating it if necessary.)

After opening (and possibly creating) the environment, the code in Listing 3 repeats the same basic process to open and configure an entity store. You can give an entity store any arbitrary name, and that name is embedded within the names of the databases created in that entity store. You can actually create multiple independent data sets based on the same entity schema, simply by specifying different entity store names. If you were running two online shopping applications, you could create separate entity stores for each.

Storing Objects

With the environment and entity store open, let's store some objects. Remember, Oracle Berkeley DB Java Edition is a B-tree database. When you store an object, you store it directly into a primary-key B-tree. To that end, begin your application by instantiating an object corresponding to each primary-key B-tree you plan to use. For example, the following code instantiates primary-key indexes for the User and Product classes: 

private PrimaryIndex<String, User> 
userByName;
private PrimaryIndex<Long, Product> 
productById;

userByName = 
store.getPrimaryIndex
(String.class, User.class);
productById = 
store.getPrimaryIndex
(Long.class, Product.class);

You can now store objects of classes User and Product through the userByName and productById index objects, respectively. And you can now (optionally) begin a transaction: 

Transaction txn = 
  env.beginTransaction(null, null);

and create and store a new user: 

User user = new User("Mr. Shopper");
userByName.put(txn, user);

and create and store a product: 

Product camera = 
new Product("Ultrazoom Camera");
productById.put(txn, camera);

Finally, you can commit your transaction: 

txn.commit();

Use of transactions is optional. You can use them or not, as your application requires. When you don't explicitly open a transaction, Oracle Berkeley DB Java Edition treats each store, retrieval, and delete operation as its own transaction. When you do explicitly begin a transaction, the second argument allows you to specify options (such as the isolation level) for the transaction you are creating.

Retrieving, Updating, and Deleting

Retrieving an object by its primary key is a trivial operation to code. For example, the following code retrieves the user named "Mr. Shopper": 

User user = userByName.get 
                (txn, "Mr. Shopper", null);

Changing the object and storing the revised version is also easy. Just change the object in the usual way, and store it the same way you did the first time: 

user.setPassword("secret");
userByName.put(txn, user);

Deleting an object by its primary key requires nothing more than a call to a delete method: 

userByName.delete(txn, "Mr. Shopper");

In the example for this article, the act of deleting a user would cascade to delete all shopping carts for that user. That's because the onRelatedEntityDelete=CASCADE rule is used in the secondary- key definition linking shopping carts to users.

Retrieving by Secondary Key

You won't always know the primary key for objects you need to retrieve. Product objects are a good example. It's far more likely that you'll be looking up products by name than by their automatically assigned (and, hopefully, unseen by users) ID numbers. Fortunately, the Product class definition specifies a secondary index on name.

To access a secondary index for a class, you must first get the primary index of the class. Here is the sequence to follow:

1. Get the primary index.
2. Use the primary index to retrieve the secondary index.
3. Use the secondary index to get objects via the secondary key.

Listing 4 illustrates these steps, by retrieving the Product object for the product named "Old Cassette Tape."

Code Listing 4: Retrieving by secondary key 

public PrimaryIndex<Long, Product> productById;
public SecondaryIndex<String, Long, Product> productByName;

productById = store.getPrimaryIndex(Long.class, Product.class);
productByName = store.getSecondaryIndex(productById, String.class, "name");
Product product = productByName.get("Old Cassette Tape");

Cursor-Based Retrieval

Oracle Berkeley DB Java Edition also supports cursor-based retrieval. Using a cursor enables you to cycle through a collection of many objects. For example, you might want to print a list of all products in the database. To order that list by product ID, you would open a cursor on the primary-key index: 

EntityCursor<Product> products = 
  productById.entities();

To order the list by product name, you would open the cursor on the secondary index on the name field: 

EntityCursor<Product> products = 
  productByName.entities();
 

Next Steps



READ more about
OracleBerkeley DB Java Edition
Direct Persistence Layer
Getting Started with Oracle Berkeley DB Java Edition
Getting Started with the Direct Persistence Layer


DOWNLOAD

Oracle Berkeley DB Java Edition
sample code for this article

DISCUSS
Oracle Berkeley DB Java Edition

You can even specify a range of index entries, such as the range of names beginning with O and going up to, but not including, P :

 
EntityCursor<Product> products = productByName.entities( "O",true, "P",false);

The true and false arguments to this entities method indicate whether the corresponding arguments are meant to be inclusive. The resulting cursor will return any product named O, should such a product exist. However, the resulting cursor will not return any product named P.

You can now easily cycle through the products returned by the cursor, by using a simple FOR loop: 

try {
    for (Product product : products) {
         System.out.println(product);
    }
} finally {
    products.close();
}

The use of a FINALLY block to ensure that the cursor gets closed is a best practice that prevents any potential memory leaks. It's also important to ensure that entity stores get closed when you're done using them and that transactions ultimately either abort or commit. Download the full example code for this article, and you'll see the TRY...FINALLY routine often used to ensure that resources are properly closed and released.

Benefits of the Direct Persistence Layer

The Oracle Berkeley DB Java Edition DPL is an elegant and painless, yet powerful, solution for making Java objects persistent. Consider Oracle Berkeley DB Java Edition and the DPL when 

  • You have a set of Java objects to store and retrieve

  • You need an embedded solution that can run as part of your application

  • Only one application requires write access

  • Your database operations are well-defined

  • Low latency and high availability are important

If you're a Java developer interested in Oracle Berkeley DB Java Edition and the DPL, download the full example program for this article from the URL given in Next Steps. Run the code. Download and read the "Getting Started" guides. Try out the DPL in your own projects. I believe you'll like what you see.

Thanks to Gregory Burd and Mark Hayes for reviewing this article. Special thanks to Mark Hayes for writing all of the code examples.

 


Jonathan Gennick (www.gennick.com) is an experienced Oracle professional and member of the Oak Table Network. He wrote the best-selling SQL Pocket Guide and the Oracle SQL*Plus Pocket Reference, both from O'Reilly Media.


Send us your comments