Ember 2, filter relationship models (hasMany, belongsTo) and calculate computed property based on relationships

梦想与她 提交于 2019-12-08 18:48:14

问题


These are my files:

Models

app/models/basket.js:

export default DS.Model.extend({
  name: DS.attr('string'),
  house: DS.belongsTo('house', { async: true }),
  boxes: DS.hasMany('box', { async: true })
});

app/models/box.js:

export default DS.Model.extend({
  qty: DS.attr('number'),
  basket: DS.belongsTo('basket'),
  cartLines: DS.hasMany('cart-line', { async: true })
});

app/models/cart-line.js:

export default DS.Model.extend({
  qty: DS.attr('number'),
  box: DS.belongsTo('box'),
  product: DS.belongsTo('product')
});

app/models/product.js:

export default DS.Model.extend({
  name: DS.attr('string'),
  price: DS.attr('number')
});

Routes

app/routes/basket.js:

export default Ember.Route.extend({
  model(params) {
    return Ember.RSVP.hash({
      basket: this.store.findRecord('basket', params.basket_id),
      boxes: this.store.findAll('box'),
      products: this.store.findAll('product')
    });
  },
  setupController(controller, models) {
    controller.setProperties(models);
    }
});

Controllers

app/controllers/basket.js:

export default Ember.Controller.extend({
  subTotal: Ember.computed('boxes.@each.cartLines', function () {
    return this.products.reduce((price, product) => {
      var total = price + product.get('price');
      return total;
    }, 0);
  })
});

Questions:

I'm newbie, so I'm studying and makings mistakes. Sorry.

1) Which is the best Ember way to filter relationships when I first enter in route? For example now I load every box in my app whith boxes: this.store.findAll('box'). I need a way to not load all the box in my webapp, just the one in basket. I need the "query with filter" directly from a backend?

UPDATED QUESTION 2) Which is the best Ember way for calculate subTotal? Now, with code below, Ember gives me the subTotal but just in console.log(tot) and after the promises! Why this? How can I wait the promises? I don't understand what to do:

subTotal: Ember.computed('basket.boxes.@each.cartLines', function () {
  let count = 0;
  console.log('subTotal called: ', count);
  // It should be 0 ever
  count = count + 1;

  return this.get('basket.boxes').then(boxes => {
    boxes.forEach(box => {
      box.get('cartLines').then(cartLines => {
        cartLines.reduce(function (tot, value) {
          console.log('tot:', tot + value.get('product.price'));
          return tot + value.get('product.price');
        }, 0);
      });
    });
  });
});

It gives me in template [object Object] because I'm also using in hbs {{log subTotal}} and in console it gives me this:

subTotal called:  0
ember.debug.js:10095 Class {__ember1476746185015: "ember802", __ember_meta__: Meta}
subTotal called:  0
ember.debug.js:10095 Class {__ember1476746185015: "ember934", __ember_meta__: Meta}
ember.debug.js:10095 Class {isFulfilled: true, __ember1476746185015: "ember934", __ember_meta__: Meta}
subTotal called:  0
ember.debug.js:10095 Class {__ember1476746185015: "ember1011", __ember_meta__: Meta}
ember.debug.js:10095 Class {isFulfilled: true, __ember1476746185015: "ember1011", __ember_meta__: Meta}
tot: 3.5
tot: 6
tot: 13.5
tot: 21
tot: 24.5
tot: 27
tot: 3.5
tot: 6
tot: 13.5
tot: 21
tot: 24.5
tot: 27
tot: 3.5
tot: 6
tot: 13.5
tot: 21
tot: 24.5
tot: 27

Why it shows three times subTotal called: 0, no matter if there are zero or one or a thousand products. He always calls three times subTotal called: 0, why?

Is it good to use computed properties with promises?

3) Am I right with that relationship encapsulation?

UPDATED QUESTION 2:

Now I'm using this code, but without success:

import Ember from 'ember';
import DS from 'ember-data';

export default Ember.Controller.extend({

  totalCount: Ember.computed('basket.boxes.@each.cartLines', function () {
    let total = 0;
    const promise = this.get('basket.boxes').then(boxes => {
      boxes.map(box => {
      // const trypromise = boxes.map(box => {
        console.log('box:', box);
        box.get('cartLines').then(cartLines => {
          console.log('cartLines:', cartLines);
          const cartLinesPromise = cartLines.map(cartLine => {
              console.log('cartLine:', cartLine);
              // return cartLine.get('qty');
              // return cartLine;
              // });
              return {
                qty: cartLine.get('qty'),
                price: cartLine.get('product.price')
              };
              //     return cartLines.map(cartLine => {
              //       console.log('cartLine:', cartLine);
              //       return cartLine.get('qty');
              //       //   return {
              //       //     qty: cartLine.get('qty'),
              //       //     price: cartLine.get('product.price')
              //       //   };
              //     });
            })
            // });
        return Ember.RSVP
          .all(cartLinesPromise)
          .then(cartLinesPromise => {
            console.log('cartLinesPromise:', cartLinesPromise);
            // cartLinesPromise.reduce((tot, price) => {
            //   console.log('tot:', tot);
            //   console.log('price:', price);
            //   console.log('tot+price:', tot + price);
            //   return tot + price, 0;
            // });

            return total = 10;
            // return total;
          })
        });

      });

      // return total;
    });

    return DS.PromiseObject.create({ promise });
  })

})

Comments are for many try.

In template I use:

{{log 'HBS totalCount:' totalCount}}
{{log 'HBS totalCount.content:' totalCount.content}}
Total: {{totalCount.content}}

But promise have null content.

Where I'm wrong?

Any incorrect return?

Is this code "promising" correct?


回答1:


There is nothing bad to being new to technology, especially when your question is well formatted and think through.

1) Which is the best Ember-Data way to filter relationships?

This is complex question with a lot of possible endings.

The easiest thing to do is just ask on that model.

Ask on basket

Given your model you can do:

model(params) {
  // we will return basket but make boxes ready
  return this.get('store').find('basket', params.basket_id).then(basket => {
    return basket.get('boxes').then(() => basket);
  });
}

But this has few limitations and advantages

  • you need to send ids with basket
  • you have to enable coalesceFindRequests to make it sane
  • it will load only boxes that are not in store

Edit: you need to send ids with basket This means that basket in your payload will have to provide identification for it's boxes. In case of rest api: {basket: {id: 1, boxes: [1,2,3], ...}. It will then check which ids are not loaded into the store already and ask api here (assuming that box with id 2 is already loaded): /boxes?ids[]=1&ids[]=3.

Ask yourself

model(params) {
  const store = this.get('store');
  const basket = params.basket_id;

  return RSVP.hash({
    model: store.find('basket', basket),
    boxes: store.query('box', {basket}),
  });
},
  • On the other hand this approach will send request for basket only if basket is not in store already (same as before) but always query for boxes(if you don't like it you would have to use peekAll and filter to check if you have all of them or smt like that).
  • Good think is that the requests will be parallel not serial so it may speed things up.
  • Basket also doesn't have to send ids of its boxes.
  • You can do server side filtering by altering query param

Edit: if you don't like it you would have to use peekAll and filter to check if you have all of them You can actually check that with hasMany.

Sideload them

Instead of sending two requests to server you can make your api so that it will append boxes into the payload.

Load only basket and let rest to load from template

You can load only bare minimum (like load only basket), let ember continue and render the page. It will see that you are accessing basket.boxes property and fetch them. This wont look good on its own and will need some additional work like spinners and so on. But this is one way how to speed up boot and initial render time.

2) Which is the best Ember way for calculate subTotal

You want to calculate sum of something that is three levels deep into async relationships, that's not going to be easy. First of I would suggest putting totalPrice computed property into basket model itself. Computed properties are lazily evaluated so there is no performance degradation and this is something that model should be able to provide.

Here is little snippet:

// basket.js
const {RSVP, computed} = Ember;

price: computed('boxes.@each.price', function() {
  const promise = this.get('boxes').then(boxes => {
    // Assuming box.get('price') is computed property like this
    // and returns promise because box must wait for cart lines to resolve.
    const prices = boxes.map(box => box.get('price'));

    return RSVP
      .all(prices)
      .then(prices => prices.reduce((carry, price) => carry + price, 0));
  });

  return PromiseObject.create({promise});
}),

You would need to write something like this for each level or give up some of the async relations. The problem with your computed property is that boxes.@each.cartLines wont listen on everything that can change overall price (for example change of price of product itself). So it won't reflect and update on all possible changes.

I would sagest to give up some async relations. For example request on /baskets/2 could sideload all of its boxes, cartLines and maybe even products. If your api doesn't support sideloading, you can fake it by loading everything in route (you would have to use second example - you are not allowed to access boxes before they are in the store in case of async: false). That would lead to much simpler computed properties to calculate total price and in case of sideloading also reduce stress on server and clients confections.

// basket.js
const {computed} = Ember;

boxes: DS.hasMany('box', {async: false}),

price: computed('boxes.@each.price', function() {
  return this.get('boxes').reduce(box => box.get('price'));
}),

Update and overall after thoughts

I don't think that doing all sums in one function is viable, doable or sane. You will end up in callback hell or some other kind of hell. Moreover this is not going to be performance bottleneck.

I made jsfiddle it is basicaly more fleshed out version of snippet above. Note that it will properly wait and propagate price which is two promises deep and also should update when something changes (also I didn't test that).




回答2:


The solution to your question is nicely explained in How to return a promise composed of nested models in EmberJS with EmberData? by @Kingpin2k.

What you want to do is just load a basket and its associated models(box, cat-line and product) rather than loading all boxes, cartLines and products. Also to compute the subTotal, we would need all those dependency promises resolved beforehand. Following the solution given in the post mentioned earlier, your solution would look like:

MODEL: app/models/cart-line.js

export default DS.Model.extend({
  qty: DS.attr('number'),
  box: DS.belongsTo('box'),
  product: DS.belongsTo('product', { async: true })//assuming you are not side-loading these
});

ROUTE: app/routes/basket.js

export default Ember.Route.extend({
    model(params) {
        return this.store.findRecord('basket', params.basket_id).then((basket)=> {
            return basket.get('boxes').then((boxes)=> {
                let cartLinesPromises = boxes.map(function (box) {
                    return box.get('cartLines');
                });
                return Ember.RSVP.allSettled(cartLinesPromises).then((array)=> {
                    let productPromises = array.map(function (item) {
                        return (item.value).get('product');
                    });
                    return Ember.RSVP.allSettled(productPromises);
                });
            });
        });
    }
});

CONTROLLER: app/controllers/basket.js

subTotal: computed('model.boxes.@each.cartLines', function () {
    //you dont need to use DS.PromiseArray because the promises all have been already resolved in the route's model hook
    let total = 0;
    this.get('model.boxes').forEach((box)=> {
        box.get('cartLines').forEach((cartLine)=> {
            total += cartLine.get('product.price');
        });
    });
    return total;
})

Lastly, for the issue you were having here:

subTotal: computed('boxes.@each.cartLines', function() {
  return DS.PromiseArray.create({
    //"this" here is DS.PromiseArray object and not your controller instance
    promise: this.get('boxes').then(boxes => {
      return boxes.filter(i => i.get('cart-line'));
    })
  });
})

you would not use the computed construct if following the solution given above, but just wanted to point out solution in similar conditions.

subTotal: computed('boxes.@each.cartLines', function() {
  let controllerInstance = this;
  return DS.PromiseArray.create({
    promise: controllerInstance.get('boxes').then(boxes => {
      return boxes.filter(i => i.get('cart-line'));
    })
  });
})


来源:https://stackoverflow.com/questions/40032747/ember-2-filter-relationship-models-hasmany-belongsto-and-calculate-computed

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!