Chapter 3. Getting Going with the Direct Persistence Layer

Table of Contents

Persistent Objects
Entity versus Persistent
Vendor.class
Inventory.class
Entity Stores
MyDbEnv
Accessing Indices
Accessing Primary Indices
Accessing Secondary Indices
DataAccessor.class

In this chapter we will build a couple of basic applications that use the DPL to store and retrieve objects. To do this, we will create two applications that use fictional inventory and vendor information. The first application, ExampleDatabasePut, is used to create inventory and vendor objects that are stored for later retrieval. The second application, ExampleInventoryRead, is used to retrieve and display this data.

Note

The examples that we use here are identical to the examples provided in the Getting Started with Berkeley DB Java Edition guide. The only difference is that the DPL is used instead of the JE API. We did this to make it easier to compare the two APIs.

Before we begin building our main applications, which are used to perform data reads and writes, we have to build several other classes that provide important infrastructure for our application. These classes encapsulate the data we want to store, provide data access to the data store, and open and close our data store.

Persistent Objects

To begin, we build the classes that we actually want to store. In our case, we build two such classes; a class that contains product inventory information and a class that contains vendor contact information.

These classes look pretty much the same as any class might that encapsulates data. They have private data members that hold the information and they have getter and setter methods for data access and retrieval.

However, to use these with the DPL, the classes must be decorated with Java annotations that identify the classes as either an entity class or a persistent class. Java annotations are also used to identify primary and secondary indices.

Entity versus Persistent

The DPL is used to store Java objects in an underlying series of databases (accessed via an EntityStore). To do this, you must identify classes to be stored as either entity classes or persistent classes.

Entity classes are classes that have a primary index, and optionally one or more secondary indices. That is, these are the classes that you will save and retrieve directly using the DPL. You identify an entity class using the @Entity java annotation.

Persistent classes are classes used by entity classes. They do not have primary or secondary indices used for object retrieval. Rather, they are stored or retrieved when an entity class makes direct use of them. You identify an persistent class using the @Persistent java annotation.

Note that all non-transient instance fields of a persistent class, as well as its superclasses and subclasses, are persistent. static and transient fields are not persistent. The persistent fields of a class may be private, package-private (default access), protected or public.

Note that simple Java types, such as java.lang.String and java.util.Date, are automatically handled as a persistent class when you use them in an entity class; you do not have to do anything special to cause these simple Java objects to be stored in the EntityStore.

Vendor.class

The simplest class that our example wants to store contains vendor contact information. This class contains no secondary indices so all we have to do is identify it as an entity class and identify the field in the class used for the primary key.

Primary and secondary indices are described in detail in the Getting Started with Berkeley DB Java Edition guide, but essentially a primary index is the main information you use to organize and retrieve a given object. Primary index keys are always unique to the object in order to make it easier to locate them in the data store.

Conversely, secondary indices represent other information that you might use to locate an object. We discuss these more in the next section.

In the following example, we identify the vendor data member as containing the primary key. This data member is meant to contain a vendor's name. Because of the way we will use our EntityStore, the value provided for this data member must be unique within the store or runtime errors will result.

When used with the DPL, our Vendor class appears as follows. Notice that the @Entity annotation appears immediately before the class declaration, and the @PrimaryKey annotation appears immediately before the vendor data member declaration.

package persist.gettingStarted;

import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.PrimaryKey;

@Entity
public class Vendor {

    private String address;
    private String bizPhoneNumber;
    private String city;
    private String repName;
    private String repPhoneNumber;
    private String state;

    // Primary key is the vendor's name
    // This assumes that the vendor's name is
    // unique in the database.
    @PrimaryKey
    private String vendor;

    private String zipcode;

    public void setRepName(String data) {
        repName = data;
    }

    public void setAddress(String data) {
        address = data;
    }

    public void setCity(String data) {
        city = data;
    }

    public void setState(String data) {
        state = data;
    }

    public void setZipcode(String data) {
        zipcode = data;
    }

    public void setBusinessPhoneNumber(String data) {
        bizPhoneNumber = data;
    }

    public void setRepPhoneNumber(String data) {
        repPhoneNumber = data;
    }

    public void setVendorName(String data) {
        vendor = data;
    }

    public String getRepName() {
        return repName;
    }

    public String getAddress() {
        return address;
    }

    public String getCity() {
        return city;
    }

    public String getState() {
        return state;
    }

    public String getZipcode() {
        return zipcode;
    }

    public String getBusinessPhoneNumber() {
        return bizPhoneNumber;
    }

    public String getRepPhoneNumber() {
        return repPhoneNumber;
    }
} 

For this class, the vendor value is set for an individual Vendor class object by the setVendorName() method. If our example code fails to set this value before storing the object, the data member used to store the primary key is set to a null value. This would result in a runtime error.

You can avoid the need to explicitly set a value for a class' primary index by specifying a sequence to be used for the primary key. This results in an unique integer value being used as the primary key for each stored object.

You declare a sequence is to be used by specifying the sequence keyword to the @PrimaryKey annotation. For example:

@PrimaryKey(sequence="")
long myPrimaryKey; 

If you provide the sequence keyword with a name, then the sequence is obtained from that named sequence. For example:

@PrimaryKey(sequence="Sequence_Namespace")
long myPrimaryKey; 

Inventory.class

Our example's Inventory class is much like our Vendor class in that it is simply used to encapsulate data. However, in this case we want to be able to access objects two different ways: by product SKU and by product name.

In our data set, the product SKU is required to be unique, so we use that as the primary key. The product name, however, is not a unique value so we set this up as a secondary key.

The class appears as follows in our example:

package persist.gettingStarted;

import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.PrimaryKey;
import static com.sleepycat.persist.model.Relationship.*;
import com.sleepycat.persist.model.SecondaryKey;

@Entity
public class Inventory {

    // Primary key is sku
    @PrimaryKey
    private String sku;

    // Secondary key is the itemName
    @SecondaryKey(relate=MANY_TO_ONE)
    private String itemName;

    private String category;
    private String vendor;
    private int vendorInventory;
    private float vendorPrice;

    public void setSku(String data) {
        sku = data;
    }

    public void setItemName(String data) {
        itemName = data;
    }

    public void setCategory(String data) {
        category = data;
    }

    public void setVendorInventory(int data) {
        vendorInventory = data;
    }

    public void setVendor(String data) {
        vendor = data;
    }

    public void setVendorPrice(float data) {
        vendorPrice = data;
    }

    public String getSku() {
        return sku;
    }

    public String getItemName() {
        return itemName;
    }

    public String getCategory() {
        return category;
    }

    public int getVendorInventory() {
        return vendorInventory;
    }

    public String getVendor() {
        return vendor;
    }

    public float getVendorPrice() {
        return vendorPrice;
    }
} 

Secondary Indices

To declare a secondary index, we use the @SecondaryKey annotation. Note that when we do this, we must declare what sort of an index it is; that is, what is its relationship to other data in the data store.

The kind of indices that we can declare are:

  • ONE_TO_ONE

    This relationship indicates that the secondary key is unique to the object. If an object is stored with a secondary key that already exists in the data store, a run time error is raised.

    For example, a person object might be stored with a primary key of a social security number (in the US), with a secondary key of the person's employee number. Both values are expected to be unique in the data store.

  • MANY_TO_ONE

    Indicates that the secondary key may be used for multiple objects in the data store. That is, the key appears more than once, but for each stored object it can be used only once.

    Consider a data store that relates managers to employees. A given manager will have multiple employees, but each employee is assumed to have just one manager. In this case, the manager's employee number might be a secondary key, so that you can quickly locate all the objects related to that manager's employees.

  • ONE_TO_MANY

    Indicates that the secondary key might be used more than once for a given object. Index keys themselves are assumed to be unique, but multiple instances of the index can be used per object.

    For example, employees might have multiple unique email addresses. In this case, any given object can be access by one or more email addresses. Each such address is unique in the data store, but each such address will relate to a single employee object.

  • MANY_TO_MANY

    There can be multiple keys for any given object, and for any given key there can be many related objects.

    For example, suppose your organization has a shared resource, such as printers. You might want to track which printers a given employee can use (there might be more than one). You might also want to track which employees can use a specific printer. This represents a many-to-many relationship.

Note that for ONE_TO_ONE and MANY_TO_ONE relationships, you need a simple data member (not an array or collection) to hold the key. For ONE_TO_MANY and MANY_TO_MANY relationships, you need an array or collection to hold the keys:

@SecondaryKey(relate=ONE_TO_ONE)
private String primaryEmailAddress = new String();

@SecondaryKey(relate=ONE_TO_MANY)
private Set<String> emailAddresses = new HashSet<String>(); 

Foreign Key Constraints

Sometimes a secondary index is related in some way to another entity class that is also contained in the data store. That is, the secondary key might be the primary key for another entity class. If this is the case, you can declare the foreign key constraint to make data integrity easier to accomplish.

For example, you might have one class that is used to represent employees. You might have another that is used to represent corporate divisions. When you add or modify an employee record, you might want to ensure that the division to which the employee belongs is known to the data store. You do this by specifying a foreign key constraint.

When a foreign key constraint is declared:

  • When a new secondary key for the object is stored, it is checked to make sure it exists as a primary key for the related entity object. If it does not, a runtime error occurs.

  • When a related entity is deleted (that is, a corporate division is removed from the data store), some action is automatically taken for the entities that refer to this object (that is, the employee objects). Exactly what that action is, is definable by you. See below.

When a related entity is deleted from the data store, one of the following actions are taken:

  • ABORT

    The delete operation is not allowed. A runtime error is raised as a result of the operation. This is the default behavior.

  • CASCADE

    All entities related to this one are deleted as well. For example, if you deleted a Division object, then all Employee objects that belonged to the division are also deleted.

  • NULLIFY

    All entities related to the deleted entity are updated so that the pertinent data member is nullified. That is, if you deleted a division, then all employee objects related to that division would have their division key automatically set to null.

You declare a foreign key constraint by using the relatedEntity keyword. You declare the foreign key constraint deletion policy using the onRelatedEntityDelete keyword. For example, the following declares a foreign key constraint to Division class objects, and it causes related objects to be deleted if the Division class is deleted:

@SecondaryKey(relate=ONE_TO_ONE, relatedEntity=Division.class, 
    onRelatedEntityDelete=CASCADE)
private String division = new String();