(Quick Reference)

Cassandra Object Persistence Framework - Reference Documentation

Authors: Bob Florian

Version: 0.2.6

1 Cassandra ORM Plugin

The Cassandra ORM Plugin provides GORM-like dynamic methods for persisting Groovy objects into Cassandra. The plugin does not implement the GORM API but generally follows the same syntax. It also includes additional methods and options that specifically suited to Cassandra. In addition to the dynamic persistence methods, the plugin extends java.util.UUID with methods to generate and use TimeUUIDs, which often server as primary keys in Cassandra.

1.1 Getting Started

The cassandra-orm plugin must be used in conjunction with a compatible Cassandra client plugin. At this time there is only one such plugin, cassandra-astyanx, which uses the Netflix Astyanax Cassandra client. Because of a conflict between the Astyanax client and Grails, its recommended that you do this in BuildConfig.groovy

compile ()">> {
    excludes "commons-logging"
    excludes 'slf4j-log4j12'
}
runtime 
">>

See the cassandra-astyanax plugin for information about configuring Grails to connect to the Cassandra cluster(s).

Create a directory named grails-app/cassandra and put the classes you wish to persist in Cassandra there. The plugin will automatically add dynamic persistence methods to any class that has a cassandraMapping static property. A simple pair of domain classes might be:

class Person
{
	String username
	String emailAddress
	String firstName
	String lastName
	String country

List posts static hasMany = [posts: Post]

static cassandraMapping = [ primaryKey: 'username', explicitIndexes: ["emailAddress", "country"], counters: [[groupBy: "country"]] ] }

class Post
{
	UUID uuid
	String title
	String text
    Date occurTime

Person user static belongsTo = [user: Person]

static cassandraMapping = [ primaryKey: 'uuid', counters: [ [groupBy: ['occurTime']], [findBy: ['user'], groupBy:['occurTime']] ] ] }

You will need to create column families for each of these classes (there is currently no automatic creation of column families but its on the road-map). The column family names are derived from the class names:

create column family Person with comparator=UTF8Type and default_validation_class=UTF8Type;
create column family Person_IDX with comparator=UTF8Type and default_validation_class=UTF8Type;
create column family Person_CTR with comparator=UTF8Type and default_validation_class=CounterColumnType;

create column family Post with comparator=UTF8Type and default_validation_class=UTF8Type; create column family Post_IDX with comparator=UTF8Type and default_validation_class=UTF8Type; create column family Post_CTR with comparator=UTF8Type and default_validation_class=CounterColumnType;

To create a new user:

new Person(username: "jdoe", emailAddress: "jdoe@anywhere.com", firstName: "John", lastName: "Doe", country: "USA").save()

There are a number of ways to read back your user record(s):

def user1 = Person.get("jdoe")
def user2 = Person.findByEmailAddress("jdoe@anywhere.com")
def users = Person.findAllByCountry("USA")

To add some posts to that user:

user.addToPosts(new Post(title: "Post 1", text: "My first post", occurTime: new Date()-45))
user.addToPosts(new Post(title: "Post 2", text: "My second post", occurTime: new Date()-32))
user.addToPosts(new Post(title: "Post 3", text: "My third post", occurTime: new Date()-15))
user.addToPosts(new Post(title: "Post 4", text: "My fourth post", occurTime: new Date()-10))
user.addToPosts(new Post(title: "Post 5", text: "My fifth post", occurTime: new Date()))

To access the posts for a user:

user.posts.each {println it.title}

To access the last 3 posts in reverse order (UUID keys default to time-based):

user.posts(reversed: true, max: 3).each {println it.title}

To only read the titles from Cassandra (rather than whole objects):

user.posts(column: "title").each {println it}

To get the number of posts a user has:

user.postsCount()

To display the number of users per country:

Person.getCountsGroupByCountry()

To display the number of posts per time period (hour, day, month):

Post.getCountsGroupByOccurTime()
Post.getCountsGroupByOccurTime(grain: Calendar.DAY_OF_MONTH)
Post.getCountsGroupByOccurTime(grain: Calendar.MONTH)

To display the user for each post:

Post.list().each {
     println "${it.title} -- ${it.user.firstName} ${it.user.lastName}"
}

1.2 TimeUUID Methods

The cassandra-orm plugin adds the following methods related to UUID.

java.lang.Integer

byte[] getBytes()

Returns the integer value as an array of 4 bytes.

10000.bytes  // returns [0, 0, 39, 16]

java.lang.Long

byte[] getBytes()

Returns the long value as an array of 4 bytes.

100000000000.bytes  // returns [0, 0, 0, 23, 72, 118, -24, 0]

java.util.UUID

static UUID timeUUID()

Generates a time-based UUID from the current clock time.

def uuid = UUID.timeUUID()

static UUID timeUUID(long msec)

Generates a time-based UUID from the specified time in milliseconds since 1 Jan 1970 UTC (i.e. System.currentTimeMillis()).

def uuid = UUID.timeUUID(System.currentTimeMillis() - 3600000)

static UUID fromBytes(byte[] bytes)

Constructs a UUID from the specified array of 16 bytes.

UUID.fromBytes([-121, -54, -108, 96, -7, -37, 17, -31, -73, -1, 0, 28, 66, 0, 0, 9])

byte[] getBytes()

Returns a 16 byte array representation of the UUID.

UUID.timeUUID().bytes

String toUrlSafeString()

Returns a Base64 encoded string representation of the UUID. These are useful for generating hashes.

UUID.timeUUID().toUrlSafeString()  // something like "h82hoPnbEeG3_wAcQgAACQ"

Long getTime()

Returns the time component of a time-based UUID in milliseconds since 1 Jan 1970 UTC.

UUID.timeUUID().time

1.3 Configuring Domain Classes

2 Key Concepts

Classes are configured for persistence in Cassandra by adding a static Map property named cassandraMapping that specifies where and how the data is stored and retrieved.

2.1 Primary Keys

Unlike GORM, the cassamdra-orm implementation does not automatically generate an id property for each object. Instead, it requires that you specify one or more properties of the object to be used in generating the id, which is used as the Cassandra row key for the object. In the typical (and simplest) case, the value is the name of one property, for example:

class Person {
    String emailAddress
    …
    static cassandraMapping = [primaryKey: 'emailAddress']
}

The primary key can also be a composite of multiple properties:

class Person {
    String emailAddress
    String firstName
    …
    static cassandraMapping = [primaryKey: ['emailAddress','firstName']]
}

In cases where no suitable natural keys are available, UUIDs are typically used for the primary key:

class BlogPost {
    UUID uuid
    …
    static cassandraMapping = [primaryKey: 'uuid']
}

Note that, in this special case of a UUID as primary key, you don't need to specify the value when constructing the object. If you leave it out, the system will automatically generate a time-based UUID the first time the object is saved. In all other cases you must specify a non-null value for all primary key properties before saving the object.

The natural order of the primary key determines the order of results returned by list() and findBy... methods (which is why time-based UUID keys are often so handy).

In applications where you will be storing a very large number of objects (i.e. hundreds of millions or more) you may want to specify an un-indexed primary key:

class PaveView {
    UUID uuid
    …
    static cassandraMapping = [unindexedPrimaryKey: 'uuid']
}

This options avoids the scaling problems of a very large primary key row in the index table (see Data Structures), but means that the list() method is not available. However, you can create other indexes on the object to use in queries.

2.2 Secondary Indexes

Cassandra has the concept of secondary indexes, which must be declared in the schema definition of the column families. If you also declare these indexes in the cassandraMapping Map you can query against these indexes with a GORM-like syntax. For example, with the schema defintion:

create column family Person
    with comparator=UTF8Type
    and default_validation_class=UTF8Type
    and column_metadata=[
        {column_name: gender, validation_class: UTF8Type, index_type: KEYS},
        {column_name: country, validation_class: UTF8Type, index_type: KEYS},
        {column_name: state, validation_class: UTF8Type, index_type: KEYS}
    ];

and the cassandraMapping definition:

class Person
{
    String username
    String state
    String country
    String gender

static cassandraMapping = [ primaryKey: 'username', secondaryIndexes: ["gender","country", "state"], ] }

you can query for objects using GORM-like finders:

Person.findAllByGender("Female")
Person.findAllByCountryAndState("USA","New York")
Person.findAllWhere(country: "USA", state: "Maine", gender: "Male")

or by specifying a CQL where expression:

Person.executeQuery("select * from Person where country = ? and state = ? limit 3", ["USA","Maryland"])

2.3 Explicit Indexes

Cassandra indexes can have undesireable performance characteristics in some cases, such as high cardinality indexes. For example, creating an index on the phone number, where there might be many millions of different numbers. For this reason the cassandra-orm plugin provides the ability to declare explicit indexes, which are implemented as rows in a companion column family. Explicit indexes can be queried using the same GORM-like syntax as secondary indexes, but they do not support query expressions. Each explicit index references one or more properties of the object. For example, with the following class definition:

class Person
{
    String username
    String emailAddress
    String phone
    String city
    String state

static cassandraMapping = [ primaryKey: 'username', explicitIndexes: ["emailAddress", "phone", "state", ["state","city"]], ] }

you can query for objects using findBy syntax:

Person.findByPhone("+13015551212")
Person.findAllByState("Maryland")
Person.findAllByCityAndState("Baltimore","Maryland")

or findWhere syntax:

Person.findWhere(emailAddress: "jdoe@anywhere.com")
Person.findAllWhere(state: "Virginia", city: "Reston")

Note that you cannot mix properties from different indexes, e.g. in this example the query Person.findByPhoneAndState() is not valid. To be able to run that query you would need to declare a "phone","city" explicit index. However the order of the properties is not important. That is, you can say findAllByCityAndState() or findAllByStateAndCity(). Explicit indexes support only the quality operator, i.e. you can not use method names containing phrases like LessThan, GreaterThan, NotEqual, Null, etc. However you can query objects matching one of a collection of values. For example:

Person.findAllByStateAndCity("Maryland",["Baltimore","Annapolis","Frederick"])

You can also count using explicit indexes:

Person.countByState("California")
Person.countByCityAndState("San Francisco","California")

Counts done in this way perform better than querying for an returning entire objects, but they are not constant time operations, i.e. performance will depend on the number of rows.

2.4 Counters

Cassandra provides atomic counters that are useful for aggregating data about the numbers of objects created. The cassandra-orm plugin to provide a very efficient capability for generating counts based on one more more object properties. Each counter declaration consists of two elements:

NameDefinition
findByZero or more property names used for filtering counts based on property values. All names in the findBy element must be specified in the getCounts query. This element can be omitted from the counter definition if no filtering is desired
groupByOne or more property names used for aggregating counts. At least one of these names must be provided in the getCounts query. Properties in listed in this element can also be used as filters.

For example, consider the following class used for tracking website visits:

class Visit
{
    UUID uuid
    String siteName
    String referrerType
    String referrerName
    Date occurTime

static cassandraMapping = [ primaryKey: 'uuid', counters: [ [groupBy: ['siteName']], [groupBy: ['occurTime']], [groupBy: ['occurTime','siteName']], [findBy: ['siteName'], groupBy: ['occurTime']], [findBy: ['siteName'], groupBy: ['occurTime','referrerType','referrerName']], ] ] }

You can query for total number of page views for each site with either of these statements:

Visit.getCounts(groupBy: 'siteName')
Visit.getCountsGroupBySiteName()

which produces a Map of results of the form:

[www.siteone.com:18, www.sitetwo.com:26]

Grouping by a time-based dimension

The previous query makes use of the [groupBy: 'siteName'] index definition. It provides a very efficient way of knowing the total visits for each site for all time, but it doesn't provide any time-based information. To display the number of visits by hour for all sites you'd use the query:

Visit.getCountsGroupByOccurTime()

which uses the [groupBy: 'occurTime'] counter to return:

[2011-12-25T05:9, 2012-01-01T05:6, 2012-01-05T05:3, 2012-01-12T05:3, 2012-01-13T05:1,
2012-02-10T05:3, 2012-02-12T05:5, 2012-02-13T05:1, 2012-02-15T05:1, 2012-02-16T05:3,
2012-02-17T05:3, 2012-02-22T05:3, 2012-02-25T05:3]

Aggregating by different time granularity

You can also aggregate the counts by day, month, or year:

Visit.getCountsGroupByOccurTime(grain: Calendar.DAY_OF_MONTH)
Visit.getCountsGroupByOccurTime(grain: Calendar.MONTH)
Visit.getCountsGroupByOccurTime(grain: Calendar.YEAR)

producing:

[2011-12-25:9, 2012-01-01:6, 2012-01-05:3, 2012-01-12:3, 2012-01-13:1, 2012-02-10:3, 2012-02-12:5, 2012-02-13:1,
2012-02-15:1, 2012-02-16:3, 2012-02-17:3, 2012-02-22:3, 2012-02-25:3]

[2011-12:9, 2012-01:13, 2012-02:22]

[2011:9, 2012:35]

Nested counter results

If you wanted to generate a stacked bar graph showing the monthly visits by site, you'd use the expression:

Visit.getCountsGroupByOccurTimeAndSiteName(grain: Calendar.MONTH)

which uses the [groupBy: ['occurTime','siteName']] counter to return:

[
 2011-12:[www.siteone.com:3, www.sitetwo.com:6],
 2012-01:[www.siteone.com:5, www.sitetwo.com:8],
 2012-02:[www.siteone.com:10, www.sitetwo.com:12]
]

Filtering with findBy properties

If you wanted to see the monthly visits for a single site you'd use either of these expressions:

Visit.getCounts(where: [siteName: 'www.siteone.com'], groupBy: 'occurTime', grain: Calendar.MONTH)
Visit.getCountsBySiteNameGroupByOccurTime('www.siteone.com', [grain: Calendar.MONTH])

producing the following using the [findBy: 'siteName', groupBy: ['occurTime']] counter:

[2011-12:3, 2012-01:5, 2012-02:10]

You can also get a site's monthly visits grouped by referrerType:

Visit.getCountsBySiteNameGroupByOccurTimeAndReferrerType('www.siteone.com', 'Search', [grain: Calendar.MONTH])

producing:

[2011-12:[Search:1, Social:1], 2012-01:[Search:2, Social:2], 2012-02:[Search:5, Social:3]]

and referrerName:

Visit.getCountsBySiteNameGroupByOccurTimeAndReferrerName('www.siteone.com', 'Google', [grain: Calendar.MONTH])

producing:

[2011-12:[Google:1, Facebook:1], 2012-01:[Bing:1, Twitter:2, Google:1], 2012-02:[Google:3, Facebook:2, Bing:2, Twitter:1]]

Multiple matching counter definitions

Both of these queries use the [findBy: 'siteName', groupBy: ['occurTime','referrerType','referrerName']] counter. You don't need to specify every groupBy property in the query, but you do need so specify every findBy property. So with the above counter definition you could execute the query Visit.getCountsGroupByReferrerType(). To do that you would need a counter definition like [groupBy: 'referrerType'] or [groupBy: ['occurTime','referrerType']]. In cases where more than one counter definition satisfies the query the system will choose the most efficient one.

The time-based counter keys are all storied and queried in Universal Time (i.e. Greenwich Mean Time). To return results in another time zone specify it as an option:

Visit.getCountsGroupByOccurTime(grain: Calendar.DAY_OF_MONTH, timeZone: TimeZone.getDefault())
Visit.getCountsGroupByOccurTime(timeZone: TimeZone.getDefault())

Filling in zero values

The default behavior of the time-based counters is to only return items where the count is non-zero. However, when graphing data it is often convenient to have all time increments returned. You can do this by saying:

Visit.getCountsGroupByOccurTime(grain: Calendar.DAY_OF_MONTH, fill: true)

which returns:

[2011-12-25:9, 2011-12-26:0, 2011-12-27:0, 2011-12-28:0, 2011-12-29:0, 2011-12-30:0, 2011-12-31:0, 2012-01-01:6,
2012-01-02:0, 2012-01-03:0, 2012-01-04:0, 2012-01-05:3, 2012-01-06:0, 2012-01-07:0, 2012-01-08:0, 2012-01-09:0,
2012-01-10:0, 2012-01-11:0, 2012-01-12:3, 2012-01-13:1, 2012-01-14:0, 2012-01-15:0, 2012-01-16:0, 2012-01-17:0,
2012-01-18:0, 2012-01-19:0, 2012-01-20:0, 2012-01-21:0, 2012-01-22:0, 2012-01-23:0, 2012-01-24:0, 2012-01-25:0,
2012-01-26:0, 2012-01-27:0, 2012-01-28:0, 2012-01-29:0, 2012-01-30:0, 2012-01-31:0, 2012-02-01:0, 2012-02-02:0,
2012-02-03:0, 2012-02-04:0, 2012-02-05:0, 2012-02-06:0, 2012-02-07:0, 2012-02-08:0, 2012-02-09:0, 2012-02-10:3,
2012-02-11:0, 2012-02-12:5, 2012-02-13:1, 2012-02-14:0, 2012-02-15:1, 2012-02-16:3, 2012-02-17:3, 2012-02-18:0,
2012-02-19:0, 2012-02-20:0, 2012-02-21:0, 2012-02-22:3, 2012-02-23:0, 2012-02-24:0, 2012-02-25:3]

By default time-based counters include all counts, but you can limit the results to a certain time range by specifying and start time and/or an finish time. To display all results from a particular time forward:

Visit.getCountsGroupByOccurTime(fill: true, grain: Calendar.DAY_OF_MONTH, start: Date.parse('yyyy-MM-dd','2012-02-01'))

or to display results only within a specific range of times:

Visit.getCountsGroupByOccurTime(grain: Calendar.DAY_OF_MONTH, fill: true,
        start: Date.parse('yyyy-MM-dd','2012-02-01'),
        finish: Date.parse('yyyy-MM-dd','2012-02-15'))

Custom DateFormats

You can also specify your own custom date format. This option is useful if you want the date values formatted in a different way, or if you want to display count histograms. Either a Java SimpleDateFormat String or a DateFormat object can be passed in to the dateFormat option. For example, the following query:

Visit.getCountsGroupByOccurTime(dateFormat: 'HH')

displays counts by hour of the day:

[22:1, 23:2, 00:41]

2.5 Expando Classes

Casandra's flexible data model can be a good match with Groovy's dynamic aspects. After all, why limit the properties of an object to a predefined set when Cassandra itself supports millions of columns per row that can differ from one row to the next. The cassandra-orm plugin supports this flexibility by allowing you do designate a class as being an "expando". For example, given the class declaration:

class Event 
{
    UUID uuid
    String eventType
    String accountId
    Map payload

static cassandraMapping = [ primaryKey: "uuid", exlicitIndexes: ["accountId",["accountId","eventType"],["accountId","eventType","source"]], expandoMap: "payload" ] }

You can construct an object with any set of properties you want:

new Event(accountId: "A001", eventType: "PhoneCall", source: "+13015551212", destination: "+14155551212").save()

and access those properties with the same syntax as the declared properties:

Event.list().each { println it.destination }

You can even query based on expando properties, provided they are declared as an index:

Event.findAllByAccountIdAndEventTypeAndSource("A001","PhoneCall", "+13015551212")

Note that as of now, all expando properties are Strings. This limitation may be relaxed in subsequent releases.

2.6 Time-To-Live

Cassandra supports the specification of a Time-to-Live (TTL) property that results in a column being automatically deleted after the specified time has elapsed. The cassandra-orm plugin allow a time to live to be declared for an entire object (and its indexes) or only certain fields of an object. Note that Cassandra counters do not support TTL, so you should not reference fields with a TTL declared in either the findBy or groupBy elements of a counter declaration. Here's an example of declaring a time-to-live for an entire object:

class LogEntry 
{
    UUID uuid
    Date occurTime
    String messageText

def cassandraMapping = [ primaryKey: "uuid", timeToLive: 90*24*3600 ] }

Here's an example of specify a time-to-live for only certain columns of an object:

class Event 
{
    UUID uuid
    String accountId
    String eventType
    String ipAddress
    Map payload

static cassandraMapping = [ primaryKey: "uuid", explicitIndexes: ["accountId",["accountId","eventType"],["accountId","eventType","source"]], expandoMap: "payload", timeToLive: [ipAddress: 90*24*3600,transcript: 30*24*3600] ] }

2.7 Custom Mapping

By default, the cassandra-orm plugin expects a column family to exist with the same name as each class with a cassandraMapping declaration. It also expects a column family with that class name and a _IDX suffix to exist to hold the indexes for the class, and if there are counters declared in the mapping, a column family with a _CTR suffix. In other words, given a class names:
  • SomeClass

there should be three column families named:

  • SomeClass (to store the objects themselves)
  • SomeClass_IDX (to store the indexes to the objects)
  • SomeClass_CTR (to store the object counters, if counters have been declared)

You can override this default behavior by adding a columnFamily property to the cassandraMapping Map:

static cassandraMapping = [
    columnFamily = "SomeOtherName"
]

The default key space and cluster used depends on the cassandra client configuration. At this time the only available client is the http://www.florian.org/grails/cassandra-astyanax/docs/guide/1%20Cassandra%20Astyanax%20Plugin.html which is able to configure multiple clusters and key spaces in this way:

astyanax {
	clusters {
		standard {
			seeds = ["localhost:9160"]
			defaultKeyspace = "MyAppDb"
		}
		tracking {
			seeds = ["localhost:9160"]
			defaultKeyspace = "MainTracking"
		}
	}
	defaultCluster = 'standard'
}

Given that configuration, the system will default to using the standard cluster and MyAppDb key space. You can override the default cluster and key space with the following cassandraMapping properties:

static cassandraMapping = [
    cluster = "tracking"
    keySpace = "SecondaryTracking"
]

2.8 Data Integrity

Unlike most relational databases, Cassandra does not provide any built-in referential integrity capabilities. The _cassandra-orm_plugin does maintain the integrity of its explicit indexes and relationships, but using a different approach.

2.9 Cassandra-Specific Arguments

There are a number of places where GORM method argument conventions have have been extended or altered because of the features and limitations of Cassandra. One of the most commonly encountered is in the paging of query results where Cassandra does not support the concept of an integer offset , but rather accepts a key value from which to make the next query. The departures from GORM syntax are described below.

Paging of query results

Queries for objects can be constrained with the following options:

startThe id of the first object to return
startAfterThe id of the object before the first object to return
finishThe id of the last object to return
maxThe maximum number of objects to return
reversedQuery for objects from last to first rather than first to last

For example:

def firstFive = Visit.list(max: 5)
def nextFive = Visit.list(startAfter: firstFive[-1], max: 5)
def lastFive = Visit.list(max: 5, reversed: true)
def nextToLastFive = Visit.list(startAfter: lastFive[-1], max: 5, reversed: true)

These parameters appy to the list() and findAll... class methods. They can also be used to limit the results returned from hasMany relationships. For example, given the definition:

class Person
{
    List posts
    static hasMany = [posts: Post]
    …
}

you can perform the following queries:

def allPosts = person.posts
def numberOfPosts = person.postsCount()
def firstTenPosts = person.posts(max:10)
def nextTenPosts = person.posts(max:10, startAfter: allPosts[-1])
def lastTenPosts = person.posts(max:10, reversed: true)
def nextToLastTenPosts = person.posts(max:10, reversed: true, start: lastTenPosts[-1])

Retrieving individual object properties

One of the reasons Cassandra is used is its support for very large rows. This capability coupled with the expando map capability of the cassandra-orm plugin could results in large objects that are inefficient to read in their entirety. For this reason there is capability to read only some of an object's values. For example, the following query returns a collection of email addresses:

Person.list(column: 'emailAddress')

You can also query for more than one property, the following returns a collection of maps, when each map has a firstName, lastName, and emailAddress entry:

Person.list(columns: ['firstName','lastName','emailAddress'])

You can specify a column or columns option for list , find... , and has many property methods, for example:

Person.list(column: 'username')
Person.findByState("Maryland", [columns: ['city','phone'])
person.posts(column: 'text')

Inserting individual object properties

In the same way that reading individual properties might be useful, the ability to write individual properties can bu useful as well. All Cassandra write operations are "upserts", i.e. they will create the column if it doesn't exist, or modify it if it does exist. To update only a single property of an object, you'd do something like:

person.insert(emailAddress: "jdoe@anywhereelse.com")

You can also update several properties at the same time:

person.insert(emailAddress: "jdoe@anywhereelse.com", firstName: "Joey")

3 Demo Data Model

3.1 Class Declarations

Person

class User
{
    String username
    String emailAddress
    String firstName
    String lastName
    String address
    String city
    String state
    String zip
    String country
    String phone
    Gender gender
    Date birthDate

List posts static hasMany = [posts: Post]

static cassandraMapping = [ primaryKey: 'username', explicitIndexes: ["emailAddress", "phone", 'birthDate', ["country","state","city"]], secondaryIndexes: ["gender","country", "state"], counters: [ [groupBy: ['birthDate']], [groupBy: ['gender']], [groupBy: ['country','state','city','gender']], [findBy: ['country','state'], groupBy:['city','gender']] ] ] }

Post

class Post
{
	UUID uuid
	String text
	Date occurTime

Person person static belongsTo = [person: Person]

List comments Set likedBy static hasMany = [comments: Comment, likedBy: Person]

static cassandraMapping = [ primaryKey: 'uuid', counters: [ [groupBy: ['occurTime']] ], keySpace: "demo" ]

Boolean isLikedBy(person) { likedByCount(start:person.id, finish:person.id) > 0 } }

Comment

class Comment
{
	UUID uuid
	String text
	Date occurTime
	Person person
	Post post
	static belongsTo = [post: Post]

Set likedBy static hasMany = [likedBy: Person]

static cassandraMapping = [ primaryKey: 'uuid', counters: [ [findBy: ['post'], groupBy:['occurTime']], [findBy: ['person'], groupBy:['occurTime']] ], keySpace: "demo" ]

Boolean isLikedBy(person) { likedByCount(start:person.id, finish:person.id) > 0 } }

3.2 Schema

CLI schema creation script

create column family Person
    with comparator=UTF8Type
    and default_validation_class=UTF8Type;

create column family Person_IDX with comparator=UTF8Type and default_validation_class=UTF8Type;

create column family Person_CTR with comparator=UTF8Type and default_validation_class=CounterColumnType;

create column family Post with comparator=UTF8Type and default_validation_class=UTF8Type;

create column family Post_IDX with comparator=UTF8Type and default_validation_class=UTF8Type;

create column family Post_CTR with comparator=UTF8Type and default_validation_class=CounterColumnType;

create column family Comment with comparator=UTF8Type and default_validation_class=UTF8Type;

create column family Comment_IDX with comparator=UTF8Type and default_validation_class=UTF8Type;

create column family Comment_CTR with comparator=UTF8Type and default_validation_class=CounterColumnType;

3.3 Data Structures

The Cassandra data structures produced by this model are shown in the following sections.

Person

Person:
    user1 =>
         class_name  => 'example.Person'
        username => 'user1'
    user2 =>
         class_name  => 'example.Person'
        username => 'user2'

Person_IDX

Person_IDX:
    Comment?likedBy=001347471378565_5c260b50-fd00-11e1-8230-001c42000009 =>
        user1 => ''
        user2 => ''
    Comment?likedBy=001347471378631_5c301d70-fd00-11e1-8230-001c42000009 =>
        user1 => ''
    Person?friends=user1 =>
        user1 => ''
        user2 => ''
    Person?friends=user2 =>
        user1 => ''
        user2 => ''
    Post?likedBy=001347471378340_5c03b640-fd00-11e1-8230-001c42000009 =>
        user2 => ''
    Post?likedBy=001347471378502_5c1c6e60-fd00-11e1-8230-001c42000009 =>
        user1 => ''
    this =>
        user1 => ''
        user2 => ''
    this#user1 =>
        Comment?likedBy=001347471378565_5c260b50-fd00-11e1-8230-001c42000009 => ''
        Comment?likedBy=001347471378631_5c301d70-fd00-11e1-8230-001c42000009 => ''
        Person?friends=user1 => ''
        Person?friends=user2 => ''
        Post?likedBy=001347471378502_5c1c6e60-fd00-11e1-8230-001c42000009 => ''
    this#user2 =>
        Comment?likedBy=001347471378565_5c260b50-fd00-11e1-8230-001c42000009 => ''
        Person?friends=user1 => ''
        Person?friends=user2 => ''
        Post?likedBy=001347471378340_5c03b640-fd00-11e1-8230-001c42000009 => ''
    this@user1 =>
        001347471378340_5c03b640-fd00-11e1-8230-001c42000009ÿpersonÿPost => ''
        001347471378631_5c301d70-fd00-11e1-8230-001c42000009ÿpersonÿComment => ''
    this@user2 =>
        001347471378502_5c1c6e60-fd00-11e1-8230-001c42000009ÿpersonÿPost => ''
        001347471378565_5c260b50-fd00-11e1-8230-001c42000009ÿpersonÿComment => ''

Post

001347471378340_5c03b640-fd00-11e1-8230-001c42000009 =>
         class_name  => 'example.Post'
        occurTime => '2012-09-12T17:36:18.308Z'
        personId => 'user1'
        text => 'User 1's Post 1'
        uuid => '5c03b640-fd00-11e1-8230-001c42000009'
    001347471378502_5c1c6e60-fd00-11e1-8230-001c42000009 =>
         class_name  => 'example.Post'
        occurTime => '2012-09-12T17:36:18.498Z'
        personId => 'user2'
        text => 'User 2's Post 1'
        uuid => '5c1c6e60-fd00-11e1-8230-001c42000009'

Post_IDX

Person?posts=user1 =>
        001347471378340_5c03b640-fd00-11e1-8230-001c42000009 => ''
    Person?posts=user2 =>
        001347471378502_5c1c6e60-fd00-11e1-8230-001c42000009 => ''
    this =>
        001347471378340_5c03b640-fd00-11e1-8230-001c42000009 => ''
        001347471378502_5c1c6e60-fd00-11e1-8230-001c42000009 => ''
    this#001347471378340_5c03b640-fd00-11e1-8230-001c42000009 =>
        Person?posts=user1 => ''
    this#001347471378502_5c1c6e60-fd00-11e1-8230-001c42000009 =>
        Person?posts=user2 => ''
    this@001347471378340_5c03b640-fd00-11e1-8230-001c42000009 =>
        001347471378565_5c260b50-fd00-11e1-8230-001c42000009ÿpostÿComment => ''
    this@001347471378502_5c1c6e60-fd00-11e1-8230-001c42000009 =>
        001347471378631_5c301d70-fd00-11e1-8230-001c42000009ÿpostÿComment => ''

Post_CTR

this#occurTime =>
        2012-09-12T17 => 2
    this#occurTime[yyyy-MM-dd] =>
        2012-09-12 => 2
    this#occurTime[yyyy-MM] =>
        2012-09 => 2

Comment

001347471378565_5c260b50-fd00-11e1-8230-001c42000009 =>
         class_name  => 'example.Comment'
        occurTime => '2012-09-12T17:36:18.559Z'
        personId => 'user2'
        postId => '001347471378340_5c03b640-fd00-11e1-8230-001c42000009'
        text => 'Comments to post 1 from user 2'
        uuid => '5c260b50-fd00-11e1-8230-001c42000009'
    001347471378631_5c301d70-fd00-11e1-8230-001c42000009 =>
         class_name  => 'example.Comment'
        occurTime => '2012-09-12T17:36:18.627Z'
        personId => 'user1'
        postId => '001347471378502_5c1c6e60-fd00-11e1-8230-001c42000009'
        text => 'Comments to post 2 from user 1'
        uuid => '5c301d70-fd00-11e1-8230-001c42000009'

Comment_IDX

Post?comments=001347471378340_5c03b640-fd00-11e1-8230-001c42000009 =>
        001347471378565_5c260b50-fd00-11e1-8230-001c42000009 => ''
    Post?comments=001347471378502_5c1c6e60-fd00-11e1-8230-001c42000009 =>
        001347471378631_5c301d70-fd00-11e1-8230-001c42000009 => ''
    this =>
        001347471378565_5c260b50-fd00-11e1-8230-001c42000009 => ''
        001347471378631_5c301d70-fd00-11e1-8230-001c42000009 => ''
    this#001347471378565_5c260b50-fd00-11e1-8230-001c42000009 =>
        Post?comments=001347471378340_5c03b640-fd00-11e1-8230-001c42000009 => ''
    this#001347471378631_5c301d70-fd00-11e1-8230-001c42000009 =>
        Post?comments=001347471378502_5c1c6e60-fd00-11e1-8230-001c42000009 => ''

Comment_CTR

this?person=user1#occurTime =>
        2012-09-12T17 => 1
    this?person=user1#occurTime[yyyy-MM-dd] =>
        2012-09-12 => 1
    this?person=user1#occurTime[yyyy-MM] =>
        2012-09 => 1
    this?person=user2#occurTime =>
        2012-09-12T17 => 1
    this?person=user2#occurTime[yyyy-MM-dd] =>
        2012-09-12 => 1
    this?person=user2#occurTime[yyyy-MM] =>
        2012-09 => 1
    this?post=001347471378340_5c03b640-fd00-11e1-8230-001c42000009#occurTime =>
        2012-09-12T17 => 1
    this?post=001347471378340_5c03b640-fd00-11e1-8230-001c42000009#occurTime[yyyy-MM-dd] =>
        2012-09-12 => 1
    this?post=001347471378340_5c03b640-fd00-11e1-8230-001c42000009#occurTime[yyyy-MM] =>
        2012-09 => 1
    this?post=001347471378502_5c1c6e60-fd00-11e1-8230-001c42000009#occurTime =>
        2012-09-12T17 => 1
    this?post=001347471378502_5c1c6e60-fd00-11e1-8230-001c42000009#occurTime[yyyy-MM-dd] =>
        2012-09-12 => 1
    this?post=001347471378502_5c1c6e60-fd00-11e1-8230-001c42000009#occurTime[yyyy-MM] =>
        2012-09 => 1