Using $resource for Data Models

InstructorBrett Cassette

Share this video with your friends

Send Tweet

AngularJS's $resource service allows you to create convenience methods for dealing with typical RESTful APIs. In this video, Brett will show you the basics of using $resource, as well as talking about some of the drawbacks with using this service for your data models.

Brett Cassetteinstructor
~ 11 years ago

Excellent question. This is among the shortcomings of working with $resource.

Short answer: I've written a library that deals with modeling. You could simply use that (https://github.com/FacultyCreative/ngActiveResource). This functionality is baked into the search functionality in my library.

Long answer (and I'll be going through more of this in detail as we examine how to build better models than $resource):

To manage multiple resources across controllers, we need to extend the concept of collections. When we have associated collections ($scope.post.comments), the collection itself belongs to the instance (post), and not a particular scope. If we used that same code:

_.remove($scope.post.comments, comment);

It would be updated across scopes. Directives like ng-repeat will also pick up on the change; no need to $scope.apply. That's fairly easy, because it's like we have a single subscriber--the owner of the comment.

Let's say, instead, you're dealing with the top-level resource, e.g. $scope.posts. You use the $resource.query method, and end up with an array that doesn't remove the instances you deleted from it across scopes. Why is that?

All Javascript objects are stored by reference, and there is no way to delete the actual object on the heap, only a pointer to the particular reference. The object will be garbage collected by Javascript when there are no longer any additional references to that object. So as long as it remains in some other array somewhere, it will not be garbage collected or removed from that array.

That leaves us with a few options in Angular. We could iterate through all $scopes, and remove any references to the object we come across. That sounds super expensive, and isn't a great option for us.

The option I think makes the most sense is to unify the interface through which you'll create instances. In $resource, that means the query method, and it necessarily means overriding it. We can create a pub/sub interface on all arrays created and returned by query; when we delete instances on the future, we can loop through each of our subscriber arrays and remove the instance. Again, since Javascript stores only references to objects on the heap, updating the object in a single location will update it in all locations--without calling $scope.apply, because we've updated the core object itself. We must create this pub/sub interface, because otherwise we'd have no means of tracking the pointers:

var originalQuery = angular.copy(Post.query);
Post.watchedCollections = [];

Post.query = function() {
        var results = originalQuery();
        Post.watchedCollections.push(results);
        return results;
}

var originalDelete = angular.copy(Post.delete);

Post.delete = function(instance) {
        originalDelete(instance);
        delete instance['$$hashKey']
        _.each(Post.watchedCollections, function(watchedCollection) {
          _.remove(watchedCollection, instance);
        });
}

If you create your own custom querying methods using $resource (anything with isArray: true set), you'll also want to add those results as subscribers, too.

Hope that helps!

John
~ 11 years ago

To manage multiple resources across controllers, we need to extend the concept of collections. When we have associated collections ($scope.post.comments), the collection itself belongs to the instance (post), and not a particular scope.

Thanks for the response! I've seen this demonstrated in an early egghead video. My thinking is, if we can do this, why not just invent an object to stick the posts on, like $scope.shared.posts? I tried something like that, and it didn't work.

All Javascript objects are stored by reference, and there is no way to delete the actual object on the heap, only a pointer to the particular reference. The object will be garbage collected by Javascript when there are no longer any additional references to that object. So as long as it remains in some other array somewhere, it will not be garbage collected or removed from that array.

Just checking if I follow this: So Post.query() puts an object on the heap, and in our controller we assign $scope.posts to reference that object. Assuming we have two controllers that are both loaded and have both have called $scope.posts = Post.query() -- that means we have 2 references to the same object? So when we call Post.delete() a new object is created by $resource. Both controllers still have reference to the old one. Not a problem for the controller that called Post.delete(), we can just set $scope.posts to refer to the new object, but the other controller is still referring to the old one, and is left hanging in the dust?

The option I think makes the most sense is to unify the interface through which you'll create instances. In $resource, that means the query method, and it necessarily means overriding it. We can create a pub/sub interface on all arrays created and returned by query; when we delete instances on the future, we can loop through each of our subscriber arrays and remove the instance. Again, since Javascript stores only references to objects on the heap, updating the object in a single location will update it in all locations--without calling $scope.apply, because we've updated the core object itself. We must create this pub/sub interface, because otherwise we'd have no means of tracking the pointers:

So instead of looping through all $scopes which you explained is expensive, we loop through only the subscribers, a shorter and complete list (no waste)?

Look forward to more $resource videos in the future, especially ones that deal with best practices for handling async gracefully and transparently to the user for the most 'desktop' like experience possible.

Brett Cassetteinstructor
~ 11 years ago

So Post.query() puts an object on the heap, and in our controller we assign $scope.posts to reference that object. Assuming we have two controllers that are both loaded and have both have called $scope.posts = Post.query() -- that means we have 2 references to the same object? So when we call Post.delete() a new object is created by $resource. Both controllers still have reference to the old one. Not a problem for the controller that called Post.delete(), we can just set $scope.posts to refer to the new object, but the other controller is still referring to the old one, and is left hanging in the dust?

Not by default. By default, every time you query, the result returned is a new array. Even if its contents are 100% identical to the previous query, they are not identical objects. That's why I track special arrays in ActiveResource (like post.comments), and why we need to wrap Post.query in the other example I gave :)

Brett Cassetteinstructor
~ 11 years ago

Thanks for the response! I've seen this demonstrated in an early egghead video. My thinking is, if we can do this, why not just invent an object to stick the posts on, like $scope.shared.posts? I tried something like that, and it didn't work.

This is a good idea. You need to make sure you're always assigning the posts to that exactly array. Query creates a new array by default, so you need to hook into that method, and write any new instances to your shared array.

If you assign the variable $scope.shared.posts to a different array, recognize that now the pointer has switched to a different array, not changed the value of the previous array.

The approach you've described is the approach I showed in the previous example, applied in a different way. See if you can use that example to figure out how to do this :)

David
~ 11 years ago

Hi Brett, great video! Do you have any idea when you plan to release the video on creating a "more robust ORM?" Could I sign up to be emailed when that becomes available somehow, or can you publish a notification in this thread so I get the subscribe message?

Does ngActiveResource address any of those ORM concerns, or is that separate?

Thanks and keep up the good work!

Dylan
~ 10 years ago

If I understand correctly, your argument against using $resource for Data Modeling is twofold: sharing collections across $scopes is unintuitive and potentially dangerous, and it's tough to define "model-ly" things like behavior and relational mapping.

The complications of sharing collections are certainly a drawback to using just $resource for modeling. It begins to violate Single Responsibility; $resource is inherently Entity and Entity Repository (not a huge deal), but adding something like Post._all or Post._current adds concerns traditionally assigned to Managers, Caches, Mappers, or other encapsulated system actors.

However, in re: "One of the most conspicuous areas where $resource is lacking is its ability to let us create business logic...", it's worth pointing out that $resource can be effectively used to shape a model, map its relationships, and to define its behaviors. We can leverage prototypical inheritance as described in Mastering Web Application Development with AngularJS, e.g.:

var Post = $resource(//...);
Post.prototype.comments = [];
Post.prototype.addComment = function (comment) {//...}

That being said, you'd be tightly coupling your entire domain model---not just to Angular, but also to one specific Angular service. I'll be very interested to see your ORM progress, because I haven't come across many efforts to handle this unavoidable application concern in a way that is idiomatically "Angular". Great stuff!

Igor
~ 10 years ago

@Brett could there be a simpler wrapper around $resource which provides model specific behavior.

Here is one I came up with (thanks SO for ideas): http://stackoverflow.com/questions/23528451/properly-wrap-new-domain-object-instance-with-resource/23529358#23529358, but I'd be curious is there is more concise/clearer method.

Igor

Brett Cassetteinstructor
~ 10 years ago

Hey David- The videos are a part of this series, which is shaping up to be quite a long one. The topics covered are rather large and diverse, and I want to do justice to the complexity of each. At the time of this writing, we're spending a lot of time on component pieces that are being used to compose the system as a whole.

-Brett

Brett Cassetteinstructor
~ 10 years ago

Hey Dylan- Sorry my responses have been so long coming. You're correct about my concerns with Angular's modeling capabilities out of the box. I'll be quite interested to see what the truly "Angular" approach is when the core team release v2, which is slated to include a new modeling library last I heard.

We'll be working through many challenges not covered in the current ngActiveResource as we proceed. After iterating through many pre-1.0 releases, I have a lot of new ideas up my sleeve for the series :)

Brett Cassetteinstructor
~ 10 years ago

Hey Igor-

The thing to remember about Angular is it's just Javascript :P Here's a list of "classical" inheritance patterns in Javascript from Douglas Crockford, some spins of which have been shown in this series. My personal favorites are the "more promiscuous" (as Crockford calls them) styles of multiple inheritance that are popular in the Ruby world, and are covered in the Inherits/Extends video ( https://egghead.io/lessons/angularjs-refactor-the-model-base-class-with-mixins). Please share other ideas you come up with in the thread :)

Dmitri
~ 10 years ago

Can you PLEASE shed some light on the future of ngActiveResource project. There haven't been any updates since April 2014 and we afraid to pick it as it looks abandoned. Thank you very much.

Brett Cassetteinstructor
~ 10 years ago

Hey Dmitri-

I would view ngActiveResource primarily as a learning tool. When I write front-end code, I do use it, but I also feel very comfortable working through new features as I require them.

There is a much more up-to-date version than the one you see (end of last year), but I have trouble getting my old company to merge my new code to the old codebase, so I've been maintaining another copy here (https://github.com/brettshollenberger/the-abstractions-are-leaking).

Either way, my day job is now primarily server-side, and I only sparingly have need to work on the project. I would love to see some new maintainers step up, but until such a time arises, I would view the codebase as a way to learn about the concepts of data modeling, and use a more actively maintained project.

Best, Brett

Dmitri
~ 10 years ago

Thank you Brett. The team was too concerned about using the unsupported library in prod app, despite how attractive it looked. We went with js-data (http://www.js-data.io) instead.

Brett Cassetteinstructor
~ 10 years ago

I think that's a good choice Dmitri :)

Brett Cassetteinstructor
~ 10 years ago

I think that's a good choice Dmitri :)

milad
~ 9 years ago

I hate it , I truly hate it when you're speaking fast and coding fast without really explaining what's happening . It's like you wanna pee all the time so you're coding fast !

Jonah
~ 9 years ago

Hello, can you provide the source code for app.js? I assumed it was just something like this..

angular.module('app', []);

.. but I am getting an unknown provider error.

Also did I miss the setup for the api? Is that in another course?

Joel Hooks
~ 9 years ago

It's there, in main.js.