Skip to content

Loading…

There is no good way to do findQuery with data binding to fetch just one record #551

Closed
darthdeus opened this Issue · 26 comments

10 participants

@darthdeus
Ember.js member

If I have a view that looks like this

App.UserLinkView = Ember.View.extend({

  user: function() {
    return App.User.findByUsername(this.get("username"));
  }.property(),

  template: Ember.Handlebars.compile('<a {{action showUser view.user href=true}}>@{{view.username}}</a>')
});

and use it like this

{{view App.UserLinkView username="wycats"}}

there is no good way of implementing the findByUsername method. For example if I try to do

App.User.reopenClass({
  findByUsername: function(username) {
    return App.store.findQuery(App.User, { username: username });
  }
});

it won't work, becuase findQuery expects to receive multiple records, not just one. Which means the server implementation should be something like this

def index
  respond_with User.find_all_by_username(params[:username])
end

This won't work though, because the view is expecting just a single instance of a App.User model.

A hackish solution to this would be to do App.store.find(App.User, username) which works, but it seems that Ember Data will store the ID you use to search by, so if you later use the user in a route, it would look like /users/wycats instead of /users/1

@tchak
Ember.js member

I have something along these lines in my app:

App.UserController = Ember.ObjectController.extend({
  _content: null,

  content: function(key, value) {
    var content = this.get('_content');
    if (arguments.length === 2) {
      content = Ember.makeArray(value);
      this.set('_content', content);
    }
    return Ember.get(content, 'firstObject');
  }.property('_content.firstObject')
});

// both will work
App.UserController.set('content', App.User.find(1));
App.UserController.set('content', App.User.find({username: 'toto'}));

Handeling it on the controller level is probably the only way, as we could not return a model object without id... We would have no way to re-associate it later. Imagine you do :

var user1 = App.store.findQuery(App.User, { username: 'toto' });
var user2 = App.store.findQuery(App.User, { username: 'toto' });

How could you ensure user1 === user2 ?

@darthdeus
Ember.js member

@tchak The issue is that even if I return the ID, it will use the queried username as the ID, not the returned parameter with key "id".

@darthdeus
Ember.js member

I'm not sure I understand the implementation though. Why are you expecting key, value in the content. Shouldn't it be just value?

In the way I need to use this, I can't really rely on a controller, as I need to bind it in the view (the example in the issue). I'm not really sure how to do that.

@darthdeus
Ember.js member

Another issue that comes to mind is that if I fetch the user like you showed, it won't get cached. Which means another time when I need to find a user by the same username, it will do the same request again, because the identity map only works with IDs, right?

It would probably require a separate method like findOne which would take into consideration that the parameters which I'm using to search can be used to identify the record, so I if request a record with the same parameters again, it will just give me the locally stored one.

I'm not sure if that's possible to do performance wise though, but it might be possible easy do with one parameter.

@darthdeus
Ember.js member

@tchak Also I've been trying to reproduce the example you gave me, but really without any luck

Scvrush.UserLinkView = Ember.View.extend({
  tagName: "span",

  users: function() {
    return Scvrush.store.find(Scvrush.User, { username: this.get("username") });
  }.property("username"),

  user: function() {
    return this.get("users.firstObject");
  }.property("users.@each"),

  template: Ember.Handlebars.compile('<a {{action showUser view.user href=true}}>@{{view.username}}</a>')
});

and invoking the view as

{{view Scvrush.UserLinkView username="someone"}}

and I'm just getting undefined every time, both in users and user properties, even though the request is made and correct data returned.

@darthdeus
Ember.js member

Looks like my example was right all along, I just needed to re-render the view on the right time.

Scvrush.UserLinkView = Ember.View.extend({
  tagName: "span",

  users: function() {
    return Scvrush.store.find(Scvrush.User, { username: this.get("username") });
  }.property("username"),

  user: function() {
    return this.get("users.firstObject");
  }.property("users.@each"),

  usersChanged: function() {
    this.rerender();
  }.observes("users.@each"),

  template: Ember.Handlebars.compile('<a {{action showUser view.user href=true}}>@{{view.username}}</a>')
});

I'm not sure why that is required though, feels like a bug to me.

@ambivalentno

It would be really convenient for me to have something like App.Object.getOne function to use in routes (setting model for slug-based urls)

@darthdeus
Ember.js member

When #571 is fixed, we can easily do that by using this as the implementation

var users = App.User.findQuery({ username: username });

users.one("didLoad", function() {
  users.resolve(users.get("firstObject"));
});

return users;
@wagenet
Ember.js member

This would be a great feature. The main issue is that when we're returning a single object, we need to update its contents, but we can't replace the whole thing. However, it's entirely possible that your findOne will return an object that's already in the store. In that case you'd end up with two records that have the same id but are not identical JS objects.

@darthdeus
Ember.js member

What about returning a wrapper, something like AdapterPopulatedRecordArray, which would be representing possibly just one record. It would have loading, found and not_found states. If it finds a record that has the same id as one in identity map, it would load it from the identity map, if not, it would represent the new record. It should also be a deferrable which would make using it quite simple

@tchak
Ember.js member

@wagenet I really believe it should be handled on the controller level, like it was the case in SC. In SC you could assign an enumerable to the content of ObjectController and it would resolve to the firstObject as soon as it was available. I can work on a PR if the idea is accepted.

@darthdeus you reinventing ObjectController :)

@wycats
Ember.js member

This is easily accomplished with 1.0:

model: function(params) {
  return this.store.find('post', { id: params.post_id }).then(function(array) {
    return array[0];
  });
}
@wycats wycats closed this
@fusion2004

@wycats At least in 1.0.0-beta.4, it did not work properly by returning the 0 indexed element of the array variable.

This however, did work for me:

model: function(params) {
  return this.store.find('post', { id: params.post_id }).then(function(array) {
    return array.get('firstObject');
  });
}
@knownasilya

Would be nice to have a wrapper for this, like findSingle, which takes a query object and expects the JSON in the singular form, e.g:

// this.store.findSingle('user', { username: 'j123' }) 
{
  "user": {
    "id": 1,
    "name": "John",
    "username": "j123"
  }
}

and would return a singular record.

@knownasilya

Maybe it could be findByQuery?

@ryanjm

@knownasilya I agree that a findByQuery would be helpful. I'm needing to find a single query but pass additional parameters with it. Have you figured out a good solution for this yet?

find with a query option is expecting an array and my server is passing a single object (I'm using wycats solution here) so I'm not sure how to work around it right now.

@MartinElvar

The problem with..

return this.store.find('post', { slug: params.slug }).then(function(array) {
  return array.get('firstObject');
});

Say i have visited the posts pages, which contains all the post, and i click on a post. Now the post has already been loaded on posts page, but it will make another request to the server, as a id was not provided to the find method.

this.store.find('post', 1)

Would search through the cache, before querying the server.

@knownasilya

@MartinElvar that's a very good point. So having a findByQuery or something similar could be optimized for the store.

@recipher
@tchak
Ember.js member

@knownasilya @MartinElvar How do you expect the store to know about the query? The whole point of findQuery is to always hit the server...

As for findByQuery returning single record, we could do it now that we have PromiseProxy. But it would be basically @wycats implementation wrapped in a PromisProxy. I am against a new method on the adapter for singular query, seems redundant, and if it just an api formatting problem, seems easy to handle with a custom adapter or serializer.

@knownasilya

@tchak I see what you mean. Only if the query was a composite key, then you could do something of the sort, otherwise the store can't possibly know that there aren't other records on the server that it doesn't posses.

So maybe it's a lack of composite key functionality in findById..

@tchak
Ember.js member

What you mean by composite key is something like "#{title}-#{date}" but where the uniciy is guaranteed only for full key ? If yes I would suggest implementing it on the adapter/serializer level by setting the id attr with composed key value.

@knownasilya

That's a very good suggestion @tchak .

@tchak
Ember.js member

@knownasilya I am not sure how common this kind of pattern is. If it is common enough maybe we could provide a config option on the serializer. But in all cases this is the place where it belongs.

@MartinElvar

@tchak I would say it's a very common use case, at least if you want to seo optimise your webpage, and "show" pages are accessed via slugs like /posts/ember-js-is-rocking-my-world; i can imagine many would like that.

@fusion2004

@MartinElvar @tchak That is definitely my use case for wanting this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.