Monday, August 25, 2008

OR-Mapper for the HTML5 database and the Google Gears database

Last week a guy on the Joose mailing list asked whether the example object relational mapper included with Joose could be used to build iPhone web application using the webkit implementation of the HTML5 database API. The immediate answer was that it wasn't possible because the OR mapper was designed to work with the Google Gears database which is currently not compantible with HTML5.

So I did two thinks
  • I implemented a wrapper for the Gears API that makes it look like the HTML5 database API (There was already one linked from Ajaxian but all the download links were dead)
  • I ported the existing OR mapper to use the new API
This means that the OR mapper can be used with the Gears db and the HTML5 db without any changes.

The HTML5 database API in inherently asynchronous. The reason being that databases requests should not block script execution and thus decrease the responsiveness of the GUI.
I, personally, don't like the API because thinks start to look a lot like degenerated LISP once you get down to the 16th callback level, but, I guess, performance was more important than beauty for the API designers.

The primary change with respect to the synchronous Gears version is that now all operations that talk to the database like Entity.save() or Entity.newFromId() take an extra parameter that is a callback function which gets executed as soon as the db operation is completed.

Here are some examples from the test suite of the OR-Mapper.

Declaring an entity looks like this (A Person that has a mother and many cars):
    Class("Person", {
isa: ORM.Entity,

classMethods: {
tableName: function () {
return "person"
}
},

has: {
mother: {
metaclass: ORM.HasOne,
isa: function () { return m.Person }
},

cars: {
metaclass: ORM.HasMany,
isa: function () { return m.Car },
foreignKey: "owner"
}
}
});
And using the classes looks like this (I warned you about the LISP effect):

/*
ok, isEq, canOk, etc. are functions from the unit testing framework that I use
*/

ORM.transaction(function (tx) {

var mother = new MyEntities.Person();

mother.setName("elke");
mother.setCity("Elmshorn");
ok(mother.isNewEntity, "... mother is a new entity object")

function testPerson (person) {
canOk(person, "getRowid")
canOk(person, "getName", "... there is a name method")
isEq(person.getName(), "malte", "... name is correct")
isEq(person.getCity(), "Hamburg", "... city is correct")
person.getMother(function (mother) {
ok(mother, "... can fetch mother")
isEq(mother.getName(), "elke", "... mother name is correct")
isEq(mother.getCity(), "Elmshorn", "... mother city is correct")
})


ok(person.getCars, "... there is a cars method")
person.getCars(function (cars) {
for(var i = 0; i < 10; i++) {
canOk(cars[i], "getBrand")
isEq(cars[i].getBrand(), "bmw", "... brand is correct");
isEq(cars[i].getModel(), "3."+i, "... model is correct");
canOk(cars[i], "getOwner")
cars[i].getOwner(function (owner) {isEq(owner.getName(), "malte", "... owner name is correct")})
}
var count = cars.length
ok(count == 10, "... there are only 10 cars: "+count)
});
}

mother.save(function () {
ok(!mother.isNewEntity, "... mother is now saved")

var person = new MyEntities.Person();

person.setName("malte");
person.setCity("Hamburg");
person.setMother(mother)
person.save(function () {

var i = 0;
function makeCar() {
var car = new MyEntities.Car();
car.setModel("3."+i);
car.setBrand("bmw");
car.setOwner(person);
car.save(function () {
if(++i < 10) {
makeCar()
} else {
testPerson(person)
MyEntities.Person.newFromId(person.getRowid(), function (aPerson) {
testPerson(aPerson)
endTests()
})

}
})
}

makeCar()
})})})

If you have either Google Gears installed or are using a web browser that implements the HTML5 database like Safari (and other browsers based on webkit), you can run the testsuite yourself.

For more info, check out the source:
The code is currently extremely alpha and missing features. If people show interest I'll push it to production quality.
Post a Comment