Bean-Managed Persistence


Bean-Managed Persistence

In contrast to the entity beans with container-managed persistence, entity beans with bean-managed persistence themselves take care of communication with the persistence medium (e.g., a database). All access to reading and writing of data is programmed in the bean class by the bean provider. The EJB container does not know which of the entity bean's data are persistent or in what persistence medium they have been stored.

For the bean developer, entity beans with bean-managed persistence definitely mean greater programming effort. They may further be portable only with restrictions, since the bean's code is often optimized for a particular storage medium. Nevertheless, there are cases in which one cannot or does not wish to use container-managed persistence, such as when an entity bean—for example, images in the form of GIF files—is to be stored in a database and must use the data type BLOB (binary large object). Precisely this data type often requires special handling that the EJB container or persistence manager does not support. Another example is that in which an entity bean stores data not in a database, but in an electronic archive. For such systems there is no support from the EJB container or persistence manager. If such a system must be used as a storage medium, then the persistence can be realized using entity beans with bean-managed persistence.

Overview

Figure 5-19 gives an overview of the classes and interfaces of an entity bean with bean-managed persistence that supports the remote client view. To the right are the entity bean classes whose instances are managed by the EJB container at run time. On the left are to be found the interfaces that are implemented through the home and remote objects of the EJB container (see Chapter 3). Through these interfaces the client obtains access to the entity bean.

click to expand
Figure 5-19: Remote interfaces and classes of the BMP entity bean.

Every entity bean class implements the interface javax.ejb.EntityBean. It is in this entity bean class that the bean developer implements the actual functionality. The bean developer has considerable freedom in the organization of the persistent data. He or she does not have to specify the persistent attributes in the deployment descriptor.

Even in the case of bean-managed persistence a primary key is defined that identifies the bean class uniquely. The primary key class appears at the top left of the figure. It implements the interface java.io.Serializable.

Figure 5-20 shows the classes and interfaces of an entity bean with bean-managed persistence that supports the local client view. To the right is the entity bean class, and to the left, the interfaces that are used by the local client and are implemented through the local home object and local object of the EJB container. The entity bean class of the local client view is identical to that of the remote client view. The differences between the two cases are handled by the EJB container through the implementation of the relevant interfaces. The bean class and bean instance remain untouched. The differences for the client of an entity bean between the local and remote client views are the same as for session beans. These differences were discussed in Chapter 4.

click to expand
Figure 5-20: Local interfaces and classes of the BMP entity bean.

The components of an entity bean with bean-managed persistence can be divided into four groups:

  • Attributes;

  • State management;

  • Identity management;

  • Application logic.

The example at the end of this section offers a practical introduction to programming entity beans with bean-managed persistence. In that example we will discuss all components of the entity bean in detail.

Attributes

In contrast to the case of entity beans with container-managed persistence, here the bean provider is completely free to choose the definition of the attributes. The visibility and type of attributes can be chosen completely freely by the bean provider. In the end, he or she is responsible for synchronizing the attributes with the storage medium.

State Management

At the top of Figures 5-19 and 5-20 can be seen the methods for state management. The EJB container calls these methods to initiate a state transition and to inform the bean about a change in state (see Figure 5-6).

void setEntityContext(EntityContext ctx)

The method setEntityContext is called by the EJB container when a bean instance is generated and changes into the state Pooled. It serves to initialize the bean. The EJB container gives the bean its EntityContext. This method should only store the EntityContext and possess no additional business-related functionality.

void unsetEntityContext()

This method is called by the EJB container when it no longer has need of a bean instance and wishes to remove it from the pool. The call informs the bean instance that its associated EntityContext is no longer valid.

void ejbActivate()

The EJB container calls this method to inform an entity bean that it has received a particular entity bean identity. The bean instance changes into the state Ready-Async. In this state, in the EntityContext, the primary key can already by queried with getPrimaryKey and the remote interface with getEJBObject. The bean instance will not yet be synchronized with the database, and thus the values of the persistent attributes not yet set.

This method can be used to open database connections or to reserve other resources. One should note that no transactions are available to this method.

void ejbPassivate()

This method is the complement to ejbActivate. It is called by the EJB container when a bean instance changes state from Ready to Pooled. The bean instance then no longer possesses a bean identity. However, the bean identity can still be used in the method.

This method is used to release resources that were reserved with ejbActivate or later in the state Ready.

void ejbLoad()

The method ejbLoad is called by the EJB container to synchronize the state of a bean instance with the persistence medium. This is necessary if the bean instance has not yet been initialized or if, for example, the database content has been changed by a parallel access. The EJB container can synchronize the bean at any time in the state Ready. This frequently happens before a method of the application logic is called. The bean changes from the state Ready-Async to Ready-Sync.

With this method the bean provider programs the functionality for loading the attribute values from the persistence medium. Additionally, it can be necessary to recalculate transient attributes.

The transaction context for the method cannot be easily predicted. The method that causes the synchronization also determines the transaction context. If, for example, an EJB container synchronizes the bean before each method call of the application logic, then the transaction attribute of the following method determines the transaction context (for transactions, see Chapter 7).

void ejbStore()

When a client changes the state of a bean, the altered attribute values must be written to the persistence medium. In this way the bean instance changes from the state Ready-Update to the state Ready-Sync. This method implements the storage of the persistent attributes.

Normally, this method is called by the EJB container before it terminates a transaction. All data in temporary storage must be written to the database, since otherwise, they will not be persistent (see Chapter 7 on transactions).

Identity Management

The methods for identity management form the second group in Figure 5-19 and Figure 5-20. They enable the generation, deletion, and search of entity bean identities. These methods are again found in the home interface and are available to the client even before it has a bean instance.

ejbFind<name>(<args>)

There are one or more search methods. The number of targets of a search method can be either a single entity bean or a collection of several entity beans. The methods are defined in the home interface and there are available to the client. In contrast to container-managed persistence, the associated search methods must be implemented by the bean developer in the bean class. In the case of bean-managed persistence there is no possibility of implementing finder methods declaratively using EJB-QL.

As with container-managed persistence, there must be at least one method findByPrimaryKey in the bean's (local) home interface that is implemented in the bean class as ejbFindByPrimaryKey. The bean provided can, according to need, define additional finder methods, which are constructed with the same naming pattern.

One should note that the search methods of an entity bean return one or more instances of the primary key class, while the methods in the (local) home interface return one or more references to beans. The requisite conversion is managed by the EJB container. It ensures that a bean instance is activated with the corresponding bean identity and transmits its remote or local interface to the client.

Searches that can have only a single target directly use the class of the primary key or the remote or local interface as data type for the return value. If a search can have several targets, then java.util.Enumeration (Java 1.1) or java.util.Collection (Java 1.2) is used as data type for the return value.

All search methods can be executed by the EJB container in a transaction. The transaction attribute of the corresponding methods in the (local) home interface determines the behavior of the EJB container (again, see Chapter 7 for transactions).

In the case of bean-managed persistence there are no select methods.

<primKeyClass> ejbCreate(<args>)

There are one or more methods to create entity beans, all of which are called ejbCreate and define the primary key class of the bean as return value. These methods are distinguished one from the other by their parameters and can otherwise be defined by the developer at will. The methods are not a part of the entity bean interface, since the parameter and return value types are different for each bean class.

These methods are available to the client and are therefore also declared in the bean's (local) home interface. There the methods are called create(...), and are otherwise distinguished by the return value of their counterparts in the bean class. The methods in the (local) home interface have the bean's remote or local interface as return value. The corresponding ejbCreate methods in the bean class use instead the primary key. The EJB container provides the necessary conversion as well as the mapping of the create methods of the (local) home interfaces to the ejbCreate methods of the bean class.

Using the ejbCreate methods, the bean provider programs the placing of a corresponding data record in the persistence system. The transaction context of the ejbCreate methods depends on the transaction attributes of the methods in the deployment descriptor.

void ejbPostCreate(<args>)

For each ejbCreate method the bean developer defines an ejbPostCreate method with the same parameter types. The ejbPostCreate method is always executed by the EJB container after the corresponding ejbCreate method with the same transaction context.

In these methods further initialization steps can be executed. In contrast to the ejbCreate methods, the bean identity is available to these methods. With the execution of ejbCreate and ejbPostCreate the bean instance changes from the state Pooled to the state Ready-Update.

void ejbRemove()

To the class of methods for managing bean identities belong as well the methods for deleting beans. The client gives the order for the deletion of an entity bean identity by a call to the method remove in the (local) home, local, or remote interface. The call is delegated by the EJB container to the method ejbRemove of the bean instance.

In this method the bean provider programs the deletion of the data record in the persistence system. After the method has been executed, the bean identity is no longer allowed to exist in the persistence system. Additionally, reserved resources must be released in this method. The bean instance changes into the state Pooled and can be used for other bean identities. The method ejbPassivate is not called in this state transition.

The method ejbRemove can be called by the EJB container with a transaction. Here the associated transaction attribute in the deployment descriptor defines the behavior of the EJB container (see Chapter 7 on transactions).

Application Logic and Home Methods

The third group of bean methods is that of the methods for the application logic, which are defined in the local or remote interface and implemented in the bean class. The definition in the bean class has the same signature as the definition in the remote or local interface. The implementation is equivalent to that of entity beans with container-managed persistence.

Entity beans with bean-managed persistence can also define home methods. Here the same rules and behaviors hold as for entity beans with container-managed persistence.

In the case of an entity bean with bean-managed persistence as well, access to the database is generally not made in the application methods. This is reserved to the methods for state management and the management of identities.

Example: Counter

The best way to explain the differences between bean-managed persistence and container-managed persistence is by means of an example. To this end we return to the example Counter and implement it this time as an entity bean with bean-managed persistence. The counter bean will (as in the case of container-managed persistence) support the remote client view.

Listing 5-48 shows the remote interface of the counter bean. It is identical to the counter bean with container-managed persistence (compare Listing 5-8). The CounterOverflowException is also familiar, as is the database schema (compare Listing 5-14).

Listing 5-48: Remote interface of the counter bean (BMP).

start example
package ejb.counterBmp;

import java.rmi.RemoteException;
import javax.ejb.EJBObject;

public interface Counter extends EJBObject {

    public void inc()
        throws RemoteException, CounterOverflowException;

    public void dec()
        throws RemoteException, CounterOverflowException;

    public int getValue()
        throws RemoteException;
}
 
end example

Listing 5-49 shows the home interface of the counter bean. The only difference between this and the counter bean with container-managed persistence is that the method getAllCounterIds is lacking (compare Listing 5-10). Instead, we now have the method findAllCounters. The home method getAllCounterIds has used a select method in the implementation to find all counters. Since there are no select methods in the case of bean-managed persistence, this functionality was implemented this time, for the purpose of illustration, as a finder method. The implementation as a home method would have been possible as before. The home method would have to have implemented database access instead of relying on the select method.

Listing 5-49: Home interface of the counter bean (BMP).

start example
package ejb.counterBmp;

import javax.ejb.CreateException;
import javax.ejb.EJBHome;
import javax.ejb.FinderException;
import java.rmi.RemoteException;
public interface CounterHome extends EJBHome {

   public Counter create(String counterId, int initCounterValue)
       throws CreateException, RemoteException;

   public Counter findByPrimaryKey(String primaryKey)
       throws FinderException, RemoteException;

   public java.util.Collection findAllCounters()
       throws FinderException, RemoteException;
}
 
end example

For the client it makes no difference whether it is dealing with an entity bean with container-managed persistence or one with bean-managed persistence. This underscores the fact that the home and remote interfaces are left essentially unchanged.

On the other hand, the implementation of the bean class has changed considerably. Listing 5-50 shows the implementation. Already in the import statements it is clear that the bean itself accesses the database (java.sql.*).

Listing 5-50: Bean class of the counter bean (BMP).

start example
package ejb.counterBmp;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import javax.sql.DataSource;

import javax.ejb.CreateException;
import javax.ejb.EJBException;
import javax.ejb.EntityBean;
import javax.ejb.EntityContext;
import javax.ejb.FinderException;
import javax.ejb.ObjectNotFoundException;

public class CounterBean implements EntityBean {

    public static final String dbRef =
        "java:comp/env/jdbc/CounterDB";

    public final static int VALUE_MAX = 100;
    public final static int VALUE_MIN = 0;

    private EntityContext ctx;
    private DataSource dataSource;

    private String counterId;
    private int counterValue;

     /*
      * the Create method of the home interface
      */

    public String ejbCreate(String counterId, int initCounterValue)
        throws CreateException
    {
        if(counterId == null) {
            throw new CreateException("id must not be null!");
        }
        if(initCounterValue < VALUE_MIN ||
           initCounterValue > VALUE_MAX)
        {
            throw new CreateException("initValue out of range!");
        }
        this.initDataSource();

        this.counterId = counterId;
        this.counterValue = initCounterValue;

        try {
            this.create();
        } catch(SQLException ex) {
            throw new CreateException(ex.getMessage());
        }

        return this.counterId;
    }
    public void ejbPostCreate(String accountId,
                              int initCounterValue)
    {}
     /*
      * Utility-Methods
      */

    private void initDataSource() {
        if(this.dataSource != null) {
            return;
        }
        try {
           Context c = new InitialContext();
           this.dataSource = (DataSource)c.lookup(dbRef);
        } catch(NamingException ex) {
           String msg = "Cannot get Resource-Factory:" + ex.getMessage();
           throw new EJBException(msg);
        }
    }

    private void create()
        throws SQLException
    {
        final String query =
            "INSERT INTO COUNTER(ID, VALUE) VALUES(?, ?)";
        Connection con = null;
        PreparedStatement st = null;
        try {
            con = this.dataSource.getConnection();
            st = con.prepareStatement(query);
            st.setString(1, this.counterId);
            st.setInt(2, this.counterValue);
            st.executeUpdate();
        } finally {
            try { st.close(); } catch(Exception ex) {}
            try { con.close(); } catch(Exception ex) {}
        }
    }
     /*
      * The finder methods of the home interface
      */

    public String ejbFindByPrimaryKey(String pk)
        throws FinderException
    {
        this.initDataSource();

        final String query =
            "SELECT ID FROM COUNTER WHERE ID=?";
        Connection con = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            con = this.dataSource.getConnection();
            st = con.prepareStatement(query);
            st.setString(1, pk);
            rs = st.executeQuery();
            if(!rs.next()) {
                throw new ObjectNotFoundException(pk);
            }
        } catch(SQLException ex) {
            ex.printStackTrace();
            throw new FinderException(ex.getMessage());
        } finally {
            try { st.close(); } catch(Exception ex) {}
            try { rs.close(); } catch(Exception ex) {}
            try { con.close(); } catch(Exception ex) {}
        }
        return pk;
    }
    public Collection ejbFindAllCounters()
        throws FinderException
    {
        this.initDataSource();

        final String query =
            "SELECT ID FROM COUNTER";
        Connection con = null;
        Statement st = null;
        ResultSet rs = null;
        ArrayList ret = new ArrayList();
        try {
            con = this.dataSource.getConnection();
            st = con.createStatement();
            rs = st.executeQuery(query);
            while(rs.next()) {
                ret.add(rs.getString(1));
            }
        } catch(SQLException ex) {
            ex.printStackTrace();
            throw new FinderException(ex.getMessage());
        } finally {
            try { st.close(); } catch(Exception ex) {}
            try { rs.close(); } catch(Exception ex) {}
            try { con.close(); } catch(Exception ex) {}
        }
        return ret;
    }
     /*
      * The business methods of the remote interface
      */

    public void inc() throws CounterOverflowException {
        if(this.counterValue < VALUE_MAX) {
            this.counterValue += 1;
        } else {
            String s = "Cannot increase above "+VALUE_MAX;
            throw new CounterOverflowException(s);
        }
    }

    public void dec() throws CounterOverflowException {
        if(this.counterValue > VALUE_MIN) {
            this.counterValue -= 1;
        } else {
            String s = "Cannot decrease below "+VALUE_MIN;
            throw new CounterOverflowException(s);
        }
    }

    public int getValue() {
        return this.counterValue;
    }

     /*
      * The methods of the entity bean interface
      */

    public void ejbActivate() {
        this.initDataSource();
    }

    public void ejbPassivate() {
        this.dataSource = null;
    }

    public void setEntityContext(EntityContext ctx) {
        this.ctx = ctx;
    }

    public void unsetEntityContext() {
        this.ctx = null;
    }
    public void ejbLoad() {
        this.counterId = (String)this.ctx.getPrimaryKey();
        final String query = "SELECT VALUE FROM COUNTER WHERE ID=?";
        Connection con = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            con = this.dataSource.getConnection();
            st = con.prepareStatement(query);
            st.setString(1, this.counterId);
            rs = st.executeQuery();
            if(rs.next()) {
                this.counterValue = rs.getInt(1);
            } else {
                String s = this.counterId + " not found";
                throw new SQLException(s);
            }
        } catch(SQLException ex) {
            ex.printStackTrace();
            throw new EJBException(ex.getMessage());
        } finally {
            try { st.close(); } catch(Exception ex) {}
            try { rs.close(); } catch(Exception ex) {}
            try { con.close(); } catch(Exception ex) {}
        }
    }
    public void ejbStore() {
        final String query =
            "UPDATE COUNTER SET VALUE=? WHERE ID=?";
        Connection con = null;
        PreparedStatement st = null;
        try {
            con = this.dataSource.getConnection();
            st = con.prepareStatement(query);
            st.setInt(1, this.counterValue);
            st.setString(2, this.counterId);
            st.executeUpdate();
        } catch(SQLException ex) {
            ex.printStackTrace();
            throw new EJBException(ex.getMessage());
        } finally {
            try { st.close(); } catch(Exception ex) {}
            try { con.close(); } catch(Exception ex) {}
        }
    }

    public void ejbRemove() {
        final String query =
            "DELETE FROM COUNTER WHERE ID=?";
        Connection con = null;
        PreparedStatement st = null;
        try {
            con = this.dataSource.getConnection();
            st = con.prepareStatement(query);
            st.setString(1, this.counterId);
            st.executeUpdate();
        } catch(SQLException ex) {
            ex.printStackTrace();
            throw new EJBException(ex.getMessage());
        } finally {
            try { st.close(); } catch(Exception ex) {}
            try { con.close(); } catch(Exception ex) {}
        }
     }
}

 
end example

In the method ejbCreate a new counter bean is placed in the database. It is called whenever the client wishes to generate a new counter and to this end calls the method create in the home interface. In addition to the initialization of the persistent attributes (which this time are declared private), the bean must see to it that the corresponding data record is placed in the database. This happens via the private auxiliary methods initDataSource and create. A further peculiarity of the ejbCreate method is that instead of null being returned (as with beans with container-managed persistence), what is returned is the bean's primary key instance. The implementation of the ejbPostCreate method remains empty, since no further intialization steps are necessary.

There follows the implementation of the two finder methods findByPrimaryKey (ejbFindByPrimaryKey) and findAllCounters (ejbFindAllCounters). In the case of container-managed persistence the finder methods are neither declared nor implemented in the bean class. They are defined with the help of EJB-QL in the deployment descriptor. The method ejbFindByPrimaryKey has the task of verifying whether a data record with the indicated primary key is to be found. If so, the relevant primary key is returned. The EJB container initializes a counter bean with this primary key instance in the state Pooled and returns the remote object of this bean to the client as return value for the findByPrimaryKey call. If such a data record is unavailable, then an exception is triggered. The same holds for the method ejbFindAllCounters, with the difference that an empty collection is returned if no data records are found. An important detail with these finder methods is the call to the auxiliary method initDataSource at the beginning of the method. Since the finder methods are called on the bean instance in the state Pooled, the data source for the database connection might be uninitialized.

The implementation of the business methods inc, dec, and getValue remains unchanged. Instead of calling the abstract persistence methods (which do not exist in the case of bean-managed persistence), the methods rely on the member variables of the bean class.

The method ejbActivate initializes the bean instance with a state change from Pooled to Ready, using the auxiliary method initDataSource. The method ejbPassivate does exactly the opposite.

The method ejbLoad has the task of synchronizing the bean instance with the data from the database. The primary key is initialized via the EntityContext, while the remaining attributes are handled with the data from the database that go with this primary key. It is important to initialize the primary key attribute from the EntityContext, since the entity bean instance may have changed its identity since the last synchronization. A possible leftover value of the primary key attribute could be invalid.

The method ejbStore has the task of synchronizing the data in the database with the data from the bean instance. As a result, the data in the database are overridden with values of the persistent attributes.

The method ejbRemove has the task of deleting the data record that is represented by the given entity bean instance. The entity bean instance then changes into the state Pooled and must be prepared to be reused with another identity.

Listing 5-51 shows the deployment descriptor of the counter bean with bean-managed persistence. In the element persistence-type you will see that the value Bean has been specified instead of Container. An entity bean with bean-managed persistence has no abstract persistence schema that would have to be declared. Instead, a resource factory reference for access to the database is defined.

Listing 5-51: Deployment descriptor of the counter bean (BMP).

start example
<?xml version="1.0" ?>
<ejb-jar version="2.1" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/ejb-jar_2_1.xsd">
  <enterprise-beans>
    <entity>
      <ejb-name>CounterBmp</ejb-name>
      <home>ejb.counterBmp.CounterHome</home>
      <remote>ejb.counterBmp.Counter</remote>
      <ejb-class>ejb.counterBmp.CounterBean</ejb-class>
      <persistence-type>Bean</persistence-type>
      <prim-key-class>java.lang.String</prim-key-class>
      <reentrant>False</reentrant>
      <resource-ref>
        <description> Euro-Datenbank </description>
        <res-ref-name>jdbc/CounterDB</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
        <res-sharing-scope>Shareable</res-sharing-scope>
      </resource-ref>
    </entity>
  </enterprise-beans>

  <assembly-descriptor>
    <container-transaction>
      <method>
        <ejb-name>CounterBmp</ejb-name>
        <method-name>inc</method-name>
      </method>
      <method>
        <ejb-name>CounterBmp</ejb-name>
        <method-name>dec</method-name>
      </method>
      <trans-attribute>Required</trans-attribute>
    </container-transaction>
  </assembly-descriptor>

</ejb-jar>
 
end example

A description of the appearance of the mapping of persistent attributes to the database is not necessary with bean-managed persistence.