

Identity Map for Backbone.js Models  - prateekdayal
http://devblog.supportbee.com/2011/11/25/identity-map-for-backbone-js-models/

======
jashkenas
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.

~~~
prateekdayal
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"

~~~
jashkenas
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.

~~~
prateekdayal
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.

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

------
moe
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...](https://github.com/documentcloud/backbone/issues/483#issuecomment-1595018)).

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"?).

~~~
prateekdayal
> 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.

------
crescentfresh
> 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)
    
    ?

~~~
prateekdayal
> 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.

~~~
prateekdayal
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

------
nikcub
> 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.

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

~~~
latch
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.

