Tutorial
In our tutorial we will write a simple quote processing application. We would like to load them into the database and make some reporting on them.
First define two classes – one abstract base class and Quote class.
public abstract class PersistentObject implements DatastoreIdentityEntity { private Object oid; public Object getOid() { return oid; } public void setOid(Object oid) { this.oid = oid; } } public class Quote extends PersistentObject { protected String ticker; protected Date date; protected double start; protected double high; protected double low; protected double end; protected double volume; // getters and setters omitted }
Persistent classes must have some form of identity. For now datastore and nondurable identities are supported. In the former case an object instance identifier (oid for short) is generated by the datastore and remembered in an instance field. In the latter case an instance identifier is also generated by the datastore but the mapping between oid the object instance is temporary and if the instance becomes detached (ie. by use of serialization/deserialization) from the session, its identity is lost. Nondurable identity does not require an extra instance field. In our example we use datastore identity.
Database is initialized by set of properties. In fact they have reasonable defaults so only a few of them are really necessary to set. Properties may be set directly in the code, read from file, read from input stream or system properties. Here we set the database file name and obtain an object sesssion, which is the main entry point for object persistence.
JalistoProperties properties = JalistoFactory.createJalistoProperties(); properties.setDbBaseDirectory(BASE_DIR); properties.setDbFilesPaths("tutorial.fdb"); ObjectSession session = properties.getObjectSession(); session.openSession();
Now we have to inform the database about our persistent class. The simplest form is:
session.defineClass(Quote.class);
During above method call the database builds the class metadata (its identity type, fields and their types). By default it includes all non static, non transient fields. The identity type is determined by the implemented interface. This behaviour may be somewhat altered by annotations (ie. cascading) but here the default behaviour is enough for us. No transaction may be active during call of define method.
We would like to remember some quotes in the database. Let's read them from a csv file having the line format:
"ticker", "start value", "high value", "low value", "end value", "volume"
We will use functions below:
public static void loadQuotesFromFile(ObjectSession session, File file) { Transaction tx = session.currentTransaction(); tx.begin(); try { InputStream is = new FileInputStream(file); Reader r = new InputStreamReader(is); BufferedReader br = new BufferedReader(r); String line; while((line = br.readLine()) != null) { if(line.trim().length() > 0) { // ticker,date,start,high,low,end,volume // cut the line into comma separated pieces ArrayListl = new ArrayList (); int idx = 0, idx2; while(true) { idx2 = line.indexOf(",", idx); if(idx2 >= 0) { l.add(line.substring(idx, idx2).trim()); idx = idx2+1; } else { l.add(line.substring(idx).trim()); break; } } if(l.size() != 7) throw new RuntimeException("unrecognized line format: " + line); String date = l.get(1); // date has format YYYYMMDD, adjust it to YYYY-MM-DD if(date.length() != 8) throw new RuntimeException("unrecognized line format: " + line); date = date.substring(0, 4) + "-" + date.substring(4, 6) + "-" + date.substring(6); l.set(1, date); createOrUpdateQuote(session, l.get(0), l.get(1), l.get(2), l.get(3), l.get(4), l.get(5), l.get(6)); } } // tx.commit(); } catch(RuntimeException e) { tx.rollback(); throw e; } catch(Exception e) { tx.rollback(); throw new RuntimeException(e); } } public static void createOrUpdateQuote(ObjectSession session, String ticker, String date, String start, String high, String low, String end, String volume) { Quote q = queryQuote(session, ticker, date); if(q != null) { println("updating quote: " + ticker + " " + date); q.setStart(Double.parseDouble(start)); q.setHigh(Double.parseDouble(high)); q.setLow(Double.parseDouble(low)); q.setEnd(Double.parseDouble(end)); q.setVolume(Double.parseDouble(volume)); session.updateEntity(q); } else { println("creating quote: " + ticker + " " + date); q = new Quote(); q.setTicker(ticker); q.setDate(Date.valueOf(date)); q.setStart(Double.parseDouble(start)); q.setHigh(Double.parseDouble(high)); q.setLow(Double.parseDouble(low)); q.setEnd(Double.parseDouble(end)); q.setVolume(Double.parseDouble(volume)); session.createEntity(q); } } public static Quote queryQuote(ObjectSession session, String ticker, String date) { Query q = session.getQueryManager().query(); q.constrain(Quote.class); q.descend("ticker").constrain(ticker).equal().and(q.descend("date").constrain(date).equal()); ObjectSet result = q.execute(); return result.size() >= 1 ? (Quote) result.next() : null; }
You can see that we use three standard persistence mechanisms in the above functions. First we query a quote by key field values. If it exists we update it, otherwise we create and persist a new quote. Note that processing must be enclosed into transactional boundaries.
If larger number of quotes are read the process becomes slower and slower. That is because querying whether a quote exists takes time proportional to number of quotes. In such a situation indexes may help. Let's create indexes on key fields:
public static void buildQueryIndex(ObjectSession session) { Transaction tx = session.currentTransaction(); tx.begin(); ClassDescription quoteMeta = session.getMetaRepository().getMetaData(Quote.class.getName()); session.getQueryManager().getIndexManager().buildIndexOnField(quoteMeta, quoteMeta.getIndex("ticker")); session.getQueryManager().getIndexManager().buildIndexOnField(quoteMeta, quoteMeta.getIndex("date")); tx.commit(); }
After index creation the database uses it during querying so it becomes much faster.
These tickers read might be quite cryptic. It would be better if we have some description of a ticker. Let's make another class:
public class TickerInfo extends PersistentObject { protected String ticker; protected String description; public String getTicker() { return ticker; } public void setTicker(String ticker) { this.ticker = ticker; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
and modify existing Quote class, adding a field
protected TickerInfo tickerInfo;
It's almost done but the database is unaware of the new class and still remembers the old version of Quote class – without extra field. We have to inform the database about it.
session.defineClass(TickerInfo.class); session.getMetaRepository().addField(session.getSession(), Quote.class, "tickerInfo");
If we have some ticker descriptions in a text file we could write a function similar to quote loading function in order to read it automatically. But in our example we simply add them directly using the functions below:
public static void insertTickers(ObjectSession session) { Transaction tx = session.currentTransaction(); tx.begin(); try { createOrUpdateTicker(session, "RBCE", "Robeco Chinese Equities (EUR)"); createOrUpdateTicker(session, "RBGB", "Robeco Global Bonds (EUR)"); // tx.commit(); } catch(RuntimeException e) { tx.rollback(); throw e; } } public static void createOrUpdateTicker(ObjectSession session, String ticker, String description) { TickerInfo ti = queryTicker(session, ticker); if(ti != null) { println("updating ticker: " + ticker); ti.setDescription(description); session.updateEntity(ti); } else { println("creating ticker: " + ticker); ti = new TickerInfo(); ti.setTicker(ticker); ti.setDescription(description); session.createEntity(ti); } } public static TickerInfo queryTicker(ObjectSession session, String ticker) { Query q = session.getQueryManager().query(); q.constrain(TickerInfo.class); q.descend("ticker").constrain(ticker).equal(); ObjectSet result = q.execute(); return result.size() >= 1 ? (TickerInfo) result.next() : null; }
Now let's write a function that matches originally read quotes with their descriptions
public static void connectQuotesWithTickers(ObjectSession session) { Transaction tx = session.currentTransaction(); tx.begin(); try { Query q = session.getQueryManager().query(); q.constrain(Quote.class); q.descend("tickerInfo").constrain(null).equal(); ObjectSet result = q.execute(); while(result.hasNext()) { Quote qq = (Quote) result.next(); TickerInfo ti = queryTicker(session, qq.getTicker()); if(ti != null) { qq.setTickerInfo(ti); session.updateEntity(qq); } } // tx.commit(); } catch(RuntimeException e) { tx.rollback(); throw e; } }
We would like some form of reporting too. Most often a specialized tool like jasperreports is used for reporting. But in our example we will use the simplest form of reporting, that is printing on the console. We would like to see day end quotes for some security in a given period of time
public static void reportQuotes(ObjectSession session, String ticker, String dateFrom, String dateTo) { Transaction tx = session.currentTransaction(); tx.begin(); try { println("== REPORT START =="); println(ticker + " from: " + dateFrom + " to: " + dateTo); Query q = session.getQueryManager().query(); q.constrain(Quote.class); q.descend("ticker").constrain(ticker).equal().and( q.descend("date").constrain(dateFrom).greaterEqual()).and( q.descend("date").constrain(dateTo).smallerEqual()); ObjectSet result = q.execute(); result.sort(new QuoteComparator()); while(result.hasNext()) { Quote qq = (Quote) result.next(); println("date: " + qq.getDate() + ", end value: " + qq.getEnd() + (qq.getTickerInfo() != null ? ", ticker description: " + qq.getTickerInfo().getDescription() : ", ticker: " + qq.getTicker() + " (no description)")); } println("== REPORT END =="); // tx.commit(); } catch(RuntimeException e) { tx.rollback(); throw e; } }
Full source code of this example is available in the distribution in samples subdirectory.