Nested Records in Sproutcore

by Evin

On the project that I am working on, we created this construct called Nested Records. I have just ported it over to the Sproutcore framework. It is extremely useful for data models that use a nested structure. These include most document based database systems like CouchDB and Persevere. These databases and subsequent APIs sometimes need to be configured to make them more Sproutcore-friendly and, now with the new Nested Records, it should be much more easy to create APIs and data structures that make more sense. These nested records can be n-levels and polymophic so that it is a highly flexible framework.

What is a Nested Record?

First, let’s define what a nested record looks like. This is the JSON that I will use for the rest of the example:

  {
    type: 'Person',
    name: 'Albert Einstein',
    address: {
      type: 'Address',
      street: '123 Genius Lane',
      city: 'Princeton',
      state: 'NJ'
    }
  }

and a list of nested records would look like this:

  {
    type: 'Group',
    name: 'Smart People',
    people: [
      {
        type: 'Person',
        name: 'Albert Einstien',
        addresses: [
          {
            type: 'Address',
            street: '123 Genius Lane',
            city: 'Princeton',
            state: 'NJ'
          },
          {
            type: 'Address',
            street: '77 Gedanken Way',
            city: 'Ether',
            state: 'MT'
          }
        ]
      },
        type: 'Person',
        name: 'Richard Feynman',
        addresses: [
          {
            type: 'Address',
            street: '6626 Planck Pl',
            city: 'Altadena',
            state: 'CA'
          },
          {
            type: 'Address',
            street: '11 Schrödinger Ct',
            city: 'Baja',
            state: 'CA'
          }
        ]
      }
    ]
  }

Nested Record Structure in Sproutcore

Now, that we know what the nested records are in JSON. What is the structure in Sproutcore? Well, it really boils down to two things: Setting up your Record to be a parent and Child Records. Obviously, parent records can have one or more child records and this is where the power comes into the equation. The parent record are set up with the use of the toOne() and toMany() attributes on the model. This will tell the specific RecordAttributes wether to handle the relationship as a guid or as the specific hashes. You can also do this on SC.ChildRecords as well to create heavily nested structures. So what would this look like for our first example:

  NestedRecord.Address = SC.ChildRecord.extend({
    street: SC.Record.attr(String),
    city: SC.Record.attr(String),
    state: SC.Record.attr(String, {defaultValue: 'VA'})
  });
 
  NestedRecord.Person = SC.Record.extend({
    /** Child Record Namespace */
    childRecordNamespace: NestedRecord,
 
    name: SC.Record.attr(String),
    address: SC.Record.toOne('NestedRecord.Address', { nested: true })
  });

and for the second example:

  NestedRecord.Group = SC.Record.extend({
    /** Child Record Namespace */
    childRecordNamespace: NestedRecord,
 
    name: SC.Record.attr(String),
    people: SC.Record.toMany('NestedRecord.Person', { nested: true })
  });
 
  NestedRecord.Person = SC.ChildRecord.extend({
    /** Child Record Namespace */
    childRecordNamespace: NestedRecord,
 
    name: SC.Record.attr(String),
    addresses: SC.Record.toMany('NestedRecord.Address', { nested: true })
  });
 
  NestedRecord.Address = SC.ChildRecord.extend({
    street: SC.Record.attr(String),
    city: SC.Record.attr(String),
    state: SC.Record.attr(String, {defaultValue: "VA"})
  });

Single Child Version

I will take the first example: A Person with an Address attached to them. First, I should talk a little about the theory and approach that I have taken. The idea is that these are javascript objects on the primary object, but they can be accessed as SC.Record objects in the store. This is an important distinction when it comes to setting the values at the different levels of the nested record stack. For example, if you were to change the variables on the address of Albert Einstein you would do the following:

  var albert = NestedRecord.get('store').find(NestedRecord.Person, 1); // assuming that it is the first record here...
  var al_address = albert.get('address');
  al_address.set('street', '2233 General Relativity Way');

but if you wanted to change the entire address you would need to do the following:

  var albert = NestedRecord.get('store').find(NestedRecord.Person, 1); // assuming that it is the first record here...
  albert.set('address', {
    type: 'Address',
    street: '2233 General Relativity Way',
    city: 'Princeton',
    state: 'NJ'
  });

The reason is having to do with the reference frame. When you are talking to a parent record, the child records are JS objects and when you are talking to the child record themselves they are SC.Records.

Polymorphism

Child Records where built with polymorphism in mind and there are two rules that need to be followed in order for this to work:

  • Parent Records must have a childRecordNamespace property
  • Child Records must have a type property that relates to the name of the SC.ChildRecord

If you don’t want polymorphism, you can just add the Record name string in the child relation definition in the model as you can see from the above examples.

Multiple Children…Duggar-style

So that last thing I will talk about is the case where you have multiple child records attached to a parent record like the second example. When you get() one of these params then you get a SC.ChildArray which is exactly like a SC.ManyArray which means that it mixes in SC.Enumerable and SC.Array to give you all the KVO/KVC compliant things, but the important thing to remember is that they must be accessed in the KVO/KVC compliant way if you want to have the bindings and observes fire in your application. So the standard calls looks something like this:

  var smart_ppl = NestedRecord.get('store').find(NestedRecord.Group, 1); // assuming that it is the first record here..
  var len = smart_ppl.get('length'); // returns the length of the array
  var albert = smart_ppl.objectAt(1); // return the first object in the array
  smart_ppl.pushObject({
    type: 'Person',
    name: 'Neumann János Lajos',
    addresses: []
  }); // Pushes a new child record onto the array
  // The same works for: popObject, shiftObject, unshiftObject etc..

Conclusion

So hopefully, this will provide a missing feature to the Sproutcore Datasource framework. I wrote 35 test with 251 assertions so it works pretty well. I need to have other people test it in different situations so that I can improve the construct. There was also a side benefit…I had to add a new function to the SC.Store to unload records when you don’t need them and it looks like this:

  unloadRecord: function(recordType, id, storeKey, newStatus) {
    // code is here
  }

The newStatus defaults to SC.Record.EMPTY, but you can set it to anything that you want like SC.Record.DESTROYED_CLEAN for the case where a record was destroyed on your datasource without your GUI’s knowledge.

So please use this new construct…it can be found on the main Sproutcore repository.

This entry was posted on Sunday, January 24th, 2010 at 8:08 pm and is filed under Coding Tutorials, Sproutcore. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

4 Responses to “Nested Records in Sproutcore”

  1. Tweets that mention It's Got What Plants Crave » Blog Archive » Nested Records in Sproutcore -- Topsy.com Says:

    [...] This post was mentioned on Twitter by Evin Grano, Erich Atlas Ocean and vikingstad, Web Developer Links. Web Developer Links said: Sproutcore: Nested Records in Sproutcore http://bit.ly/65KsgY [...]

  2. Stevie Graham Says:

    Has this made it into core yet? If not, anyone have a diff i can merge?

  3. Mike Says:

    Yep, its on sproutit/master

  4. Macario Says:

    Hi, I am taking a lot of my work time to learn SproutCore and it promisses to be a path full of joy and sorrow. I am having really weird things going on with nested records:

    Registration.Form = SC.Record.extend(
    {
    childRecordNamespace: Registration,
    authors: SC.Record.toMany(‘Registration.User’, {nested: true}),

    }

    Registration.User = SC.Record.extend(
    {
    type: “User”,
    firstName: SC.Record.attr(String),
    lastName: SC.Record.attr(String)

    }

    var user = Registration.store.find(Registration.loginController); // No problem, store retrieves my user

    // First attempt:
    var form = Registration.store.createRecord(Registration.Form, {authors: []});
    form.get(‘authors’).pushObject(user) // => Object
    form.getPath(‘authors.length’) // => 1
    form.get(‘authors’).objectAt(0) // => undefined, what??

    // Second attempt:
    var form = Registration.store.createRecord(Registration.Form, {authors: [user]});
    form.getPath(‘authors.length’) // => 1
    form.get(‘authors’).objectAt(0) // => Object
    user.get(‘email’) // => ‘myemail@host.com’
    form.get(‘authors’).objectAt(0).get(‘email’) // => null, DAMN!!!

    form.getPath(‘authors’).objectAt(0).set(‘email’, ‘anothermail’) // => SC.Error:sc31:Internal Inconsistency (-1)
    form.get(‘authors’).objectAt(0).set(‘email’, ‘ooo’) // => Object
    form.get(‘authors’).objectAt(0).get(‘email’) // => null, Men, I am lost!

Leave a Reply