A Java solution for storing objects in relational databases.
Links
|
|
This package provides a lightweight and flexible solution for storing and retrieving java objects in and from relational databases. It is intended for developers who don't want to cope with SQL statements in their code. It is not an alternative to a full-featured application server because of several limitations this package has. But if you want to create a small or midium-sized application with access to a database, with or without a web-tier, this might be what you need.
Although the code has matured by now, it is still work in progress. For me it has been quite useful. If you have any comments and suggestions please feel free to contact me.
DB Objects is under the LGPL. Have a look at GNU Project: List of Licenses for more details.
Credits go to Marc A. Mnich for his class DBConnectionBroker that he allowed me to include into DB Objects and Brad Matlack for his patches that are included in the current version.
New version 0.34 (July 25, 2001): After almost a year with little progress, a new version is available. See version history for details.
Please note that there is a new project available that builds on the ideas of DB Object. It is called jStorm, developed by Gareth Reeves. Future development on his project will be more avtive than on this one.
This project is hosted by
Technical Summary
The approach taken in this package is to view a database only as a storage mechanism. The advantage is that storing and retrieving data can be done generically without any user-specific code. The drawback is that special database features are hard to support from a user's point of view.
One class, the DBManager, is responsible for all access to the database. It uses connection pooling for high performance access to the database, executes pre-generated statements, and creates new objects after they are retrieved from the database.
Each class whose objects have to be made persistent has to extend the abstract class StorableObject and needs to be initialized. During the initialization, an ObjectMapping object is created and all information needed about the class is set by the DBManager.
This object mapping defines all fields of the class that should be stored and their corresponding database table and columns. Each time the DBManager stores or retrieves objects, the object mapping is called up to get all information about the class.
For a better understanding of the architecture please look at the following UML diagram:
Consider an application that needs a storage mechanism for the data of some of its classes. To use the DB Objects package, all classes of the application with persistent data have to extend StorableObject. During initialization of the application, each one of these classes is initialized and an ObjectMapping is created with all the information about the persistent fields. Each field of the class needs getter and setter methods.
Our example application needs to store data about a customer. Therefore, there is a Customer class with fields like name, email address, etc. For each field, there are getter and setter methods.
Once a new customer signs in, a new Customer object is created. This is done using new Customer(). After all fields are set, the object has to be stored in the database. This is done by calling the store() method, which belongs to StorableObject.
Later on, the customer's data is updated. Some values change and the database has to be updated. This is performed by another call to store().
In a different situation the application wants to list all customers in a table. To achieve this, the Customer class is expanded by a new static method called getCustomerList(). This method implements a call to DBManager.getByField() that, in turn, retrieves a list of all customers stored in the database.
If a customer cancels his or her account, the data in the database needs to be
deleted. To do this, the Customer object must first be retrieved.
A single call to delete() removes the database entry then.
Some source code
To avoid writing complete and in-depth documentation, here is some commented source
code that is taken from the dbobjects.test.mysql package.
1:import org.gjt.tw.dbobjects.*; 2:import java.util.*; 3:To use the package you need to import org.gjt.tw.dbobjects.*.
4: public class Customer extends StorableObject { 5: private static ObjectMapping mapping; 6: private long id; 7: private String name; 8: private String password; 9:To become a storable object, the Customer class extends the class StorableObject. The declaration of an object of class ObjectMapping must be part of each class extending StorableObject. In this example, the field "id" becomes the object's key; name and password are two normal fields.
10: public Customer() { 11: super(); 12: name = ""; 13: password = ""; 14: } 15:The constructor of StorableObject needs to be called. Afterwards all fields that should be stored, have to be initialized.
16: public long getId() { 17: return id; 18: } 19: 20: public String getName() { 21: return name; 22: } 23: 24: public String getPassword() { 25: return password; 26: } 27: 28: public void setId(long newValue) { 29: this.id = newValue; 30: } 31: 32: public void setName(String newValue) { 33: this.name = newValue; 34: } 35: 36: public void setPassword(String newValue) { 37: this.password = newValue; 38: } 39:For each field that is part of the object mapping, there must be a getter and a setter method.
40: protected ObjectMapping getMapping() { 41: if (mapping == null) 42: mapping = new ObjectMapping (); 43: return mapping; 44: } 45:This method has to be copied, as is, into each class extending StorableObject. It returns a reference to the static ObjectMapping object of this class.
46: public static void init () throws ObjectException, IncompleteDefinitionException { 47: mapping = new ObjectMapping (); 48: mapping.setTableName ("customer"); 49: mapping.setObjectClass (Customer.class); 50: mapping.addField ("id", Long.TYPE, "id"); 51: mapping.addField ("name", String.class, "customer_name"); 52: mapping.addField ("password", String.class, "customer_password"); 53: mapping.setKeyField ("id"); 54: mapping.setAutoIncrement (ObjectMapping.AUTOINCREMENT_MYSQL); 55: mapping.prepareSQLStatements (); 56: } 57:Each class needs a static init() method that is called once during initialization of the application.
First, a new mapping object is created. This object contains all the information needed by DBManager to store the fields of this class into the database. Therefore, several calls are needed: The name of the table in which all fields are stored and the current class has to be set explicitly. For each field you want to store, you have to define the name of the field, the class, or type of the field, and the name of the database column. Finally, you have to define the name of the field that is the key for this class.
This package supports MySQL's auto_increment feature. This means, if you use an integer or long as key and this database field is defined as auto_increment, the key of your object is set automatically when the object is stored into the database for the first time. For this to work, you need to use the JDBC driver from org.gjt.mm.mysql. You can get this driver here.
If you prefer not to use MySQL you can use any other jdbc complient database and use the generic id generating feature provided by DB Objects.
The last thing to do is to call prepareSQLStatements(). This checks the completeness and validation of the given information.
58: public static Customer getByName (String name) throws DatabaseException, ObjectException { 59: StorableObject[] objects = DBManager.getByField (mapping, "name", name); 60: if (objects == null) 61: return null; 62: else 63: return (Customer) objects[0]; 64: } 65:}This is an example of how to retrieve an object that is already stored in the database. Calling DBManager.getByField() returns a list of StorableObject objects. If the list is not empty, it can be casted directly to Customer[]. The three parameters include the reference to the mapping object, the name of the field, and the value of that field. In this example, this call is translated to the SQL statement "SELECT * FROM customer WHERE name = xxx" where xxx is the name of the current customer.
Note that there are two kinds of getByXXX-methods. In this example, an array of objects is retrieved that can be cast directly to the objects' class. This is helpful if you only want to have a fixed list of objects (for example, if you are developing a web application). If you want to store the objects permanently in memory and want to add and remove objects, there are getByXXXVector-methods that return a vector of the objects. To add a new object to your application you have to create the object, store it, and add it to the vector. To remove an object, is has to be both removed from the vector and deleted from the database manually. Keep in mind that you are not using Enterprise Java Beans.
1:import org.gjt.tw.dbobjects.*; 2:import java.util.*; 3:import java.io.*; 4: 5:public class TestApplication { 6: 7: public static void main(String args[]) { 8: 9: // Loading properties 10: Properties properties = new Properties(); 11: try { 12: properties.load(new FileInputStream("dbobjects.properties")); 13: } 14: catch (FileNotFoundException f) { 15: System.out.println ("Error during loading of properties: File not found!"); 16: System.exit (-1); 17: } 18: catch (IOException e) { 19: System.out.println ("Error during loading of properties: IOException!"); 20: System.exit (-1); 21: } 22:First, a property file containing information about the database and how to access it needs to be read. Such a property file is part of the distribution. For explanation of the properties, please look at the javadoc documentation for DBManager.
23: // Initialize database connection broker 24: try { 25: DBManager.init (properties); 26: } 27: catch (DatabaseException e) { 28: System.out.println ("Error during initializing of connection to database:\n"+e); 29: System.exit (-1); 30: } 31:The next thing to do is to initialize the DBManager which, in turn, initializes the connection broker that takes care of the database connections.
32: // Initialization of storable objects. 33: try { 34: Customer.init (); 37: } 38: catch (IncompleteDefinitionException e) { 39: System.out.println ("Definition of object mapping is not complete!"); 40: System.exit (-1); 41: } 42: catch (ObjectException e) { 43: System.out.println ("Definition of object mapping is inconsistent with data structure!"); 44: System.exit (-1); 45: } 46:Now, all classes that extend StorableObject need to be initialized.
47: // Do some tests. 48: try { 49: Customer customer = new Customer (); 50: customer.setName ("Test Name"); 51: customer.setPassword ("Test password"); 52: customer.store (); 53: customer.print (); 54: 55: customer = Customer.getByName ("Test Name"); 56: customer.print (); 57: 58: customer.delete (); 59: } 60: catch (Exception e) { 61: System.out.println (e); 62: System.exit (-1); 63: } 64:This is the fun part of the application. A new customer object is created and its values are set. Afterwards the object is stored and its content is printed to the console.
Then, the customer object is retrieved from the database using Customer.getByName(name).
Finally, this customer object is removed from the database using customer.delete().
65: // Shut down database connection broker 66: try { 67: DBManager.destroy (); 68: } 69: catch (Exception e) { 70: System.out.println ("Error during destruction of database connectivity!"); 71: System.exit (-1); 72: } 73: } 74:}Before quitting the application, you should call DBManager.destroy() that, in turn, cleans up and shuts down the database connections.
That implementation queries the database for the highest ID currently used, increases that value, and uses the increased value as the new ID.
Yes, it is, as long as there is only one application using the database at a time. The method that queries that database and creates a new entry is synchronized. Therefore, running a multi-threaded application is not a problem.
However, there are cases where this isn't sufficient: Either you could create a client application that is run by several people at the same time, or you could build a server of which several instances exist. Both times, there might be race conditions that make one application's insert fail. If this happens, an exception is thrown that has to be handled by the application.
As an alternative, you could turn the automatic key generation off and do it manually. You could use, for example, a file that always holds the highest key given so far.