Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Identity Map for Backbone.js Models (supportbee.com)
30 points by prateekdayal on Nov 25, 2011 | hide | past | favorite | 17 comments


Naturally, in most cases you'll want to only have a single representation of any given model on the client-side at a time. However -- there's really no need to do something this (https://gist.github.com/1393465) fancy to get this behavior.

Instead of making a new Ticket with an existing ticket's "id" ... just use the existing ticket. To alter SupportBee's example:

    var ticketView = new SB.Views.TicketView({
      model: SB.Tickets.get(ticketId)
    });
prateekdayal responds, elsewhere in this thread:

    > Because the collection could be inside another object. 
    > Also there would be other places in the app where 
    > ticket models are being initialized. By defining/having 
    > to refer to global variables, you are just writing code 
    > that is harder to maintain.
    > 
    > The identity map approach is more like Rails where in 
    > a single request, the same SQL does not hit the db again 
    > and just returns the pre-fetched results. You don't have 
    > to think about where else you are fetching the same 
    > records ever.
Not quite. Being "more like Rails" doesn't always make more sense, when you're talking about a JavaScript application. In this case, you're creating an implicit global list for all Tickets, regardless of collection -- there's nothing that's less "global" about it.

The global lookup is simply hidden inside of the extension, and subtly changes the behavior of "new". Now, when I write: "new Ticket(id)", will I get a new Ticket? Maybe, maybe not -- it depends on the id, and whether the client happens to have a cached copy. Clearer patterns for this include:

    Tickets.get(id) || new Ticket;

    Tickets.findOrCreate(id);
... and so on.


While this is correct for cases where you need to access a single ticket, there are two use cases where you need a deeper approach:

1) Entity relationships; ie., one-to-one, one-to-many and many-to-many relationships. For example, if you have a list of Task models, each with a "creator" property that references a User model, then you want to each creator to be loaded only once, and always refer to the same object.

Last I checked, Backbone did not support relationships, so this becomes awkward. I solved it in my Backbone 0.5.2 app by preloading the complete /users collection; then for each Task model, I looked up the creator_id attribute in that collection. Luckily I don't currently have many relationships to juggle, but it's quite brittle and manual at the moment even with just a few.

2) Dynamic resource collections. For example, good REST practice dictates that server-side-filtered collections should be expressed with query strings: /tasks, /tasks?status=active and /tasks?status=completed are three different views on the same logical collection of tasks. For some apps, this kind of filtering can be done client-side; but what when the dataset is too big and the possible set of dimensions (eg., free text) is complex? Then you will need server-side ad-hoc querying.

I haven't solved this one yet, and currently do some manual reloading whenever data changes, in order to keep collections in sync. But in my opinion, in order to solve this one, you will need an identity map solution.

Unfortunately, Backbone's code seems to have been structured not for OO cleanness, but for size. It's all pretty tangled and ad hoc, and not really designed for extensibility. (I haven't looked at the code for prateekdayal's code, perhaps it can be accomplished cleanly.)


Thanks for the comment Jeremy. What about this case (copying from my comment on my blog)

"Also a ticket could be present in two different collections fetched from different urls. For example, the same ticket (id) could be in /tickets/all_tickets and /tickets/my_tickets.

The book-keeping just keeps getting harder as your app grows"


Sure -- let's look at this particular case. In your implementation, you're using the "name" property of the constructor function to namespace lookups in your global cache:

    instance = Backbone.CacheStore.get(this.name + "/" + attributes.id);
... relying on the "name" property simply won't work in older versions of IE, unfortunately.

Here's one quick sketch of a way to build a "findOrCreate" that you can use on all of your ticket collections -- and also have a reference to all of your client-side loaded tickets if you ever need it.

    SB.TicketCollection = Backbone.Collection.extend({

      findOrCreate: function(id) {
        var model = SB.Tickets.get(id) || new SB.Ticket({id: id});
        this.add(model);
        return model;
      }

    });

    SB.Tickets = new SB.TicketCollection;
Now, whenever you use an SB.TicketCollection, you can get the appropriate behavior with "findOrCreate". This particular pattern isn't terribly useful for many applications because a model that consists of only an ID doesn't give you much to work with. But perhaps I'm not fully understanding your use case.


Let me explain my use case. We have several listings (Unassigned, My Tickets, All Tickets etc). A list view initializes a TicketList collection and then does the usual rendering etc. Also, once we render a listing we cache the rendered view so moving between them is snappy. Every listing basically fetches from a collection url (for instance /tickets/unassigned, /tickets/my_tickets etc).

Let us say a ticket with id 10 is in two listings. If we do a .fetch() on one of the listings and the ticket with id 10 is updated (let's say the replies_count changed in the server), not only is the view corresponding to this listing updated, it is also updated for the other listings.

If every listing was based on the same TicketList collection object, your solution would work well. However since different listings use different TicketList objects (initialized with a different url), I came up with the identity map solution.


Have you considered filtering of "all tickets" in the client, for different subsets? Eg http://stackoverflow.com/q/6865316/172188


What would you do to solve this model-syncing in a situation where your model consisted of more than an id?


Putting these patterns and explanations in the backbone documentation would really be beneficial to the community.


This seems like a useful addition. I hope it will gain traction (if the impl is sober, I haven't tried it out yet).

My main issue with backbone are the Collection shortcomings (like this one) which require just the kind of boilerplate-workarounds that are so easy to get subtly wrong.

Another example would be the lack of infrastructure for nested Collections (e.g. cf. https://github.com/documentcloud/backbone/issues/483#issueco...).

I understand that backbone doesn't want to grow fat in the core. However it would be nice to see a standard library emerge to cover these very common requirements (perhaps call it "ribcage"?).


> if the impl is sober, I haven't tried it out yet

The way Backbone is structured (or this could be my limited understanding of it), you have to hack it at a very low level to get something like this going. Some people may have concerns with respect to Backbone.js upgrades breaking this. However, if you have test coverage in your app, upgrading backbone.js should not be an issue. It has worked well for us from 0.3.3 through 0.5.3 for example.


> One way to avoid the problem is to remember to pass the ticket object from the collection to the model and not instantiate a new one.

My initial thought exactly.

> However, as your app grows complex, you will not be able to keep a track of all the places where an object with a particular id is being initialized.

That's a bit vague. Why not?

This seems not much more than trying to standardize a global registry lookup for your models. Why not just make your collection globally accessible, and reference the fetched model with

    $app.tickets.get(id)

?


> This seems not much more than trying to standardize a global registry lookup for your models. Why not just make your collection globally accessible, and reference the fetched model with

Because the collection could be inside another object. Also there would be other places in the app where ticket models are being initialized. By defining/having to refer to global variables, you are just writing code that is harder to maintain.

The identity map approach is more like Rails where in a single request, the same SQL does not hit the db again and just returns the pre-fetched results. You don't have to think about where else you are fetching the same records ever.


Also a ticket could be present in two different collections fetched from different urls. For example, the same ticket could be in /tickets/all_tickets and /tickets/my_tickets.

The book keeping just keeps getting harder as your app grows


> This is where the problem starts. You now have two instances of the model for the same ticket id.

The problem is your architecture. The simplest solution is to always use a collection cache. The reason you separate views and collections is so that you only have each object in one place, you are killing the separation here by gluing them back together.


The description of the solution sounds like a Factory to me. Is the Identity Map any different to a Factory approach?


Yes, they are different. The Factory Pattern is a pretty basic creational pattern. An Identify Map is more specialized. It specifically exists to avoid having multiple instances of the same entity.

An identity map doesn't create objects, but it does cache and short-circuit the creation.

If you did:

   id = 1
   userA = Factory.create(:user, id)
   userB = Factory.create(:user, id)
You should reasonably expect userA and userB to be different instances. If you asked an Identity Map for the same thing, they'd be the same instance.


Sounds more like singleton pattern. The concept is interesting, but the implementation is very intrusive Every new release of "Backbone" they must be merge his changes




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: