jStorm Cook book

Gareth Reeves


Simple Persistence

How do I persist an Object into the database?

public class User {
    String name;
    String passwd; 
    
    public String getName() {
    	return name;
    }
    public String getPasswd() {
    	return passwd;
    }
    public void setName(String aName) {
    	name = aName;
    } 
    public void setPasswd(String aPasswd) {
    	passwd = aPasswd;
    }
}
Given the simple class listed above how could we persist it into the database? Assuming that a table called Users with columns 'name' and 'passwd, a simple SQL statement should work fine.
INSERT INTO User (name, passwd) VALUES ('Joe', 'Password')

Another class could query the user object for the name and password, build the statement and execute it. Alternatively we could add a method to the User class to build and execute the statement.

public void createInDatabase() {
	Connection connection = DriverManager.getConnection("UrlToDatabase");
	PreparedStatement statement = connection.prepareStatment("INSERT INTO User (name, passwd) VALUES (?,?)");
	statement.setString(1, getName());
	statement.setString(2, getPasswd());
	statement.execute();
}

To retrieve the users from the database you could write another method on the user object.

public static Vector getUsersFromDatabase() {
	Vector users = new Vector();
	Connection connection = DriverManager.getConnection("UrlToDatabase");
	Statement statement = connection.createStatement();
	ResultSet resultSet = statement.execute("SELECT * FROM User");
	while( resultSet.next() ) 
	{
		User newUser = new User();
		newUser.setName(resultSet.getString("name"));
		newUser.setPasswd(resultSet.get("passwd"));
		
		users.add(newUser);
	}
	
	return users;
}

This method works well when you have a simple example like this. Putting the code in the User object makes the User cohesive from a persistence stand point and it reduces coupling (clients of user only need to call User and not the database as well). However, this solution doesn't scale very well. To start with you have to write similar methods for all objects that need to be persisted and that can become tedious and error prone. More importantly is that additional methods need to be written to add constraints (a where clauses) to a query. Additional methods also have to be written to aggregate (join) two objects together. This results in lots of  SQL statements littered throughout the code, leaving you with a high cost of changing the way that data is persisted.

Simple Mapping

The Mapping class is the most important class in jStorm. The simple example above would be implemented as follows with jStorm.

Mapping mapping = new Mapping();
mapping.setTableName("User");
mapping.setObjectClass(User.class);
mapping.addAttribute("name", "name", String.class);
mapping.addAttribute("passwd", "passwd", String.class);

The Mapping object above tells jStorm how to persist to, and load User's from the database. The addAttribute method takes (at minimum) three parameters:

  1. The name of the field declared in the class.
  2. The name of the column in the table.
  3. The type of the field declared in the class.

A new user could be persisted with the following code.

SqlRunner runner = new SqlRunner();
User user = new User();
user.setName("Joe");
user.setPasswd("Password");
runner.doInsert(user, mapping);

Keys

Most of objects have unique identifiers that consist of one or more keys which are sometimes generated by the database. To do anything useful with our User objects we would probably need them to have a primary key. Lets add it to the User class.

public class User {
	String name;
	String passwd; 
	long userId;
    
	/* Getters and Setters omitted */
}

And lets also add it to the mapping.

mapping.addAttribute("userId", "UserId", Long.TYPE, true);

As you can see, we have added a fourth parameter of 'true'. This tells jStorm that the attribute is a key. If only a single attribute is defined as being a key, jStorm will treat it as a primary key and omit it from any insert statements leaving the database to generate the value. When the object has been inserted the id that was generated will be set back on the object. If more than one attributes are defined as a key, jStorm will treat them as composite keys and include them in the insert statements.

Now that we have a primary key for our User object we can modify an instance of the user and update it in the database.

//assume that user has been read from the databases
user.setName("Joe Bloggs");
runner.doUpdate(user, mapping);

Query Objects

The StorableObject base uses the Template Method pattern to combine a mapping and a object that is to be persisted. It can also be used as a to constrain queries  when doing selects. For example, if we modify user to extend StorableObject and add an attribute of zip code then we ask jStorm to get us all users that reside in a certain zip code.

User queryObject = new User();
queryObject.setZipCode(60601);
Iterator users = runner.doSelect(queryObject);

The above code will return all of the users in the '60601' zip code. Queries will be built using the attributes of the query object that are not null or not zero when the attribute is primitive. This has obvious limitations and can be bypassed by using the overloaded doSelect method that takes a mapping and a where clause in the form of a string.

Aggregation

Aggregation can be used to retrieve two objects from the database at the same time. Lets take the example of our user and add an address.

public class User {
	String name;
	String passwd;
	long userId; 
	Address address;
	long addressId;
	/* Getters and Setters omitted */
}
public class Address {
	long addressId;
	String street;
	String city;
	String zip;
	/* Getters and Setters omitted */
}

Given the two mappings, we can ask jStorm to return us both users and addresses at the same time.

runner.doSelect( addressMapping, userMapping, "Address.AddressId", "User.AddressId" );

jStorm assumes that the object represented by the first mapping is to be set on the represented by the second mapping. Reflection is used to find a setter method on the second object that takes a parameter of the first object. When it does it sets it. In the above scenario all users that have corresponding addresses will be returned.

A simpler form of this is lazy instantiation. This is where the getter method on user for the address would make a second call to the database to load the instance of the address.