The ability to reply to discussions is limited to PRO members. Want to join in the discussion? Click here to subscribe now.

Using $resource for Data Models

Using $resource for Data Models

5:29
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.
Watch this lesson now
Avatar
egghead.io

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.

Avatar
John

_.remove($scope.posts, post)} will remove from $scope.posts, but what if i'm using Posts in another controller? After calling an update, insert, or delete, what is the best approach for updating all controllers that are using the Post factory ?

My thinking is we need to have a callback within the factory that calls Post.query() again, but would that trigger the controllers to update their models?

In reply to egghead.io
Avatar
Brett Shollenberger

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!

Avatar
John

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.

In reply to Brett Shollenberger
Avatar
Jose Luis Monteagudo

Hello,

I would like to expose a problem that I'm facing sending complex objects as parameters to ngResource. I'm trying to do the following query with ngResource (I know I shouldn't do this in my controller, but it's only a spike):

var Customer = $resource('http://localhost:3000/api/users');
        var params = 
        {
            conditions: {
                age: { "$gt": 30 }
            },

            options: {
                limit: 2
            }
        };

        Customer.query(params, 
            function(data) {
                $scope.users = data;
            }
        );

And in the browser console I get the following error:

GET http://localhost:3000/api/users?conditions=%7B%22age%22:%7B%7D%7D&options=%7B%22limit%22:2%7D 400 (Bad Request)

As you can see, the parameters that I'm sending are being escaped, and this is causing a bad request.

If I try to do the following request through $http I don't get any problem:

$http.get('http://localhost:3000/api/users?conditions={"age":{"$gt":30}}&options={"limit":2}').success(function(data) {
                    $scope.users = data;
        });

So, I have two questions:

1) Is there anyway to avoid the problem that I'm facing when sending complex parameters to ngResource.

2) Do you think that is a good practice sending parameters as I'm doing in my previous examples? Or do you think that would be better in this way: http://localhost:3000/api/users?age-gt=30&limit=2 ? Undoubtedly, this last way is much cleaner, but I think that in my previous examples I have a lot of flexibility, because this is the way that MongoDB uses to query its collections, and as I have a MongoDB in my back-end then I can send directly the parameters to the MongoDB query.

I didn't like brackets in the URL, but I have seen that parse.com uses a similar syntax to query its data:

curl -X GET \
  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
  -G \
  --data-urlencode 'where={"score":{"$gte":1000,"$lte":3000}}' \
  https://api.parse.com/1/classes/GameScore

I would appreciate a lot your comments regarding these two questions.

Thank you very much!!

In reply to egghead.io
Avatar
Brett Shollenberger

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 :)

In reply to John
Avatar
Brett Shollenberger

Hey Jose-
I've run into this issue before with MongoDB, but I haven't specifically written an adapter to deal with it. My general thoughts are that $resource's url encoding is a feature, not a bug. They're suggesting a particular approach to API design, which can be challenging because it's the wild west in the wonderful world of APIs.

So I wouldn't say you need to avoid this challenge unless you don't have control over your API, in which case, I would write a method to parameterize the query string in the way you showed in your first example. You're exactly right, you'll have greater flexibility working with the grain of MongoDB's query interface. While your second method is clean, it's a proprietary API. It's likely developers working on your app will know MongoDB, but they'll absolutely have to learn your own API if you write something different.

If you do have control over your API, I would decode the URL on the backend, and use the request generated by $resource. Many of the characters in the request are reserved and ought to be percent encoded.

Be careful how you expose these endpoints. You don't want just anyone to be able to query arbitrarily into your database :)

In reply to Jose Luis Monteagudo
Avatar
Brett Shollenberger

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 :)

In reply to John
Avatar
Jose Luis Monteagudo

Hello Brett,

Thank you for your response.

I have already seen where is the problem although I have not resolved it yet. I'm sending the following condition to my endpoint:

...
conditions: {
                age: { "$gt": 30 }
            },
...

The problem is that when Angular sends that request to my API endpoint, Angular removes those fields that starts with $. So Angular sends the following information to my endpoint: conditions={age:{}} If I send this information to MongoDB then it returns an error.

I dont' know still how to avoid this problem. Maybe it's easier to use my own propietary API, like in this way: http://localhost:3000/api/users?age-gt=30&limit=2

Best regards!

In reply to Brett Shollenberger
Avatar
Brett Shollenberger

You're right, Jose. Creating your own API to work with $resource could solve this scenario, but I think you would get better mileage out of either wrapping $resource and changing it to allow your $ arguments through than you would parsing \w-gt on the backend, since it would allow you to stick to MongoDB conventions on the frontend. Either way, I think you're on the right track solving this.

In reply to Jose Luis Monteagudo
Avatar
David

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!

Avatar
Dylan

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!

Avatar
Igor

@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

Avatar
Brett Shollenberger

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

In reply to David
Avatar
Brett Shollenberger

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 :)

In reply to Dylan
Avatar
Brett Shollenberger

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 :)

In reply to Igor
Avatar
Dmitri

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.

Avatar
Brett Shollenberger

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

In reply to Dmitri
Avatar
Dmitri

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.

In reply to Brett Shollenberger
Avatar
Brett Shollenberger

I think that's a good choice Dmitri :)

In reply to Dmitri
HEY, QUICK QUESTION!
Joel's Head
Why are we asking?