Backbone model.destroy with MongoDB persistence - javascript

I'm new to Backbone and to MongoDB. I'm running into problems destroying models (that are backed by MongoDB). I believe it has something to do with the fact that a MongoDB "document" uses the abnormal _id attribute and is not wired up with Backbone by default. I've tried setting idAttribute: '_id' which does not seem to solve the issue.
A sample Mongo document looks like:
{
_id: Object
$oid: "527303e82f3504ba5bf4b21f"
__proto__: Object
feeling: "ok"
location: "california"
name: "Kevin"
}
models.coffee
class Models.Campaign extends Backbone.Model
urlRoot: "http://localhost:4567/api/campaigns"
# setting idAttribute: '_id' causes issues rendering the Backbone collection
controller.coffee
...
model.destroy() #=> only removes the item from the dom but does not make a DELETE request
Am I able to pass in an id to the destroy() function?
Does anyone have advise in how to properly wire up Backbone with MongoDB without having to monkeypatch a ton of built-in Backbone functions?

I think you'll have an easier time if you add a parse to your model to sort out the confusing data you're getting from the server. You don't need or care about _id, you do care about $oid, and Backbone would prefer to work with an id property; you can sort out all three with a simple parse in your model like this:
parse: function(response) {
var parsed = _(response).pick(
'feeling',
'location',
'name'
);
parsed.id = response.$oid;
return parsed;
}
Demo: http://jsfiddle.net/ambiguous/pn773/
You'd probably be fine if your parse modified response rather than making a semi-copy using _.pick and modifying that copy, however, parse doesn't technically own response so it would be rude to change it and there could be surprising side effects. Good habits, etc.
Then when Backbone tries to convert the server's data to a model, it will end up working with:
{
id: "527303e82f3504ba5bf4b21f",
feeling: "ok",
location: "california",
name: "Kevin"
}
and everyone should be happy:
Backbone won't see the irrelevant _id.
Backbone won't see $oid and you won't need to set idAttribute to anything special.
Backbone will see an id like it expects and all the standard machinery will use the BSON ObjectId as the model's unique identifier.
The simple method should get you your DELETE /api/campaigns/527303e82f3504ba5bf4b21f request when you model.destroy().

Related

Backbone fetching data from model vs collection

I created a simple backbone project, where it fetches all books details and show it in UI. I am fetching all the books details from the model. not at all using collection something like this
var BookModel= Backbone.Model.extend({
initialize: function(){
this.fetchData();
},
fetchData: function(){
this.url= "/get/all_books";
this.fetch({
success: success_callback,
error: error_callback
})
}
});
This is working fine. But why do I have to use collections ? If I would have used collection it would be something like as follows
var BookModel= Backbone.Model.extend({
defaults:{
id:'',
name: '',
author: ''
}
});
var BookCollection= Backbone.Collection.extend({
model: BookModel,
initialize: function(){
this.fetchData();
},
fetchData: function(){
this.url= "/get/all_books";
this.fetch({
success: success_callback,
error: error_callback
})
}
});
The same effect. I don't understand why to use Collections in my case. please help me to understand this concept with my example why do I have to use collections here. I Googled a lot and could find the best answer.
Thanks
Imagine that you have two 2 routes:
/books
/books/:id
Now for getting a specific book you can send a request to /book/:id route, where :id is the id of the book.
GET /books/1
< { id: 1, title: 'On the Genealogy of Morality', ... }
Now what happens if you want to get all the books? You send a request to /books route.
GET /books
< [{ id: 1, title: '...', ... }, { id: 2, title: '...', ... }, ...]
Backbone follows the same principle. Model for a single book. Collection for many books. When you use a collection, Backbone creates one Model for each book. Using a Model for more than one item is wrong.
You said "Backbone creates one Model for each book.". at what step it creates?
It creates the models on the sync event, i.e. when the request for getting all the items is complete.
...how does it helps me. In my case I always fetch all books, not single book.
Backbone Collections always use Backbone Models. If you don't set the model property of the Collection explicitly, Backbone uses a normal Model, i.e. the model property of a Collection should always refer to a Model.
// https://github.com/jashkenas/backbone/blob/master/backbone.js#L782
// Define the Collection's inheritable methods.
_.extend(Collection.prototype, Events, {
// The default model for a collection is just a **Backbone.Model**.
// This should be overridden in most cases.
model: Model,
Consider a Model as an object and a Collection as an array of objects.
More than anything, dividing your data into logical units (models) and grouping them into categories (collections) make it easy for you to reason about, manipulate and change your data. Once you build something that is even just a tiny bit more complex than what you have built, this becomes a priority. The point isn't that you are getting some magic functionality that you couldn't otherwise get. It's all javascript after all. The point is that models and collections provide data-structuring that is helpful when building dynamic applications. In fact that's the whole point of backbone and MV* in general, to provide helpful tools and abstractions. Collections are a solution to a whole host of problems that you will run into later down the road, when you add even the tiniest bit of extra complexity to your app.
So, you ask why you have to use collections and I guess the answer that you already knew is, you don't have to use collections. In fact, it sounds like you don't need to use an MV* library at all.

'Backbone Relational' model relation adds an 'id' with a collection as a value, preventing PUT and DELETE

I successfully implemented loading and showing relations with 'Backbone Relational' from an API I created. I get how things work by trial and error. I do think the docs are lacking some clarity though since it took a lot of time to figure out how things work. Especially on how to map things to the API I think the docs are lacking a bit.
Problem
Adding a bookmark works, it's the editing and deletion that don't work. The PUT becomes a POST and the DELETE simply doesn't fire at all. When I set an id to the model hardcoded it does work. So the id is missing which makes sense for the PUT becoming a POST.
The problem seems to be that the id doesn't hold an actual id, but a collection. The view where the problem occurs does not requires the BookmarkBinding, it's used somewhere else. Simply the fact that it has Bookmark as a relation makes the DELETE and PUT break.
BookmarkBinding model:
App.Model.BookmarkBinding = Backbone.RelationalModel.extend({
defaults: {
set_id: null,
bookmark_id: null
},
relations: [{
type: Backbone.HasOne,
key: 'bookmark',
relatedModel: 'App.Model.Bookmark',
reverseRelation: {
type: Backbone.HasOne,
key: 'id'
}
}],
urlRoot: 'http://api.testapi.com/api/v1/bookmark-bindings'
});
Bookmark model:
App.Model.Bookmark = Backbone.RelationalModel.extend({
defaults: {
url: 'undefined',
description: 'undefined',
visits: 0,
},
relations: [{
type: Backbone.HasMany,
key: 'termbindings',
relatedModel: 'App.Model.TermBinding',
reverseRelation: {
key: 'bookmark_id',
}
}],
urlRoot: 'http://api.testapi.com/api/v1/bookmarks'
});
From Backbonejs.org
The default sync handler maps CRUD to RESTful HTTP methods like so:
create → POST /collection
read → GET /collection[/id]
update → PUT /collection/id
delete → DELETE /collection/id
Your question suggests that you're making an HTTP PUT request, and therefore a Backbone update. If you want to make an HTTP POST, use Backbone create. The PUT request maps onto update, and requires that an id be sent in the URL, which isn't happening according to your server log. If your're creating a new object, then most server-side frameworks such as Rails / Sinatra / Zend will create an id for the object
Another possible source of error is the keys that you chose for the relations, like you suspected.
A Bookmark has many BookmarkBindings, and it seems that Backbone-relational will store them in the field that you specify in BookmarkBindings.relations.reverseRelation.key, which is currently defined as 'id'.
So the collection of related BookmarkBindings ids will to be stored on the same attribute as the Bookmark.id, creating a collision. Backbone.sync will send an undefined value to the server (which you see in your logs), because it finds a collection there instead of an integer.
First suggestion - You may not need a bidirectional relation, in which case drop it from the BookmarkBinding model.
Second suggestion - define the reverse relation on another key, so that it doesn't collide with Bookmark.id, such as BookmarkBindings.relations.reversRelation.key : 'binding_ids'
due disclosure - I've never used Backbone-relational.js, only Backbone.js.
The problem was that on editing or deleting the bookmark model, the bookmark binding model wanted to do it's work too since it is related too the bookmark from it's side. I already tried to remove the reverse relation which didn't prove to be a solution since in the other part of my application where I used the bookmark bindings things wouldn't work anymore.
Solution
I did end up removing the reverse relation (#jarede +1 for that!), but the crux was how to implement the foreign key to fetch relations from the API without a reverse relation. I ended up adding the keySource and keyDestination which made everything work out.
Sidenote
Backbone Ralational cannot handle identical foreign keys either, this gave me some problems too. This will make the lastly declared foreign key overwrite all the previous ones. This can be quite impractical since within an API it's not uncommon that model's are related to a column named id. So the idAttribute can be set with idAttribute: '_id' for example, but the foreign key has to be unique across your application.
BookmarkBinding model:
App.Model.BookmarkBinding = Backbone.RelationalModel.extend({
defaults: {
set_id: null,
bookmark_id: null
},
relations: [{
type: Backbone.HasOne,
key: 'bookmark',
keySource: 'id',
keyDestination: 'bookmark',
relatedModel: 'App.Model.Bookmark'
}],
urlRoot: 'http://api.testapi.com/api/v1/bookmark-bindings'
});

Creating and updating nested objects in Ember

I got nested JSON data from the server like this:
{
name: "Alice",
profile: {
something: "abc"
}
}
and I have the following model:
App.User = Ember.Object.extend({
name: null,
profile: Ember.Object.extend({
something: null
})
})
If I simply do App.User.create(attrs) or user.setProperties(attrs), the profile object gets overwritten by plain JS object, so currently I'm doing this:
var profileAttr = attrs.profile;
delete attrs.profile
user.setProperties(attrs); // or user = App.User.create(attrs);
user.get('profile').setProperties(profileAttrs);
It works, but I've got it in a few places and in the real code I've got more than one nested object, so I was wondering if it's ok to override User#create and User#setProperties methods to do it automatically. Maybe there's some better way?
Based on your comment, you want the automatic merging behaviour you get with models (the sort of thing you get with .extend()). In that case, you could try registering a custom transformer, something like:
App.ObjectTransform = DS.Transform.extend({
deserialize: function(json){
return Ember.Object.create(json);
}
});
App.User = DS.Model.extend({
profile: DS.attr('object')
});
See: https://github.com/emberjs/data/blob/master/TRANSITION.md#json-transforms
If you are doing your server requests without an adapter you can use the model class method load() with either an array of json objects or a single object. This will refresh any known records already cached and stash away the JSON for future primary key based lookups. You can also call load() on a model instance with a JSON hash as well but it will only update that single model instance.
Its unclear why you are not using an adapter, you can extend one of the Ember Model adapters and override the the record loading there, eg. extend from the RESTAdapter and do any required transform on the JSON if required by overriding _loadRecordFromData
You can also override your models load function to transform data received if required as well. The Ember Model source is fairly easy to read so its not hard to extend to your requirements.

Backbonejs - use nested object as id attribute

Is it possible to use a nested object as the id attribute in Backbone?
For e.g. something like,
var MyModel = Backbone.Model.extend({
defaults : {
'info': {
'name': ""
},
},
idAttribute: "info.name"
}
BTW, the above doesn't work as an ID, I added it here just to give an idea of what I was trying to achieve.
TIA
I don't think you can directly assign a nested object as an idAttribute .
But you can directly set the id on the model when the response is served by the server in the parse method
parse: function(response) {
response.id = response.info.name;
return response;
}
as #Sushanth says, parse is for sure a good way to do this.
But generally using nested objects within Backbone Models is unsafe and not the best way to do it. When you change your response.info.name property, and you bind events to response.info, you will not get notified.
After a few years with Backbone, I would like to tell you, that parsing your received models from server into primitive objects is the best you can do.
If you want your models to go back to server exactly the same way they came in, you could override the toJSON function, which could transform it back. Of course, you need to implement those two... :/

Persisting & loading metadata in a backbone.js collection

I have a situation using backbone.js where I have a collection of models, and some additional information about the models. For example, imagine that I'm returning a list of amounts: they have a quantity associated with each model. Assume now that the unit for each of the amounts is always the same: say quarts. Then the json object I get back from my service might be something like:
{
dataPoints: [
{quantity: 5 },
{quantity: 10 },
...
],
unit : quarts
}
Now backbone collections have no real mechanism for natively associating this meta-data with the collection, but it was suggested to me in this question: Setting attributes on a collection - backbone js that I can extend the collection with a .meta(property, [value]) style function - which is a great solution. However, naturally it follows that we'd like to be able to cleanly retrieve this data from a json response like the one we have above.
Backbone.js gives us the parse(response) function, which allows us to specify where to extract the collection's list of models from in combination with the url attribute. There is no way that I'm aware of, however, to make a more intelligent function without overloading fetch() which would remove the partial functionality that is already available.
My question is this: is there a better option than overloading fetch() (and trying it to call it's superclass implementation) to achieve what I want to achieve?
Thanks
Personally, I would wrap the Collection inside another Model, and then override parse, like so:
var DataPointsCollection = Backbone.Collection.extend({ /* etc etc */ });
var CollectionContainer = Backbone.Model.extend({
defaults: {
dataPoints: new DataPointsCollection(),
unit: "quarts"
},
parse: function(obj) {
// update the inner collection
this.get("dataPoints").refresh(obj.dataPoints);
// this mightn't be necessary
delete obj.dataPoints;
return obj;
}
});
The Collection.refresh() call updates the model with new values. Passing in a custom meta value to the Collection as previously suggested might stop you from being able to bind to those meta values.
This meta data does not belong on the collection. It belongs in the name or some other descriptor of the code. Your code should declaratively know that the collection it has is only full of quartz elements. It already does since the url points to quartz elements.
var quartzCollection = new FooCollection();
quartzCollection.url = quartzurl;
quartzCollection.fetch();
If you really need to get this data why don't you just call
_.uniq(quartzCollecion.pluck("unit"))[0];

Categories