Real Android apps leveraging db4o persistence engine (Part 1)
Join the DZone community and get the full member experience.
Join For FreeThis the first
delivery in a series of articles targeted at showing developers how db4o
(an open source database that leverages today's object-oriented
languages, systems, and mindset) is being used in several Android
projects to avoid all the pitfalls and hassles of object-relational
mapping while benefiting from an elegant and straight forward way to
evolve a domain model which, in the end, translates into faster, easier
upgrades for users.
There are many benefits to using an object
database like db4o, including easier code maintenance, and the ability
to create applications based on more complex data models. Unlike in
rigid, predefined SQL tables, you can store dynamic, free-form data,
which can be changed or amended any time. In addition, db4o allows for
data replication, another missing element in Android's software stack.
Let's take a look at
the code in these projects to learn how developers leverage object
database technology on their apps and also use the opportunity to
introduce key concepts about db4o. Let's start with project DyCaPo.
DyCaPo stands for
“Dynamic Car Pooling”, a system that facilitates the ability of drivers
and passengers to make one-time ride matches close to their departure
time, with sufficient convenience and flexibility to be used on a daily
basis. The project is the result of research activities on the adoption
of a FREE/OPEN Dynamic Carpooling system in the province of Trento,
Italy.
Riccardo Buttarelli,
chose db4o as the persistence engine in the client application for the
DyCaPo Service running on Android OS (aka dycadroid). If you check
dycadroid's db4o configuration:
private static Configuration configure(){ dbConfiguration = Db4o.newConfiguration(); dbConfiguration.objectClass(Trip.class).objectField(Trip.ID).indexed(true); dbConfiguration.objectClass(Trip.class).cascadeOnUpdate(true); dbConfiguration.objectClass(Trip.class).cascadeOnDelete(true); dbConfiguration.objectClass(Location.class).objectField(Location.GEORSSPNT).indexed(true); dbConfiguration.objectClass(Location.class).cascadeOnDelete(true); dbConfiguration.objectClass(Location.class).cascadeOnUpdate(true); dbConfiguration.objectClass(Person.class).objectField(Person.USERNAME).indexed(true); dbConfiguration.objectClass(Person.class).cascadeOnUpdate(true); dbConfiguration.objectClass(Person.class).cascadeOnDelete(true); //[...] dbConfiguration.objectClass(ActiveTrip.class).objectField(ActiveTrip.ID).indexed(true); dbConfiguration.objectClass(ActiveTrip.class).cascadeOnUpdate(true); dbConfiguration.objectClass(ActiveTrip.class).cascadeOnDelete(true); dbConfiguration.objectClass(Route.class).cascadeOnUpdate(true); dbConfiguration.objectClass(Route.class).cascadeOnDelete(true); dbConfiguration.lockDatabaseFile(false); dbConfiguration.messageLevel(2); return dbConfiguration; }you'll see that he does a heavy use of cascading on updates and deletes in the db4o configuration for several classes. When you use cascading in db4o you can save a lot of time because you're basically telling the database to apply an operation on your object considering all the objects that are referenced from it (object tree). For example, when you delete an ActiveTrip object you'll probably want to also delete all the Route objects that are referenced from it. In order to achieve this functionality automatically you’ll just have to enable cascaded deletes for that class:
dbConfiguration.objectClass(ActiveTrip.class).cascadeOnDelete(true);and forget about managing deletion of referenced objects!
Cascaded deletes can be applied to all members of an object or can be limited to specific fields.
On the other hand, if you changed an ActiveTrip object tree you also probably want to reflect the changes on the associated Routes automatically (that are referenced from the ActiveTrip object). It's quite easy to achieve that by enabling cascaded updates on the class
dbConfiguration.objectClass(ActiveTrip.class).cascadeOnUpdate(true);this way changed Route objects will also be updated on the database when you update an ActiveTrip and you won't have to traverse all the referenced objects to look for updated data (let db4o handle that!)
As you might have guessed already the trade-off of a cascading-heavy configuration is database performance: if you're dealing with deep object structures you might want to exercise a more granular control during updates (ie. manually set the update depth). By default db4o uses an update depth of 1 meaning that only primitives values in the object will be updated (not the changes in referenced objects). You can set the update depth to a fixed value of your choice so stores() can go as deep as you want or you can just use cascaded updates all the object tree will be traversed looking for changes.
If you want full automation on database operation you can try the Transparent Persistance framework. Basically this is a way to make your object fully database-aware. You store the object once in the database. After that, all changes you make on the object are reflected in the database ‘by magic’. You can implement this manually or use byte-code-enhancers.
If you take a closer look at the configuration code above you’ll also notice that some fields in specific classes are configured as “indexed”:
dbConfiguration.objectClass(ActiveTrip.class).objectField(ActiveTrip.ID).indexed(true);Indexing works like in the relational world and will give you a faster access when you're querying over that field. db4o automatically uses indexes for queries if they are present. Lots of complaints about slow queries have to do with the omission of indexes in the configuration.
We saw some tips related to updates and deletes but what about fetching objects? The most important concept of object retrieval is the concept of “activation” and has many similarities with updates and deletes in terms of cascading. Suppose I want to load an ActiveTrip from the database, shall I also get all the associated Routes in the same operation? What if I just need to know the value of an int field in the ActiveTrip but Routes are irrelevant to me for this operation?
db4o allows you to handle how deep in the object tree you want to go when loading an object (aka “activation depth”). Same as before, via the configuration you can opt to activate everything when you fetch the parent object via cascading:
dbConfiguration.objectClass(ActiveTrip.class).cascadeOnActivate(true);or if you want more control you can go with a manual activation (ObjectContainer#activate(object, depth);) or let db4o handle everything via the transparent activation framework which activates object trees from the database on demand (ie. as object members are accessed).
Right in the end of the configuration code you’ll find the line:
dbConfiguration.lockDatabaseFile(false);which is listed as our number one dangerous practice! During normal operation, and if you don't use the configuration option above, db4o locks the database file to prevent concurrent access which could leave you with a corrupted database. If you find yourself struggling with constant DatabaseFileLockedExceptions that's a clear sign that you need to change your database access pattern: if you need multiple concurrent transactions you can try db4o’s embedded client server mode of operation.
So, we've seen the db4o configuration options in dycadroid but how difficult are the standard upsert, delete, query operations? Actually extremely easy!
The beauty of db4o is that you don't have to do any kind of processing to your object in order to make it persistent (ie. no mapping). You just pass the full object as a parameter to the store() method of a db4o’s object container and voila it's saved (and also the sibling objects depending on your configuration). The store() operation acts as an insert if the passed object is new (ie. was never stored in the db during the current transaction) or as an update if the object was previously saved during the same session (hence we could call it an ”upsert” operation):
public static void saveActiveTrip (ActiveTrip trip){ Log.d(TAG, "saving ActiveTrip from trip"); ObjectContainer db = DBProvider.getDatabase(); //[...] db.store(trip); db.commit(); }Note the commit() in the last line above. As any decent transactional database db4o supports commit and rollback operations during transactions. Transaction semantics is implicit to match db4o's simplicity. In the code above a transaction is started when the object container is opened (eg. openFile() inside getDatabase()) and ends when the object container is closed (db.close()). Along the same lines of simplicity a close() operation will force a commit().
Important: after a close() operation in an object
container all the objects that where saved to db4o will be seen as new
objects when you open a new object container (for more information see db4o's unique
identity concept).
Similarly when you want to delete an object from the database
you also interact with a db4o object container but this time it's delete() that you must use.
Same rules apply with regards to object identity: in order for delete to
work you must have stored or fetched the object passed as parameter
during the same transaction otherwise db4o won’t be able to tell that
the object belongs to the database and the operation will be
ineffective. The delete operation applies to
the passed object only and does not delete the siblings except if you
configured cascaded deletes (see above).
How about
querying? Querying with db4o can be as easy as issuing one line of
code. You have the choice of 3 different query mechanisms:
- Native Queries: refactorable query interface that is very close to the language. The queries are optimized and converted to SODA queries behind the curtains. In db4o for .NET a LINQ provider is also provided and is the preferred method for querying. Since Dalvik (Android VM) uses it’s own binary format db4o’s native query optimizer won’t work during runtime. If you want to optimize NQs on Android you’ll have to use db4o’s build time native query enhancer.
- SODA Queries: low level and powerful graph based query system where you basically build a query tree and pass it to db4o for execution.
- Query by Example: useful for simple queries where you pass a prototype object that will serve as an example to find similar objects. Uses reflection to analyze the passed prototype object and build the query.
Here's an example (also from dycadroid) that shows a delete plus a simple query using “query by example”:
public static boolean deletePrefs(){ try{ ObjectContainer db = DBProvider.getDatabase(); ObjectSet prefs = db.queryByExample(new Preferences()); db.delete(prefs); db.commit(); return true; }catch (Exception e){ return false; } }Note that in the example query by example is used to retrieve all instances of class Preferences:
db.queryByExample(new Preferences());but you could also pass a prototypical instance with some fields filled in to further constraint the query:
db.queryByExample(new Preferences("trip", null, null));which will retrieve all objects of class Preferences where the type is ”trip” and won't pay attention to the remaining values in the constructor (will match anything there).
Well, I hope you have enjoyed db4o's simplicity by taking a look at parts of a real project. On the next article in the series we'll take a look at QuiteSleep another Android app that is available in the Android Market that leverages db4o for all persistence needs.
Opinions expressed by DZone contributors are their own.
Comments