Side of Software
|
|
Persistence Library TutorialTable of ContentsThe Application without Persistence The Application with Persistence IntroductionThis tutorial steps you through the creation of two functionally identical banking applications—one that uses the Persistence Library and one that does not. By the end of the tutorial, we will have discussed these files:
Account.java. The interface for a simple bank account. DefaultAccount.java. A straightforward implementation of an account. BankExampleWithoutDatabase.java. The example application that does not use a database. BankExampleWithDatabase.java. The example application that uses a database.
We show that adding an object-oriented database to a well-designed application requires very little additional programming. A head-to-head comparison of the two applications highlights the differences. The RequirementsOur sample application should be simple enough to describe in a tutorial yet complex enough to show the benefits of the Persistence Library. We choose a banking application because the domain is well understood and the programs are often multi-threaded.
Our application will have the following capabilities:
The application creates 10 accounts each with an initial
balance of 100 units. It launches 5 deposit threads, 5 transfer threads, and
5 report threads. Each deposit thread deposits 10 units in each account 10 times.
Each transfer thread moves 20 units from Account The ModelThe object model of our banking system is quite simple. Each
account is an object that implements the The Account InterfaceHere is the code for the
public interface Account { /** * Returns this account's balance.<p> */ public double getBalance(); /** * Sets this account's balance.<p> * * @param balance this account's new balance */ public void setBalance( double balance ); }
For our purposes, an account simply contains a balance and
a way to set and retrieve it. The method
In a real-world application, the account interface would be much more complex. It would contain account numbers, references to its owners, and a history of balances (see the Dated Collections Library for an easy way to add a history of values). Furthermore, it may support property and vetoable change listeners. The DefaultAccount ImplementationWe implement our
public class DefaultAccount implements Account, Serializable { private double balance; public double getBalance() { return balance; } public void setBalance( double balance ) { this.balance = balance; } }
The
Rule 1: The Persistence Library requires that all database objects be serializable. The Application without PersistenceWe first write the application without persistence and then show how adding persistence requires little effort. You can find the complete database-less application in BankExampleWithoutDatabase.java. Threads and AtomicitySince our application uses multiple threads, we must
ensure that the map and the accounts are accessed safely. A thread invoking
To achieve this behavior, we could associate a read-write lock with each account. Before a thread accesses an account, the thread acquires either its read lock or write lock, depending on the thread’s intentions. Unfortunately, this approach can easily lead to deadlock. Consider a transfer transaction. Suppose Thread A wishes to transfer from Account 5 to Account 16, and Thread B wishes to transfer from Account 16 to Account 5. If Thread A acquires the write lock for 5 and Thread B acquires the write lock for 16, each thread prevents the other thread from acquiring the next required lock (assuming a write lock cannot be obtained until its corresponding read lock has been released by all other threads).
It turns out to be extremely difficult to write this application with maximum concurrency. The application would not only have to handle the read-write locks but also implement a policy to avoid or detect deadlock, increasing the application’s code significantly.
It is much easier to use Java’s The Open Account TransactionThe application first creates 10 accounts (numbered 0 through 99) with an initial balance of 100: // create the accounts for( int i = 0; i < NUM_ACCOUNTS; i++ ) openAccount( i, INITIAL_BALANCE );
To create an account, we construct an instance of private synchronized Account openAccount( int accountNumber, double initialBalance ) { // create the account Account account = new DefaultAccount(); // store this account in the map indexed by account number Object key = new Integer( accountNumber ); accounts.put( key, account ); // set the initial balance of this account account.setBalance( initialBalance ); return account; } If the method were not marked The Lookup TransactionGiven an account number, we can retrieve the corresponding account by doing a synchronized lookup in the map: private synchronized Account lookupAccount( int accountNumber ) { Object key = new Integer( accountNumber ); return (Account)accounts.get( key ); // search the accounts } Access to the map must be synchronized. If a different thread were trying to open a new account at the same time, we would have a conflicting data race. The Deposit / Withdrawal TransactionWe can implement both deposits and withdrawals with a single method. Withdrawals are simply a deposit of a negative amount. private synchronized void deposit( Account acct, double amount ) { double balance = acct.getBalance(); // get the current balance balance += amount; // adjust the balance acct.setBalance( balance ); // set the new balance }
Again, we make the method synchronized to ensure that two threads depositing to the same account at the same time set the correct amounts. The Transfer TransactionThe transfer operation is comprised of two transactions—a withdrawal from one account and a deposit to another. private synchronized void transfer( Account fromAcct, Account toAcct, double amount ) { deposit( fromAcct, -amount ); // take money from one account deposit( toAcct, amount ); // add money to the other account }
Even though the The Report TransactionThe report thread iterates through the accounts, summing their balances as it goes: private synchronized void generateReport() { int numAccounts = accounts.size(); // how many accounts? double total = 0; // for each account Collection values = accounts.values(); Iterator acctIterator = values.iterator(); while( acctIterator.hasNext() ) { Account account = (Account)acctIterator.next(); double balance = account.getBalance(); // get the balance total += balance; // keep a running total } // print the report System.err.println( total + " for " + numAccounts + " accounts " ); }
The entire iteration must be synchronized; otherwise, a thread opening an account may alter the map in the middle of the iteration. The Worker ThreadsThe main body of our application spawns numerous threads, starts them, and waits for them to finish:
The code that creates a deposit thread ( private Thread createDepositThread( final double depositAmount ) { Thread thread = new Thread( new Runnable() { public void run() { for( int acctNumber = 0; acctNumber < NUM_ACCOUNTS; acctNumber++ ) { Account acct = (Account)lookupAccount( acctNumber ); for( int i = 0; i < NUM_DEPOSITS; i++ ) { deposit( acct, depositAmount ); // make the deposit } } } } ); return thread; } The code that creates a transfer thread and report thread is similar and can be found in the application’s source code. A Sample ExecutionYou can find the complete database-less application in BankExampleWithoutDatabase.java. An execution of the application may produce the following output:
The actual values printed depend on the thread ordering. Each execution may yield a different order. We can see from this execution that deposits and transfers were made before, during, and after the numerous report generations. The final account balances are verified with the code: // verify that the account balances are what we expected double expectedBalance = INITIAL_BALANCE + NUM_DEPOSIT_THREADS * NUM_DEPOSITS * AMOUNT_TO_DEPOSIT; for( int i = 0; i < NUM_ACCOUNTS; i++ ) { Account acct = lookupAccount( i ); double balance = acct.getBalance(); assert balance == expectedBalance : "Incorrect balance for account " + i + ": " + balance; } Since no assertion error was thrown, the accounts had the expected balance. The Application with PersistenceSaving the map and accounts in an object-oriented database requires little additional programming. To add persistence to our application, we:
The complete application can be found in BankExampleWithDatabase.java.
Rule 2: The Persistence Library requires you to program
by interface. This means that all of the domain classes should be
completely defined by one or more interfaces and the types of all mutable method
parameters should be interfaces. The domain model of our bank account system—as
well as the model of most well-designed applications—follows this convention.
The Java Collections API also satisfies this requirement. Consequently, the application
may not invoke a method that is defined in a class but not in an interface
(such as Opening and closing the databaseTo incorporate a database, we must first decide what type
of database our application should use. The Persistence Library provides two types:
In our application, the type of database does not matter since each run re-initializes the database with 10 accounts. We choose to use a file database, but you may uncomment the line above it to use an in-memory database:
// construct the database // db = new InMemoryDatabase( "Account Database" ); db = new FileDatabase( "Account Database", "test\\accts.db" ); The name of our database is “Account Database” and it resides in the file “test\accts.db” relative to the current working directory.
Before we can use the database, however, we must open it. It cannot be opened unless it exists, so our application first creates the database and then opens it:
// create the database with the account map as the root object db.create( new HashMap() ); // open the database db.open( false ); In the call to
To open the database, we simply invoke // close the database db.close();
Adding objects to the databaseAll objects to be saved in the database must be registered
with the database via a call to
Rule 3: After an object is passed to
Following this rule, the private Account openAccount( int accountNumber, double initialBalance ) { db.startTransaction(); // create the account Account account = new DefaultAccount(); account = (Account)db.addObject( account ); // store this account in the map indexed by account number Object key = new Integer( accountNumber ); accounts.put( key, account ); // set the initial balance of this account account.setBalance( initialBalance ); db.commitTransaction( null ); return account; } As soon as the account is instantiated, it is added to the
database. It is also okay to initialize the account before adding it to the
database, although it is good practice to add the object immediately after instantiation.
Notice that, after the account is added to the database, the variable is
reassigned to the downcast proxy. This means that
Rule 4: Immutable objects (like instances of
The root object also requires a proxy that must be
retrieved via the // load the root object accounts = (Map)db.getRoot(); Identifying transactionsJust as we did in the first version of the application, we must identify groups of operations that should execute as a unit. Instead of using Java’s built-in synchronization, however, we use the database to identify transactions. Each database transaction exhibits the ACID properties (Atomicity, Consistency, Isolation, and Durability).
We invoke
Rule 5: A transaction must be carried out by a single thread.
Using private void deposit( Account acct, double amount ) { db.startTransaction(); double balance = acct.getBalance(); // get the current balance balance += amount; // adjust the balance acct.setBalance( balance ); // set the new balance db.commitTransaction( null ); } private void generateReport() { db.startTransaction(); int numAccounts = accounts.size(); // how many accounts? double total = 0; // for each account Collection values = accounts.values(); Iterator acctIterator = values.iterator(); while( acctIterator.hasNext() ) { Account account = (Account)acctIterator.next(); double balance = account.getBalance(); // get the balance total += balance; // keep a running total } db.commitTransaction( null ); // print the report System.err.println( total + " for " + numAccounts + " accounts " ); } private void transfer( Account fromAcct, Account toAcct, double amount ) { db.startTransaction(); deposit( fromAcct, -amount ); // take money from one account deposit( toAcct, amount ); // add money to the other account db.commitTransaction( null ); } Transactions can be nested. We can invoke
The private Account lookupAccount( int accountNumber ) { Object key = new Integer( accountNumber ); return (Account)accounts.get( key ); // search the accounts } Registering read-only methodsThe database uses a method’s read-only classification
to determine if a registered object changes as a result of a method
invocation. A method is read-only if it is guaranteed not to alter
the receiver in any way. Our
Although it is not necessary for correctness, for
efficiency we should inform the read-only manager that the
ReadOnlyManager.setMethodStatus( Account.class, "getBalance", null, ReadOnlyManager.READ_ONLY );
Without this statement, each invocation of an account’s A Sample ExecutionYou can find the complete application in BankExampleWithDatabase.java and a line-by-line comparison of the two versions here. An execution may produce the following output:
Again, since no assertion error was thrown, the final account balances matched the expected balances. Additional Database TopicsThe above example discusses the minimum steps needed to incorporate mostly transparent persistence in an application. This section discusses the following additional topics:
Transaction FailuresTransactions may fail. They may fail because of an underlying I/O error or because of a transaction conflict. For example, the database may be corrupted or it may decide to abort a transaction because it has deadlocked with another transaction.
Whenever a transaction fails, the database throws a
Our banking application does not handle transaction failures. To make it more robust, we would have to examine each transaction and determine an appropriate action to a failure. Common actions include ignoring it, quitting, retrying, and informing the user. The following code shows how the deposit thread can forever retry aborted transactions: private Thread createDepositThread( final double depositAmount ) { Thread thread = new Thread( new Runnable() { public void run() { for( int acctNumber = 0; acctNumber < NUM_ACCOUNTS; acctNumber++ ) { try { Account acct = (Account)lookupAccount( acctNumber ); for( int i = 0; i < NUM_DEPOSITS; i++ ) { try { deposit( acct, depositAmount ); // make the deposit } catch( TransactionAbortedException tae ) { i--; // try again } } } catch( TransactionAbortedException tae ) { acctNumber--; // try again } } } } ); return thread; } Failure of nested transactions requires attention as well.
If an inner transaction fails, should we abort the outer transaction? In the
following code, we modify private void transfer( Account fromAcct, Account toAcct, double amount ) { db.startTransaction(); try { deposit( fromAcct, -amount ); // take money from one account deposit( toAcct, amount ); // add money to the other account db.commitTransaction( null ); } catch( TransactionAbortedException tae ) { db.abortTransaction(); throw tae; } } The above code uses Transaction PoliciesIt is the database’s responsibility to ensure that
transactions exhibit the ACID properties. The library’s implementation
actually delegates transaction behavior to an instance of
The constructors of both
As we mentioned before, this approach does not allow any
concurrency, causing transactions that touch disjoint portions of the
database to unnecessarily interfere. The Persistence Library provides another
transaction policy, called
Since
This policy is not ideal for our application because of the high number of conflicting transactions. If you change the instantiation of the database to: // construct the database TransactionPolicy policy = new DefaultReadWriteTransactionPolicy( 200, 100 ); db = new FileDatabase( "Account Database", "test\\accts.db", policy ); then you can see that many LoggingIn the sample execution above, you may have noticed the log messages that the application outputted. The persistence framework uses Java’s Logging API to log database activity. By default, the following database activities are logged to the logger named “sos.db”:
We can adjust our application’s logging behavior either
statically or at run-time, using the approaches described in the Logging
Specification. For example, we can lower the log level to FINE by adding the
following code to
With this change in place, messages regarding transaction starts, aborts, and commits are also outputted: Mar 4, 2004 8:57:05 AM sos.db.AbstractSerializationDatabase logDatabaseOpened INFO: Database "Account Database" opened. Mar 4, 2004 8:57:05 AM sos.db.AbstractSerializationDatabase logTransactionStarted FINE: Transaction started for Thread[main,5,main] in database "Account Database". Mar 4, 2004 8:57:05 AM sos.db.AbstractSerializationDatabase logTransactionStarted FINE: Transaction started for Thread[main,5,main] in database "Account Database". Mar 4, 2004 8:57:05 AM sos.db.AbstractSerializationDatabase logTransactionCommitted FINE: Transaction committed for Thread[main,5,main] in database "Account Database". Mar 4, 2004 8:57:05 AM sos.db.AbstractSerializationDatabase logTransactionStarted FINE: Transaction started for Thread[main,5,main] in database "Account Database". Mar 4, 2004 8:57:05 AM sos.db.AbstractSerializationDatabase logTransactionCommitted FINE: Transaction committed for Thread[main,5,main] in database "Account Database". ... [Zillions of messages omitted] ... Mar 4, 2004 8:58:48 AM sos.db.AbstractSerializationDatabase logTransactionCommitted FINE: Transaction committed for Thread[main,5,main] in database "Account Database". Mar 4, 2004 8:58:48 AM sos.db.AbstractSerializationDatabase logTransactionStarted FINE: Transaction started for Thread[main,5,main] in database "Account Database". Mar 4, 2004 8:58:48 AM sos.db.AbstractSerializationDatabase logTransactionCommitted FINE: Transaction committed for Thread[main,5,main] in database "Account Database". Mar 4, 2004 8:58:48 AM sos.db.AbstractSerializationDatabase logTransactionStarted FINE: Transaction started for Thread[main,5,main] in database "Account Database". Mar 4, 2004 8:58:48 AM sos.db.AbstractSerializationDatabase logTransactionCommitted FINE: Transaction committed for Thread[main,5,main] in database "Account Database". Mar 4, 2004 8:58:48 AM sos.db.AbstractSerializationDatabase logDatabaseClosed INFO: Database "Account Database" closed. Developers can use the logging to aid in debugging, and system administrators can use it to monitor client usage.
For additional information on the Persistence Library, consult the Persistence Library API.
Home | Contact Us | Privacy Policy
Copyright © 2016 Side of Software, a branch of Natavision. All rights reserved.
|