davidgtonge / backbone_query

A lightweight query api for Backbone Collections
MIT License
378 stars 29 forks source link

$elemMatch doesn't work with backbone-relational? #5

Open aroman opened 12 years ago

aroman commented 12 years ago

First off, this project is awesome.

I have data that looks like this:

courses = [
    {
        title: "Biology",
        assignments: [
            {
                done: false,
                due: "2012-03-05T05:00:00.000Z",
             },
            {
                done: false,
                due: "2012-03-05T05:00:00.000Z",
             },
            {
                done: true,
                due: "2012-03-06T05:00:00.000Z",
             }
        ]
    },
    {
        title: "Math",
        assignments: [
            {
                done: true,
                due: "2012-03-05T05:00:00.000Z",
             },
            {
                done: false,
                due: "2012-03-03T05:00:00.000Z",
             },
            {
                done: true,
                due: "2012-03-06T05:00:00.000Z",
             }
        ]
    }

So basically a collection of courses which have a list of assignments.

I'm using backbone-relational to model this; courses is a QueryCollection, the individual course model is a RelationalModel, and the individual assignment model is a RelationalModel as well.

The query I want to do is to find all of the assignments (in all courses) that are due within a specific range and are not done.

From the examples (and being somewhat familiar with Mongo -- it's what I use on the server) I think that would look like this:

courses.query({
    assignments: {
      $elemMatch: {
        "done": true,
        "date": "2012-03-05T05:00:00.000Z" 
      }
    }
  });

(I'm just matching one specific date, not a range, for simplicity purposes).

The problem is that the above query does not return anything. It seems as if it's not applying the $elemMatch criteria to the 'assignments' field of each course.

Any idea why that might be? Feel free to call me names if I'm doing something incredibly dumb here -- my brain is a bit fried from staring at my computer all day ;)

Thanks!

davidgtonge commented 12 years ago

Hi, its a while since I've used Backbone Relational, but if I remember it will set up a backbone collection for each list of assignments. Backbone Query is expecting raw data rather than another collection. So to get this to work you would probable need to define a custom method on your courses model that will return the raw array of the assignments data. You could then use the new computed operator. I'm not at my computer at the moment, but I'll write a more complete example later. I'll also think if there is a more elegant way to combine with backbone relational

aroman commented 12 years ago

Thanks for the fast reply.

I took your suggestion of using computed properties + a static serialization of the collection via a method on the collection (I just used .toJSON). That worked as I would have expected, and it seems like a reasonable workaround.

That said, it doesn't work the way I want it to. elemMatch does what it does in MongoDB; it doesn't return the specific elements matched, it returns the document that has the array it is being called on. Is there any way of having the query return the specific array slices that matched the $elemMatch critera, or do I need to denormalize my data further?

davidgtonge commented 12 years ago

No problem, thanks for the feedback I'm thinking of adding a $relationMatch operator which would avoid the need to create a new method and use the $computed operator, however this would still return the "course" models from your data.

When I've worked with Backbone Relational I've worked with data from a relational database and I got round the problem your having by having for example a global store of all the assignments, and they would each have a course ID. However with your current data structure, I don't think that way would work.

I suppose I could add an operator that would return the child models, but I'm not sure how common the use case if for this. Essentially you'd be querying a collection of courses but would get back a collection of assignments. I'd probably do a custom method to achieve what your trying to do. Something like this:

CourseModel = Backbone.RelationModel.extend({
  relations: {} // Make sure you define a Collection Type for assignments 
                   // that is extended from Backbone.QueryCollection,
  assignment_query: function(date, done) {
     // probably need to check if any assignments were returned before doing the query
    return this.get("assigments").query({
      date: date,
      done: done
    });
  }
});

CourseCollection = Backbone.QueryCollection.extend({
  get_assignments: function(date, done) {
    var assignments = this.map(function(model) {
      return model.assignment_query(date, done);
    });
    return _.flatten(assignments, true);
  }
});

Hope this helps.

aroman commented 12 years ago

+1 for the $relationMatch operator. Though it wouldn't be helpful for my use case, I can imagine it would be nice to at least have a section in the README about working with relational models.

I have actually been thinking about whether I ought to simply have a single non-relational collection of assignments (which is how they are stored in MongoDB -- I do some map/reduce logic to get them into the assignments-per-course structure I work with in Backbone). Thinking along those lines is actually what led me to this project :)

At this point I think I might be able to go either way. It's kind of an optimization question though, so I'll go with the assignments-per-course structure, and go the extra mile to get things for a range of dates.

So that said, your code worked superbly -- several orders of magnitude faster than my previous hack to do the same thing.

I guess I'll stick with that, then, with an eye open for refactoring into a denormalized collection of top-level assignments.

Again, thanks so much for your help and detailed responses.

wulftone commented 12 years ago

You can use the $cb operator to get the query result for a one-level nested attribute in a backbone-relational world:

var arr = courses.query( {
  assignments: {
    $cb: function(attr) {
      return attr.get('done') == true
    }
  }
});

However, it returns a regular array (filled with backbone models), so you'll need to make a new collection if you want to use the results as such:

var finished_assignments = new AssignmentsCollection(arr);

: )