Cassandra Object Persistence Framework - Reference Documentation
Authors: Bob Florian
Version: 0.2.6
Table of Contents
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.groovycompile ()">> {
excludes "commons-logging"
excludes 'slf4j-log4j12'
}
runtime
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']] ] ] }
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;
new Person(username: "jdoe", emailAddress: "jdoe@anywhere.com", firstName: "John", lastName: "Doe", country: "USA").save()
def user1 = Person.get("jdoe") def user2 = Person.findByEmailAddress("jdoe@anywhere.com") def users = Person.findAllByCountry("USA")
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()))
user.posts.each {println it.title}
user.posts(reversed: true, max: 3).each {println it.title}
user.posts(column: "title").each {println it}
user.postsCount()
Person.getCountsGroupByCountry()
Post.getCountsGroupByOccurTime() Post.getCountsGroupByOccurTime(grain: Calendar.DAY_OF_MONTH) Post.getCountsGroupByOccurTime(grain: Calendar.MONTH)
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 namedcassandraMapping
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 anid
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'] }
class Person { String emailAddress String firstName … static cassandraMapping = [primaryKey: ['emailAddress','firstName']] }
class BlogPost {
UUID uuid
…
static cassandraMapping = [primaryKey: 'uuid']
}
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']
}
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 thecassandraMapping
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} ];
cassandraMapping
definition:class Person { String username String state String country String gender static cassandraMapping = [ primaryKey: 'username', secondaryIndexes: ["gender","country", "state"], ] }
Person.findAllByGender("Female") Person.findAllByCountryAndState("USA","New York") Person.findAllWhere(country: "USA", state: "Maine", gender: "Male")
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"]], ] }
Person.findByPhone("+13015551212") Person.findAllByState("Maryland") Person.findAllByCityAndState("Baltimore","Maryland")
Person.findWhere(emailAddress: "jdoe@anywhere.com") Person.findAllWhere(state: "Virginia", city: "Reston")
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"])
Person.countByState("California") Person.countByCityAndState("San Francisco","California")
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:Name | Definition |
---|---|
findBy | Zero 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 |
groupBy | One 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. |
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']], ] ] }
Visit.getCounts(groupBy: 'siteName') Visit.getCountsGroupBySiteName()
[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()
[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)
[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)
[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])
[findBy: 'siteName', groupBy: ['occurTime']]
counter:[2011-12:3, 2012-01:5, 2012-02:10]
Visit.getCountsBySiteNameGroupByOccurTimeAndReferrerType('www.siteone.com', 'Search', [grain: Calendar.MONTH])
[2011-12:[Search:1, Social:1], 2012-01:[Search:2, Social:2], 2012-02:[Search:5, Social:3]]
Visit.getCountsBySiteNameGroupByOccurTimeAndReferrerName('www.siteone.com', 'Google', [grain: Calendar.MONTH])
[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)
[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]
Visit.getCountsGroupByOccurTime(fill: true, grain: Calendar.DAY_OF_MONTH, start: Date.parse('yyyy-MM-dd','2012-02-01'))
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 JavaSimpleDateFormat
String or a DateFormat
object can be passed in to the dateFormat
option. For example, the following query:Visit.getCountsGroupByOccurTime(dateFormat: 'HH')
[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" ] }
new Event(accountId: "A001", eventType: "PhoneCall", source: "+13015551212", destination: "+14155551212").save()
Event.list().each { println it.destination }
Event.findAllByAccountIdAndEventTypeAndSource("A001","PhoneCall", "+13015551212")
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 ] }
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
- 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)
static cassandraMapping = [ columnFamily = "SomeOtherName" ]
astyanax { clusters { standard { seeds = ["localhost:9160"] defaultKeyspace = "MyAppDb" } tracking { seeds = ["localhost:9160"] defaultKeyspace = "MainTracking" } } defaultCluster = 'standard' }
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:start | The id of the first object to return |
---|---|
startAfter | The id of the object before the first object to return |
finish | The id of the last object to return |
max | The maximum number of objects to return |
reversed | Query for objects from last to first rather than first to last |
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)
class Person
{
List posts
static hasMany = [posts: Post]
…
}
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')
Person.list(columns: ['firstName','lastName','emailAddress'])
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")
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