Structured Data Management for AJAX Applications, Part 2

Part 1 of this series introduced Stache, a Javascript library for structured data management in AJAX applications. Part 2 describes how to construct data schemas and work with associations, indexing, and constraints. To run the code samples below, you’ll need to include two Javascript files in addition to stache.js: common.js and bplus.js.

As an example scenario, consider a social networking app, which has users, profiles, posts, and ratings. These can be modeled as a Stache schema like this:

var db = new com.anvesaka.stache.Stache({
	id:"db"
});
db.addTableSpec({
	name:"com.anvesaka.stache.user",
	schema:{
		username:{
			type:"string",
			index:true
		},
		posts:{
			type:"set",
			association:"com.anvesaka.stache.post",
			inverse:"author"
		},
		ratings:{
			type:"set",
			association:"com.anvesaka.stache.rating",
			inverse:"rater"
		},
		follows:{
			type:"set",
			association:"com.anvesaka.stache.user"
		},
		friends:{
			type:"set",
			association:"com.anvesaka.stache.user",
			inverse:"friends"
		},
		profile:{
			type:"object",
			association:"com.anvesaka.stache.profile",
			inverse:"user"
		}
	}
});
db.addTableSpec({
	name:"com.anvesaka.stache.profile",
	schema:{
		user:{
			type:"object",
			association:"com.anvesaka.stache.user",
			inverse:"profile"
		},
		name:{
			type:"string"
		},
		email:{
			type:"string"
		}
	}
});
db.addTableSpec({
	name:"com.anvesaka.stache.post",
	schema:{
		title:{
			type:"string",
			index:true,
			unique:false
		},
		content:{
			type:"string",
			unique:false
		},
		author:{
			type:"object",
			association:"com.anvesaka.stache.user",
			index:true,
			unique:false,
			inverse:"posts"
		},
		ratings:{
			type:"set",
			association:"com.anvesaka.stache.rating",
			inverse:"post"
		}
	}
});
db.addTableSpec({
	name:"com.anvesaka.stache.rating",
	schema:{
		value:{
			type:"number",
			index:true,
			unique:false
		},
		post:{
			type:"object",
			association:"com.anvesaka.stache.post",
			index:true,
			unique:false,
			inverse:"ratings"
		},
		rater:{
			type:"object",
			association:"com.anvesaka.stache.user",
			index:true,
			unique:false,
			inverse:"ratings"
		}
	}
});
// Activate the database.
db.activate();

There’s a lot to digest there. The first few lines set up a Stache instance to use as a local “database”. Table specifications are then added to the database, and finally, it’s activated, which causes the table specifications to be transformed into actual tables, with associations, indexes, and constraints as specified in the schema.

Every table specification consists of a name and a schema section. The name should be unique within the scope of the Stache instance to which this table spec belongs.

The schema section is an object whose property names correspond to the property names of the objects that will be stored in the table. The property values of the schema object describe the characteristics of the corresponding properties in the stored objects. So for the schema above, a rating object sent back from the server would contains at least the following:

var rating = {
	value:...,
	post:{
		...
	},
	rater:{
		...
	}
}

For each property, a number of configuration options can be specified (default values in bold):

  • type [object|set|list|number|string|boolean] – string value describing the type of data expected as a value for this property
  • index [true|false] – boolean value describing whether or not the property should be indexed, making it queryable
  • unique [true|false] – boolean value describing whether or not the property must contain a unique value
  • association – a table name, indicating that this property forms an association to entities in the corresponding table
  • inverse – a property name on an associated table, indicating that the association reference should be synchronized during calls to mutator methods

Additionally, each table is automatically augmented with an “id” property, which is a unique, indexed, non-associated, numeric property, and serves as a primary key for the table.

Now with this schema defined, and the Stache instance activated, we can work with actual data. As with the prior post, there is no actual server, data is created inline so the discussion can focus on how to use Stache. Each example code block below is cumulative. First, define some variables we’ll use throughout the examples:

var userTable = db.getTable("com.anvesaka.stache.user");
var profileTable = db.getTable("com.anvesaka.stache.profile");
var postTable = db.getTable("com.anvesaka.stache.post");
var ratingTable = db.getTable("com.anvesaka.stache.rating");

var user;
var profile;
var post;
var rating;

Simple object creation:

var user1 = {
	id:1,
	username:"sbasu"
};
userTable.merge(user1);
user = userTable.get(["ID", 1]);
com.anvesaka.common.assert(user.getUsername()=="sbasu");			
com.anvesaka.common.assert(user.getId()==1);			

Idempotence of object creation:

userTable.merge(user1);
user = userTable.get(["ID", 1]);
com.anvesaka.common.assert(user.getUsername()=="sbasu");			
com.anvesaka.common.assert(user.getId()==1);			

Demonstration of uniqueness constraints:

var user2 = {
	id:2,
	username:"sbasu"
};
try {
	userTable.merge(user2);
}
catch (e) {
	com.anvesaka.common.log(com.anvesaka.common.LOG_WARN, e.message);			
}
user2 = {
	id:2,
	username:"bledbetter"
};
userTable.merge(user2);
user = userTable.get(["ID", 2]);
com.anvesaka.common.assert(com.anvesaka.common.isDefined(user));			
com.anvesaka.common.assert(user.getId()==2);			
com.anvesaka.common.assert(user.getUsername()=="bledbetter");			

Implicit associations initialized as part of object creation:

var profile1 = {
	id:1,
	name:"Santanu Basu",
	email:"santanu.basu@email.com",
	user:user1
}
var profile2 = {
	id:2,
	name:"Brain Ledbetter",
	email:"brian.ledbetter@email.com",
	user:user2
}
profileTable.merge([profile1, profile2]);
com.anvesaka.common.assert(profile1.getUser().getId()==user1.getId());			
com.anvesaka.common.assert(user1.getProfile().getId()==profile1.getId());			
com.anvesaka.common.assert(profile2.getUser().getId()==user2.getId());			
com.anvesaka.common.assert(user2.getProfile().getId()==profile2.getId());			

Referential equivalence:

profile = profileTable.get(["ID", 2]);
profile.setEmail("brian.ledbetter@beardhat.com");
com.anvesaka.common.assert(profile2.getEmail()=="brian.ledbetter@beardhat.com");			

Merging of object graphs, with implied associations:

var user3 = {
	id:3,
	username:"sabrams",
	profile:{
		id:3,
		name:"Steve Abrams",
		email:"steve.abrams@email.com"
	}
};
var user4 = {
	id:4,
	username:"mberlan",
	profile:{
		id:4,
		name:"Mike Berlan",
		email:"mike.berlan@email.com"
	}
};
userTable.merge([user3, user4]);
profile = profileTable.get(["ID", 3]);			
com.anvesaka.common.assert(com.anvesaka.common.isDefined(profile));			
com.anvesaka.common.assert(profile.getUser().getId()==user3.getId());			
com.anvesaka.common.assert(user3.getProfile().getId()==profile.getId());			

Nested object modification:

userTable.merge({
	id:4,
	profile:{
		id:4,
		email:"mike.berlan@beardhat.com"
	}
});
profile = profileTable.get(["ID", 4]);
com.anvesaka.common.assert(profile.getEmail()=="mike.berlan@beardhat.com");			

Unidirectional self association:

user1.addFollows([user2, user3]);
com.anvesaka.common.assert(user1.getFollows().length==2);
com.anvesaka.common.assert(user2.getFollows().length==0);

Bidirectional self association:

user2.addFriends([user3, user4]);
com.anvesaka.common.assert(user2.getFriends().length==2);
com.anvesaka.common.assert(user3.getFriends().length==1);
com.anvesaka.common.assert(user4.getFriends().length==1);

Idempotence of set associations:

user2.addFriends([user3, user4]);
com.anvesaka.common.assert(user2.getFriends().length==2);
com.anvesaka.common.assert(user3.getFriends().length==1);
com.anvesaka.common.assert(user4.getFriends().length==1);

Implicit set associations:

user4.addPosts([{
	id:1,
	title:"Post 1",
	content:"Content 1",
}, 
{
	id:2,
	title:"Post 2",
	content:"Content 2",
},
{
	id:3,
	title:"Post 3",
	content:"Content 3",
}]);
com.anvesaka.common.assert(user4.getPosts().length==3);
post = postTable.get(["ID", 3]);
com.anvesaka.common.assert(post.getAuthor().getId()==user4.getId());
com.anvesaka.common.assert(post.getTitle()=="Post 3");

Object deletion, with implicit set dissociation:

postTable.remove(post);
post = postTable.get(["ID", 3]);
com.anvesaka.common.assert(com.anvesaka.common.isNotDefined(post));
com.anvesaka.common.assert(user4.getPosts().length==2);

Create some numeric ratings:

var post1 = postTable.get(["ID", 1]);
var post2 = postTable.get(["ID", 2]);
ratingTable.merge({
	id:1,
	value:1.0,
	rater:user1,
	post:post1
});
ratingTable.merge({
	id:2,
	value:2.0,
	rater:user2,
	post:post1
});
ratingTable.merge({
	id:3,
	value:3.0,
	rater:user3,
	post:post1
});
ratingTable.merge({
	id:4,
	value:4.0,
	rater:user4,
	post:post1
});
ratingTable.merge({
	id:5,
	value:3.0,
	rater:user4,
	post:post2
});

Criteria queries:

var ratings = ratingTable.list(["RANGE", "value", 0.0, 2.5]);
com.anvesaka.common.assert(ratings.length==2);
com.anvesaka.common.assert(ratings[0].getId()==1);
com.anvesaka.common.assert(ratings[1].getId()==2);
ratings = ratingTable.list(["AND", ["RANGE", "value", 0.0, 3.5], ["EQ", "rater", user4.getId()]]);
com.anvesaka.common.assert(ratings.length==1);
com.anvesaka.common.assert(ratings[0].getId()==5);

These examples illustrate Stache syntax and usage patterns. In a future post, topics like performance, best practices, and common pitfalls will be discussed.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: