View on GitHub

EmberData - What happens when you commit ?

Deep dive into the EmberData commit process

I have been using Ember.js and EmberData for a long time now and most of my battles still seem to be in EmberData. Therefore i have decided to dive into EmberData and see exactly how the magic works and hopefully why the problems are happening, so that one day i may be able to help fix them !

I am going to start with detailing the process of what actually happens when commit is called.

The test code

I have this simple one-to-many relationship. ( Coffeescript )

App.Product = DS.Model.extend 
  name: DS.attr('string')
  children: DS.hasMany('App.BuildingBlocks')

App.BuildingBlock = DS.Model.extend 
  name: DS.attr('string')
  parent: DS.belongsTo('App.Product')

I have created and committed a product prior to this so now have access to App.product and now i want to add a building block to this product, so i have done

newBuildingBlock = App.product.get("children").createRecord()
newBuildingBlock.set("parent", App.product)
App.store.commit()

This magically saves this of to my server but what actually happens ?

Lets follow it through the process.

DS.Store

Within DS.Store the commit method just delegates to the stores defaultTransaction's commit method.

With EmberData you can create multiple transactions and add specific creates, changes and deletes to any transaction. Doing this allows you to group related changes together and commit just those changes or roll back the changes.

If you do not create a DS.Transaction and add changes to it, EmberData with use the 'defaultTransaction' and add all changes to this global transaction. This means that when you commit, all changes with be saved.

DS.Transaction

When the defaultTransaction is initialised it creates some buckets (clean, created, updated, deleted and inflight) in the form of Ember.OrderedSet's.

set(this, 'buckets', {
  clean:    Ember.OrderedSet.create(),
  created:  Ember.OrderedSet.create(),
  updated:  Ember.OrderedSet.create(),
  deleted:  Ember.OrderedSet.create(),
  inflight: Ember.OrderedSet.create()
});

Whenever a record is changed it is added to the relavent bucket to be used when commit is called on the transaction. For example when we created a new App.BuildingBlock record, this object was added to the created bucket.

Within DS.Transaction#commit we get the store, the stores adapter and the stores defaultTransaction to use. Then we get the stores 'relationships'. ????WHAT ARE RELATIONSHIPS ????

Then a commitDetails object with created, updated, deleted and relationships properties is created. Each of these references the relavent bucket on this transaction.

For each record in the created, updated, deleted buckets, it's send method is called with 'willCommit'.

We only have a record in the created bucket.

DS.Model

Within DS.Model the send method just delegates to the App.BuildingBlock instances statemanger#send method

DS.Statemanager

Within DS.Statemanager the send method receives the 'willCommit' event and checks that the event can be sent while in this current state (which is "uncommited" at the moment). If it can't it will thow the dreaded 'Cannot send event while currentState is' error message!

The sendEvent function is called that then calls the sendRecursively function on the statemanager with event name and the currentState which is an instance of DS.State.

the sendRecursively function then transitions the statemanager's state to 'inflight'

Back to DS.Transaction

If this transaction is the defaultTransaction then a new transaction is created with this store. Then the stores defaultTransaction is set to this new transaction.

Next removeCleanRecords is called does what it says and removes all records from within the clean bucket.

Check if there are any records in the created, updated, deleted or relationships. If so if there is an adapter and it has a commit method then adapter.commit is called with the store and the commitDetails object containing the buckets. Error if no adapter or commit method.

DS.Adapter

The DS.Adapter#commit method just delegates to the save method

DS.Adapter#save method creates an Ember.MapWithDefault for each of the created, updated and deleted buckets and loops each record.

We only have one in the created bucket.

The created bucket is passed through the filter method that loops each record in the bucket and adds it to a new Ember.OrderedSet if the adapter shouldSave method is true.

DS.RESTAdapter

The shouldSave method gets the records '_reference' and checks that it does not have a parent. Which ours does'nt and returns true.
The '_reference' property on the record is a computed property that calls the store#referenceForClientId method with the records clientId.

DS.Store

referenceForClientId checks the recordReferences object for the clientId and returns this object if exists. If not it gets the clientIdToType[clientId] object and adds this record to the recordReferences object and returns this object.

The recordReferences and clientIdToType objects are stated as internal bookkeeping will have to look into that later!

Back to DS.RESTAdapter

The createRecords method is called with the store, type ( App.BuildingBlock ) and the new Ember.OrderedSet with the created record in it.

In createRecords it checks if bulkCommit is set to false. I have this set to false so we will continue on this path for now!

The adapters _super createRecords method is then called which is DS.Adapter.

DS.Adapter

This method then loops each record in the bucket and calls DS.RESTAdapter createRecord

DS.RESTAdapter

The createRecord method calls the rootForType method with the type (App.BuildingBlock) that returns "building_block". The rootForType method calls rootForType on its 'serializer'

NOTE: get(this, 'serializer').toString() returns "DS.RESTSerializer:ember3759" and not "DS.JSONSerializer:ember3759". Is this a bug ?

DS.JSONSerializer

this rootForType method turns App.BuildingBlock into "building_block" string.

Back to DS.RESTAdapter createRecord method

calls serialize method with the recoed and { includeId: true } object.

DS.Adapter

serialize method just delegates to its serializer#serialize method.

DS.Serializer

The serialize method calls createSerializedForm that creates an empty hash for anyone to hook into. This is saved as serialized var.

If the options has includedId which is default and the record has an id which it has not yet call '_addId' unless call addAttributes

addAttributes loops the records eachAttributes and calls '_addAttribute' with the so far empty serialized hash, the record, the name of the attribute (ie name) and the attribute.type ( string in this case )

_addAttribute calls _keyForAttributeName to get the key for the current attribute ( name )

_keyForAttributeName calls _keyFromMappingOrHook with 'keyForAttributeName'

_keyFromMappingOrHook checks for a mapping key for this attribute and if found returns this unless call the public hook method 'keyForAttributeName' where you can implement your own functionality

back in the _addAttribute method call public addAttribute method with the still so far empty serialized hash, the key ( name ) and the result of serializedValue(value, type). This performs any transforms that you have setup for this attribute type. You can create your own custom transforms here. Basically strings are just converted to String, number to Number etc just in case.

DS.JSONSerializer

addAttribute method just adds this attribute name and value to the serialized hash. You can hook into here to cusomize how you want to add the key/value to the serialized hash

back into DS.Serializer serialize method

call addRelationships

addRelationships loops each relationship for this record and if its a 'hasMany' calls _addHasMany

_addHasMany gets the key for the relationship via mappings or public custom hooks and calls addHasMany

DS.JSONSerializer

the addHasMany method will add a has-many relationship if it is embedded. In our case we are not using embedded relationships so we are just returned to _addHasMany

back into DS.Serializer serialize method

addRelationships now calls _addBelongsTo for the product relationship. This then goes and looks up the mapping or hooks for this relationship key 'product' via the _keyForBelongsTo method. We have a mapping for this called 'parent' so this is returned.

the _addBelongsTo method then calls the public addBelongsTo method with the current serialized data hash, the record, the key (now 'parent') and the relationship (product belongsTo).

the addBelongsTo checks of this relationship is embedded. Which in our case its not. Then its gets the relationship key id which is 'product_id' in our case and gets the records product_id value. This is then added to the serialized hash via its key and value.

back to the DS.Serializer serialize method

this now has the serialized data hash built up and returns this hash

Back to DS.RESTAdapter createRecord method

The serialized hash is added to a data hash under the root key which in our case is "building_block".

Finally we have reached the ajax method that actually sends the data to the server and receives the response back. This method is called with the buildURL which in our case is "/building_blocks", the "POST" type and an object containing the serialized data hash, the context (which is the adapter instance) and success and error callbacks.

back to DS.Transaction commit method

Now that the transaction has been committed the relationships for this transaction are deleted.

When the server responds with a successful response didCreateRecord is called. This then calls the store#didSaveRecord.

DS.Store

The didSaveRecord first calls the record#adapterDidCommit method

DS.Model

The adapterDidCommit method loops through the new attributes returned from the server and updates the records attribute values.

The send method is called with 'didCommit'. This just delegates to the records statemanager's send method

DS.Statemanager

???

Back to DS.RESTAdapter

'didCommit' is sent to the sendEvent method which changed the currentState to 'saved'

???DOES SOMETHING ELSE HERE - calls didCommit method that then does something with invokeLifecycleCallbacks ???

back in the DS.Model adapterDidCommit method

calls the updateRecordArraysLater method

the updateRecordArraysLater method sets the updateRecordArrays method to be called in this run loop. Therefore this will be run later.

back in DS.Store didSaveRecord method

the didUpdateAttributes method is called

The didUpdateAttributes method loops through the records attributes and calls didUpdateAttribute on each that calls adapterDidUpdateAttribute WHAT DOES THIS DO ????

Back in DS.Store didCreateRecord method

This then does something with store.load but its not called yet. ?????

The serializer#extract method is then called

DS.JSONSerializer

In the extract method the sideload method loads any sideloaded data from the json payload. In our case there is none.

The extractRecordRepresentation method is the called

DS.Serializer

In the extractRecordRepresentation method the load function is called that calls updateId on the store and then calls store#load

DS.Store

updateId adds this new records clientId to the typeMaps hash's clientIds array. Then adds the records id and clientId to the typeMaps idToCid hash. It the add the clientId and id to the store's clientIdToId hash and add to the recordReferences hash if not there.

In the load method the clientId and the records payload hash is added to the cidToData hash as key and value. Then if the records clientId is in the stores recordCache (which it is) the loadedData method is set to be run on the record once in this run loop. This will be run later.

Back in the DS.Serializer extractRecordRepresentation method

The stores prematerialize method is called

DS.Store

the prematerialize method adds an empty object to the stores clientIdToPrematerializedData hash (not sure what this is for yet)

DS.Model

The run loop now calls the updateRecordArrays method which calls the store dataWasUpdated method

DS.Store

The dataWasUpdated method checks if the record has been deleted during this run loop and if not calls updateRecordArrays.

In the updateRecordArrays method loops through the typeMap for this type (App.BuildingBlock) recordArrays which we currently have none of and also loops through the loadingRecordArrays of which we also have none of.

DS.Model

The run loop now calls the loadedData method which calls send with 'loadedData' that then delegates to the statemanager's send method

DS.Statemanager

in the send method it calls sendEvent.

The sendEvent function is called that then calls the sendRecursively function on the statemanager with event name and the currentState which is an instance of DS.State.

the sendRecursively function calls didChangeData on the DS.State instance with the statemanager. This method the calls the materializeData method on the record.

DS.Model

The materializeData method calls send with 'materializingData'. That again delegates to the statemanagers send method, calls sendEvent then sendRecursively and then calls materializingData that transistions the statemanagers state to 'loaded.materializing'

The store materializeData method is called

DS.Store

The materializeData method sets the stores clientIdToData hash with key of this records clientId to be an object called MATERIALIZED instead of containing the records data hash.

The records setupData method is called that just create a _data hash on the record with emtpy placeholders for attributes, belongsTo, hasMany and id.

Then the adapters materialize method is called with just delegates to the serializers materialize method

DS.JSONSerializer

In the materialize method calls the records materializeId method that sets the id to the materialized id from the payload hash.

The materializeAttributes method is called which calls materializeAttribute for each of the records attributes

DS.Serializer

The materializeAttribute method get the value for the current attribute from the serialized hash via the attribute name as key or using any mapping or hooks set for this attribute. Then through the deserializeValue method it applies any transforms to the attributes value. Finally it adds the key and value to the records "_data.attributes" hash via the records materializeAttribute method.

Back in the DS.JSONSerializer materialize method

The materializeRelationships method is called which loops each of the records relationships. The materializeHasMany method is called if the relationship is a hasMany and the materializeBelongsTo method is called if its a belongsTo. Ours has a belongsTo relationship.

The materializeBelongsTo method gets the relationship key or uses any mapping or hooks set for this relationship to use as the key. In our case the key is 'parent'. Then through the extractBelongsTo method it gets the value for that key from the serialized hash which is the id of the parent / product. Finally through the materializeBelongsTo method, the key ( product ) and value ( id ) are added to the records "_data.belongsTo" hash.

Back in the DS.Model materializeData method

Next the suspendRelationshipObservers method is called that temporarily suspends the relationships observers (belongsToDidChange and belongsToWillChange) so that materialization can happen that does not fire the usual observers such as making a record dirty when it changes.

Then the notifyPropertyChange method is called with 'data'

Ember.Observable (in Ember.js)

This just calls propertyWillChange and propertyDidChange with 'data' that then enters Embers observable system.