ReactiveAggregate()+ collection.update()->错误:期望找到要更改的文档

时间:2019-06-09 07:28:51

标签: javascript reactjs mongodb meteor reactive-programming

反应式聚合最初没有错误地发布到客户端。当Meteor.user从客户端更新Meteor.call()集合时,似乎触发了该错误:

updateProductFavorites = (product_key, action) => {
    const { ranking } = this.props
    const { product_keys } = ranking[0]
    Meteor.call('Accounts.updateProductFavorites', product_key, action, (err, response) => {
        if (err)
            makeAlert(err.reason, 'danger', 3000)
        else 
            this.getProductsByKeys(product_keys)    
    })
}

我已经订阅了Meteor.user()和反应式聚合:

export default withTracker(() => {
    const handle = Meteor.subscribe("products.RankingList")
    return {
        ranking: AggregatedProductRanking.find({}).fetch(),
        user: Meteor.user(),
        isLoading: !handle.ready() || !Meteor.user()
    }
})(ProductRankingList)

我已经在两侧声明并导入了clientCollection,正如本answer中所建议的那样。这是服务器端的相关代码:

const getProductRankingList = (context) => ReactiveAggregate(context, Meteor.users, [
      // aggregation stages can be seen in the code snippet below
    ], { clientCollection: "aggregatedProductRanking"})

Meteor.methods({
    'Accounts.updateProductFavorites': function(product_key, action) {
         allowOrDeny(this.userId)
         action = action == 'add' ? { $addToSet: { productFavorites: product_key }} : { $pull: { productFavorites: product_key }}
         return Meteor.users.update({_id: this.userId}, action)
     }
})

Meteor.publish('products.RankingList', function() {
    const callback = () => this.stop()
    allowOrDenySubscription(this.userId, callback)
    return getProductRankingList(this)
})

让我感到困惑的是,即使抛出了此错误,update调用的Meteor.call('Accounts.updateProductFavorites')仍然可以可靠地执行。

因此,对登录的Meteor.user()所做的更改将流回客户端,并且组件将重新提供。只有ReactiveAggregate的订阅似乎停止工作。引发以下错误,我必须重新加载浏览器才能查看聚合结果中的更改。 (底部有完整的堆栈跟踪)

Uncaught Error: Expected to find a document to change
    at Object.update (collection.js:207)
    at Object.store.<computed> [as update] (livedata_connection.js:310)
    ...

// In a certain case the error message is a bit different:
Exception in flushing DDP buffered writes: Error: Expected to find a document to change
    at Object.update (collection.js:207)
    at Object.store.<computed> [as update] (livedata_connection.js:310)
    ...

我猜测update()会调用此ReactiveAggregate(),以便填充clientCollection 但是我在做什么错了?

  

有关更完整的代码示例:

服务器端

import { Meteor } from 'meteor/meteor'
import { ReactiveAggregate } from 'meteor/jcbernack:reactive-aggregate';

// Collections
import { AggregatedProductRanking } from '../imports/collections'

const getProductRankingList = (context) => ReactiveAggregate(context, Meteor.users, [
        {
            $match: { productFavorites: {$ne: [] }}
        },{
            $project: {
                _id: 0,
                productFavorites: { $concatArrays: "$productFavorites" },
            }
        },{
            $unwind: "$productFavorites"
        },{
            $facet: {
                rankingList: [
                    {
                        $group: {
                            _id: "$productFavorites",
                            count: { $sum: 1 }
                        }
                    },{
                        $sort: { "count": -1 }
                    }
                ],
                product_keys: [
                    {
                        $group: { 
                            _id: 0,
                            product_keys: { $addToSet: "$productFavorites" }
                        }
                    }
                ]
            }
        },{
            $unwind: "$product_keys"
        },{
            $project: {
                _id: 0,
                rankingList: 1,
                product_keys: "$product_keys.product_keys"
            }
        }
    ], { clientCollection: "aggregatedProductRanking"})

Meteor.methods({
    'Accounts.updateProductFavorites': function(product_key, action) {
        allowOrDeny(this.userId)
        action = action == 'add' ? { $addToSet: { productFavorites: product_key }} : { $pull: { productFavorites: product_key }}
        return Meteor.users.update({_id: this.userId}, action)
    },
    'Products.getByProductKey': function(productFavorites) {
        allowOrDeny(this.userId)
        if (productFavorites == undefined)
            productFavorites = Meteor.users.findOne({_id: this.userId}, {fields: {productFavorites: 1}}).productFavorites
        if (productFavorites.length > 0) {
            return Products.find(
                { product_key: {$in: productFavorites }, price_100_g_ml: {$ne: null} },
                { sort: {product_name: 1} }).fetch()
        } else
            return []
    },
})

function allowOrDenySubscription(userId, callback) {
    if (!userId) {
        callback()
        return;
    }
}

Meteor.publish(null, function() {
    if (!this.userId)
        return false
    return Meteor.users.find({_id: this.userId}, { fields: {
        firstName: 1, lastName: 1,
        zip: 1, city: 1, street: 1, houseNumber: 1,
        phone: 1, iban: 1, bic: 1,
        memberId: 1, membershipFee: 1,
        productFavorites: 1
    }})
}, { is_auto: true })

Meteor.publish('products.RankingList', function() {
    const callback = () => this.stop()
    allowOrDenySubscription(this.userId, callback)
    return getProductRankingList(this)
})

客户端

import { Meteor } from 'meteor/meteor';
import React, { Component } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
// ... more imports

// Collections
import { AggregatedProductRanking } from '../../../imports/collections';


class ProductRankingList extends Component {
    constructor() {
        super()

        this.state = {
            products: [],
            productDetails: true,
            singleProductDetails: 0,
        }
    }

    getProductsByKeys = (product_keys) => {
        Meteor.call('Products.getByProductKey', product_keys, (err, response) => {
            if (err)
                makeAlert(err.reason, 'danger', 3000)
            else {
                this.setState({products: response})
                console.log(response)
            }
        })
    }

    updateProductFavorites = (product_key, action) => {
        const { ranking } = this.props
        const { product_keys } = ranking[0]
        console.log(product_keys)
        Meteor.call('Accounts.updateProductFavorites', product_key, action, (err, response) => {
            if (err)
                makeAlert(err.reason, 'danger', 3000)
            else 
                this.getProductsByKeys(product_keys)    
        })
    }

    toggleProductFavorite = (product_key) => {
        const { productFavorites } = this.props.user
        if (productFavorites.includes(product_key))
            this.updateProductFavorites(product_key, 'remove')
        else
            this.updateProductFavorites(product_key, 'add')
    }

    mapProductFavorites = () => {
        const { products, productDetails, singleProductDetails } = this.state
        const { productFavorites } = this.props.user
        const { ranking } = this.props
        const { rankingList } = ranking[0]

        if (products.length == 0)
            return <div className="alert alert-primary col-12">No one has favorited any products at the moment, it seems.</div>

        products.map((product, i) => {
            const { order_number, supplierId } = product
            product["count"] = rankingList.find(product => product._id == `${supplierId}_${order_number}`).count
        })

        products.sort((a, b) => b.count - a.count)

        return (
            products.map((product, i) => {
                if (product.price_100_g_ml) {
                    var [euro, cent] = product.price_100_g_ml.toFixed(2).toString().split('.')
                }

            const { product_name, units, trading_unit, certificate, origin, order_number, supplierId, count } = product
            const isFavorite = productFavorites.includes(`${supplierId}_${order_number}`) ? 'is-favorite' : 'no-favorite'

            return (
                <div className="col-lg-6" key={i}>
                    <div key={i} className="product-card">
                        <div className="card-header" onClick={() => this.toggleSingleProductDetails(order_number)}>
                            {product_name}
                            {/* <span className="fa-layers fa-fw heart-with-count">
                                <FontAwesomeIcon icon="heart"/>
                                <div className="fa-layers-text">{count}</div>
                            </span> */}
                        </div>
                        {productDetails || singleProductDetails == order_number ?
                        <>
                            <div className="card-body">
                                {euro ?
                                    <>
                                        <div className="product-actions">
                                            <button className={`btn btn-light btn-lg product-${isFavorite}`}
                                                onClick={() => this.toggleProductFavorite(`${supplierId}_${order_number}`)}>
                                                <FontAwesomeIcon icon="heart"/>
                                                <span className="ml-2">{count}</span>
                                            </button>
                                        </div>
                                        <div className="price-100-g-ml">
                                            <small>pro 100{units == 'kg' ? 'g' : 'ml'}</small><sup></sup>
                                            <big>{euro}</big>.<sup>{cent.substring(0,2)}</sup>
                                        </div>
                                    </> : null}
                            </div>
                            <div className="card-footer">
                                <div className="row">
                                    <div className="col-4">{trading_unit}</div>
                                    <div className="col-4 text-center">{certificate}</div>
                                    <div className="col-4 text-right">{origin}</div>
                                </div>                            
                            </div>
                        </> : null }
                    </div>
                </div>)
            })
        )
    }

    componentDidUpdate(prevProps, prevState) {
        const { isLoading, ranking } = this.props
        if (ranking.length < 1)
            return null
        if (!isLoading && (prevProps.ranking != ranking)) {
            this.getProductsByKeys(ranking[0].product_keys)
        }
    }

    render() {
        const { isLoading, ranking } = this.props
        if (isLoading || ranking.length < 1)
            return null

        return(
            <div className="row mt-3">
                {this.mapProductFavorites()}
            </div>
        )
    }
}

export default withTracker(() => {
    const handle = Meteor.subscribe("products.RankingList")
    return {
        ranking: AggregatedProductRanking.find({}).fetch(),
        user: Meteor.user(),
        isLoading: !handle.ready() || !Meteor.user()
    }
})(ProductRankingList)

  

完整堆栈跟踪

// When loading component through history.push()
Exception in flushing DDP buffered writes: Error: Expected to find a document to change
    at Object.update (collection.js:207)
    at Object.store.<computed> [as update] (livedata_connection.js:310)
    at livedata_connection.js:1192
    at Array.forEach (<anonymous>)
    at livedata_connection.js:1191
    at Array.forEach (<anonymous>)
    at Connection._performWrites (livedata_connection.js:1187)
    at Connection._flushBufferedWrites (livedata_connection.js:1167)
    at meteor.js?hash=857dafb4b9dff17e29ed8498a22ea5b1a3d6b41d:1234
    
    
// After second call of Meteor.call('Accounts.updateProductFavorites')
Uncaught Error: Expected to find a document to change
    at Object.update (collection.js:207)
    at Object.store.<computed> [as update] (livedata_connection.js:310)
    at livedata_connection.js:1192
    at Array.forEach (<anonymous>)
    at livedata_connection.js:1191
    at Array.forEach (<anonymous>)
    at Connection._performWrites (livedata_connection.js:1187)
    at Connection._flushBufferedWrites (livedata_connection.js:1167)
    at Connection._livedata_data (livedata_connection.js:1133)
    at Connection.onMessage (livedata_connection.js:1663)

1 个答案:

答案 0 :(得分:0)

事实证明,解决方案非常简单:发生上述错误的原因是聚合结果中的缺少_id字段

    ...
    },{
            $project: {
                _id: 0, // This will give the error.
                rankingList: 1,
                product_keys: "$product_keys.product_keys"
            }
        }
    ], { clientCollection: "aggregatedProductRanking"})

ReactiveAggregate()meteor-reactive-aggregate)的文档指出_id字段可以省略,因为它将由ReactiveAggregate()自动创建。但是即使删除了_id: 0,它也仍然无法正常工作。

  

这是什么工作:

    ...
    },{
            $project: {
                _id: "whatTheFuckIsGoingOnHere", // Calm down, bro!
                rankingList: 1,
                product_keys: "$product_keys.product_keys"
            }
        }
    ], { clientCollection: "aggregatedProductRanking"})

出色的反应性聚集,只是给a **带来一点痛苦。 我在github repo

中做了一个错误报告