alekseykulikov / backbone-offline

[Deprecated] Allows your Backbone.js app to work offline
MIT License
720 stars 56 forks source link

Make backbone.offline compatible with backbone-relational #22

Closed asgeo1 closed 11 years ago

asgeo1 commented 11 years ago

I'm using this plugin alongside backbone-relational plugin.

It's a little bit tricky, because of the way backbone-offline stores/retrieves the models to/from localStorage.

First let me explain the basic use case in which it does work. In backbone-relational, I can setup relational models like this:

Model A
    |
    |------- Model B
          |
          |------- Model C

I can then apply backbone.offline storage to Model A. This stores Model A in localStorage. Nested within the attributes of Model A, will be the related Model B's and Model C's.

I can sync Model A to the server and it will also sync the related Model B's and Model C's - as backbone-relational overrides toJSON so it creates a big nested object of everything under Model A to be synced. You then just need to code your server endpoint so it can handle the nested data. Which is easy to do if you're using something like Rails.

So that use case works OK. But the problem is there is no way to sync Model B's or Model C's independent of Model A.

If you also apply the backbone.offline storage to Model B, backbone.offline will store the Model B's in a separate localStorage key/value, which is great.

But unfortunately, the actual Model B objects (as created by backbone-relational) are still nested under Model A.

So now you have two copies of the same data in localStorage and the whole thing falls down.

So to be clear, I think the syncing behaviour of backbone.offline is correct, it's just how it stores/retrieves nested models to/from localStorage that is the issue.

Would like to hear if anyone has had this issue, and what some potential solutions could be!

alekseykulikov commented 11 years ago

Hi @asgeo1, thank you for experience. Yes, nested models is a problem for backbone.offline and I will try to create more straightforward solution in next release: https://github.com/Ask11/backbone.offline/issues/21 Now you can try to play with keys: option, but it's a bit tricky. Anyway lets make this topic as a starting point for discussions about how to implement nested models to backbone.offline.

johnkron commented 11 years ago

asgeo1 did you resolve this issue some how?

asgeo1 commented 11 years ago

@jask24 - not really, I hacked around until I got something working. But it isn't very good and has issues. Backbone.offline is just not designed to work with relational data models in it's current state.

I'm probably going to have to write my own offline solution to work with Backbone-relational in the interim.

ntheile commented 11 years ago

{ cascadeDelete: true } - One to Many

I had a similar issue with one to many relationships and cascading deletes, especially when the parent model was offline. I wrote some helper functions.

Use the { cascadeDelete: true } option when you initialize storage and when you use the the keys option

If the key is used as a foreign key in a one to many relationship on the collection it will automatically delete the child models. This is used by default only when the parent is dirty because the default behavior assumes your sql server will take care of cascading when online. If your sql server does not take care of cascading you can pass an optional parameter { serverCascades : false} This will delete all child dependent models when the parent is deleted

usage

this.storage = new Offline.Storage("Files", this, { 
        keys: { folderId: folderCollection }, cascadeDelete: true
});

optional usage: if your server does not cascade

this.storage = new Offline.Storage("Files", this, { 
     keys: { folderId: folderCollection }, cascadeDelete: true, serverCascades: false
});

example (for people who speak in code)

One to many relationship with cascade example (One folder to many files)

// Parent
    //Folder Collection
    var FolderCollection = Backbone.Collection.extend({  
        initialize: function (models, options) {
            var self = this;
            self.storage = new Offline.Storage("Folder", self);
        }
    });

    var folderCollection = new FolderCollection({ name: "Folder1"});

 // Child
    //Files Collection
    var FilesCollection = Backbone.Collection.extend({  
        initialize: function (models, options) {
            var self = this;
            self.storage = new Offline.Storage("Files", self, { keys: { folderId: folderCollection }, cascadeDelete: true });
        }
    });

    var fid = folderCollection.models[0].get("id");
    var filesCollection = new FilesCollection([
        { name: "File1", folderId: fid },
        { name: "File2", folderId: fid }
    ]);

// Cascade Delete - destroy the parent folder
    // when a folder is deleted from the FolderCollection there is a listener for the remove event
    // It loops the FileCollection and cascades a deletion on all models that have the folderId as the fk

    folderCollection.models[0].destroy({
        success: function (model, response) {
            // if autoPush is not turned on
            folderCollection.storage.sync.push();
            console.log("cascade complete");
        }
    });

Code to add to backbone.offline source

[... Add to Storage, maybe after this.autoPush = options.autoPush || false; ... ]

// <cascade setup>
this.cascadeDelete = options.cascadeDelete || false; 
if ( this.cascadeDelete == true ) { 

    var parentCollection;
    var cascadeKey;
    var cascadeCollection = this.collection;
    var serverCascades = options.serverCascades || true;

    // loop keys to snag parentCollection and cascadeKey
    var _ref = this.keys;
    for (var field in _ref) {
        parentCollection = _ref[field];
        cascadeKey = field;
    }

    // console.log("parentCollection");
    // console.log(parentCollection);
    // console.log("cascadeKey");
    // console.log(cascadeKey);
    // console.log("cascadeCollection");
    // console.log(cascadeCollection);

    this.cascadeListener(parentCollection, cascadeCollection, cascadeKey, serverCascades);

}
// </ cascade setup >

[... After replaceKeyFields ... ]

///
/// < cascade functions >
/// Added for cascade deletes.
/// Pass in the parentModel that is being deleted.
/// If the model has cascadeDelete options passed it will cascade 
///    deletions down to all dependent child models in the cascadeCollection
///    based on the cascadeKey (this is usually the pk of the parentModel and the fk on the cascadeCollection model)
///    
    Storage.prototype.cascadeListener = function (parentCollection, cascadeCollection, cascadeKey, serverCascades) {
        // bind to this collections remove event so we can cascade deletes down to children
        var self = this;

        parentCollection.on("remove", function (parentModel) {
            console.log("parent model");
            console.log(parentModel);
            self.cascadeOnDelete(parentModel, cascadeCollection, cascadeKey, serverCascades);
        }, self);
    };

    Storage.prototype.cascadeOnDelete = function (parentModel, cascadeCollection, cascadeKey, serverCascades) {

        // when a model from parentCollection is deleted the cascadeOnDelete function is 
        // fired which loops all models in cascadeCollection where cascadeKey equals parentModelId
        // if serverCascades equals false or the parentModel is dirty then the child Models will be deleted
        // The case where the cascade will not occur is if serverCascades = true and the parent model is not dirty, 
        // meaning your sql server will take care of cascading the delteions for you. 

        var parentModelId = parentModel.get("id");
        var dirty = parentModel.get("dirty");

        whereClause = $.parseJSON('{"' + cascadeKey + '":"' + parentModelId + '"}');

        if (dirty == true || serverCascades == false) {
            console.log("whereClause");
            console.log(whereClause);

            _.each(cascadeCollection.where(whereClause), function (childModel) {
                console.log("deleting child model");
                console.log(childModel);
                childModel.destroy();
            });
        }
        else{
            console.log("the server will take of the cascade for you :)");
        }

    };
/// </ end cascade functions >

return Storage;

})();

[...]
reinventit commented 11 years ago

Hi, I'm trying to use this plugin alongside backbone-relational as well, but contrary to asgeo1 I can't seem to make this work for the use case given.

When I fetch my nested models from the server for a collection with offline storage enabled, the 'relations' are not instantiated. I can't figure out why this isn't working, does backbone_offline somehow block the related models from being instantiated? Could this be an issue with my code or is this expected considering that backbone_offline and backbone-relational don't play well together?

Would be really nice if someone could help me out.

asgeo1 commented 11 years ago

Hi @reinvanmeeteren, just answering your pm here.

Basically I created a hack to replace backbone-offline. See here: https://gist.github.com/asgeo1/6774805

Wouldn't call it pretty, but it worked for me.

I'm still hoping the nested model support can be added to backbone-offline, as this is a better library then my hack.

But sharing since a couple of people have asked me what my solution was.