发布/订阅同一服务器集合的多个子集

时间:2012-09-28 01:36:36

标签: collections meteor publish-subscribe

编辑:这个问题,一些答案以及一些评论都包含很多错误信息。有关发布和订阅同一服务器集合的多个子集的准确理解,请参阅how Meteor collections, publications and subscriptions work


如何在服务器上发布单个集合的不同子集(或“视图”)作为客户端上的多个集合?

这里有一些伪代码可以帮助说明我的问题:

服务器上的

items集合

假设我在服务器上有一个items集合,有数百万条记录。我们也假设:

  1. 50条记录的enabled属性设置为true,并且
  2. 100条记录的processed属性设置为true
  3. 所有其他人都设置为false

    items:
    {
        "_id": "uniqueid1",
        "title": "item #1",
        "enabled": false,
        "processed": false
    },
    {
        "_id": "uniqueid2",
        "title": "item #2",
        "enabled": false,
        "processed": true
    },
    ...
    {
        "_id": "uniqueid458734958",
        "title": "item #458734958",
        "enabled": true,
        "processed": true
    }
    

    服务器代码

    让我们发布同一服务器集合的两个“视图”。一个将发送一个包含50个记录的游标,另一个将向下发送一个包含100个记录的游标。这个虚构的服务器端数据库中有超过4.58亿条记录,客户端不需要了解所有这些记录(事实上,在这个例子中将它们全部发送可能需要几个小时):

    var Items = new Meteor.Collection("items");
    
    Meteor.publish("enabled_items", function () {
        // Only 50 "Items" have enabled set to true
        return Items.find({enabled: true});
    });
    
    Meteor.publish("processed_items", function () {
        // Only 100 "Items" have processed set to true
        return Items.find({processed: true});
    });
    

    客户端代码

    为了支持延迟补偿技术,我们不得不在客户端上声明单个集合Items。应该明白缺陷的位置:如何区分Items enabled_itemsItems processed_items

    var Items = new Meteor.Collection("items");
    
    Meteor.subscribe("enabled_items", function () {
        // This will output 50, fine
        console.log(Items.find().count());
    });
    
    Meteor.subscribe("processed_items", function () {
        // This will also output 50, since we have no choice but to use
        // the same "Items" collection.
        console.log(Items.find().count());
    });
    

    我当前的解决方案涉及猴子修补_publishCursor,以允许使用订阅名称而不是集合名称。但这不会做任何延迟补偿。每次写入都必须往返于服务器:

    // On the client:
    var EnabledItems = new Meteor.Collection("enabled_items");
    var ProcessedItems = new Meteor.Collection("processed_items");
    

    随着猴子补丁到位,这将有效。但是进入离线模式并且客户端不会立即显示更改 - 我们需要连接到服务器才能看到更改。

    什么是正确的方法?


    编辑:我刚刚重新审视了这个帖子,我意识到,就目前而言,我的问题和答案以及过多的评论带来了很多错误的信息。

    归结为我误解了发布 - 订阅关系。我认为,当您发布游标时,它将作为与源自同一服务器集合的其他已发布游标的单独集合登陆客户端。这根本不是它的工作原理。我们的想法是客户端和服务器都具有相同的集合,但 的集合中有不同的集合。 pub-sub合同协商哪些文档最终在客户端上。汤姆的答案在技术上是正确的,但缺少一些细节来扭转我的假设。我根据汤姆的解释在另一个SO线程中回答了类似的问题,但请记住我对Meteor的pub-sub的原始误解:Meteor publish/subscribe strategies for unique client-side collections

    希望这可以帮助那些遇到这个问题并且比任何事情都更加困惑的人!

3 个答案:

答案 0 :(得分:34)

当你想看项目时,你能不能只使用相同的查询客户端?

在lib目录中:

enabledItems = function() {
  return Items.find({enabled: true});
}
processedItems = function() {
  return Items.find({processed: true});
}

在服务器上:

Meteor.publish('enabled_items', function() {
  return enabledItems();
});
Meteor.publish('processed_items', function() {
  return processedItems();
});

在客户端

Meteor.subscribe('enabled_items');
Meteor.subscribe('processed_items');

Template.enabledItems.items = function() {
  return enabledItems();
};
Template.processedItems.items = function() {
  return processedItems();
};

如果您考虑一下,这样更好,就像您插入(本地)一个既启用又处理过的项目,它可以出现在两个列表中(如果您有两个单独的集合,则相反)。

我意识到我有点不清楚,所以我已经扩展了一点,希望它有所帮助。

答案 1 :(得分:6)

你可以制作两个单独的出版物..

服务器出版物

Meteor.publish("enabled_items", function(){
    var self = this;

    var handle = Items.find({enabled: true}).observe({
        added: function(item){
            self.set("enabled_items", item._id, item);
            self.flush();
        },
        changed: function(item){
            self.set("enabled_items", item._id, item);
            self.flush();
        }
    });

    this.onStop(function() {
        handle.stop();
    });
});

Meteor.publish("disabled_items", function(){
    var self = this;

    var handle = Items.find({enabled: false}).observe({
        added: function(item){
            self.set("disabled_items", item._id, item);
            self.flush();
        },
        changed: function(item){
            self.set("disabled_items", item._id, item);
            self.flush();
        }
    });

    this.onStop(function() {
        handle.stop();
    });
});

客户订阅

var EnabledItems = new Meteor.Collection("enabled_items"),
    DisabledItems = new Meteor.Collection("disabled_items");

Meteor.subscribe("enabled_items");
Meteor.subscribe("disabled_items");

答案 2 :(得分:1)

通过在每个集合中使用单个发布/订阅来解决问题,并在$or查询中利用find,我设法取得了一些有希望的初步结果。

我们的想法是提供一个Meteor.Collection的包装器,允许您添加“视图”,它们基本上被命名为游标。但真正发生的是这些游标不是单独运行的......它们的选择器被提取,$或者一起运行并作为单个查询运行到单个pub-sub上。

这并不完美,因为偏移/限制不适用于这种技术,但目前minimongo不支持它。

但最终它允许你做的是声明看起来像同一个集合的不同子集,但在引擎盖下它们是相同的子集。前面只有一些抽象,让他们感觉干净利落。

示例:

// Place this code in a file read by both client and server:
var Users = new Collection("users");
Users.view("enabledUsers", function (collection) {
    return collection.find({ enabled: true }, { sort: { name: 1 } });
});

或者如果你想传递参数:

Users.view("filteredUsers", function (collection) {
    return collection.find({ enabled: true, name: this.search, { sort: { name: 1 } });
}, function () {
    return { search: Session.get("searchterms"); };
});

参数作为对象给出,因为它是一个单独的发布/订阅$或者在一起,我需要一种方法来获得正确的参数,因为它们混合在一起。

并在模板中实际使用它:

Template.main.enabledUsers = function () {
    return Users.get("enabledUsers");
};
Template.main.filteredUsers = function () {
    return Users.get("filteredUsers");
};

简而言之,我利用在服务器和客户端运行相同的代码,如果服务器没有做某事,客户端将会,反之亦然。

最重要的是,只有您感兴趣的记录才会被发送到客户端。通过简单地执行$或者自己,这可以在没有抽象层的情况下实现,但是随着更多子集的添加,这个$或者会变得非常难看。这只是用最少的代码来帮助管理它。

我快速写了这个来测试它,为长度和缺乏文档道歉:

test.js

// Shared (client and server)
var Collection = function () {
    var SimulatedCollection = function () {
        var collections = {};

        return function (name) {
            var captured = {
                find: [],
                findOne: []
            };

            collections[name] = {
                find: function () {
                    captured.find.push(([]).slice.call(arguments));
                    return collections[name];
                },
                findOne: function () {
                    captured.findOne.push(([]).slice.call(arguments));
                    return collections[name];
                },
                captured: function () {
                    return captured;
                }
            };

            return collections[name];
        };
    }();

    return function (collectionName) {
        var collection = new Meteor.Collection(collectionName);
        var views = {};

        Meteor.startup(function () {
            var viewName, view, pubName, viewNames = [];

            for (viewName in views) {
                view = views[viewName];
                viewNames.push(viewName);
            }

            pubName = viewNames.join("__");

            if (Meteor.publish) {
                Meteor.publish(pubName, function (params) {
                    var viewName, view, selectors = [], simulated, captured;

                    for (viewName in views) {
                        view = views[viewName];

                        // Run the query callback but provide a SimulatedCollection
                        // to capture what is attempted on the collection. Also provide
                        // the parameters we would be passing as the context:
                        if (_.isFunction(view.query)) {
                            simulated = view.query.call(params, SimulatedCollection(collectionName));
                        }

                        if (simulated) {
                            captured = simulated.captured();
                            if (captured.find) {
                                selectors.push(captured.find[0][0]);
                            }
                        }
                    }

                    if (selectors.length > 0) {
                        return collection.find({ $or: selectors });
                    }
                });
            }

            if (Meteor.subscribe) {
                Meteor.autosubscribe(function () {
                    var viewName, view, params = {};

                    for (viewName in views) {
                        view = views[viewName];
                        params = _.extend(params, view.params.call(this, viewName));
                    }

                    Meteor.subscribe.call(this, pubName, params);
                });
            }
        });

        collection.view = function (viewName, query, params) {
            // Store in views object -- we will iterate over it on startup
            views[viewName] = {
                collectionName: collectionName,
                query: query,
                params: params
            };

            return views[viewName];
        };

        collection.get = function (viewName, optQuery) {
            var query = views[viewName].query;
            var params = views[viewName].params.call(this, viewName);

            if (_.isFunction(optQuery)) {
                // Optional alternate query provided, use it instead
                return optQuery.call(params, collection);
            } else {
                if (_.isFunction(query)) {
                    // In most cases, run default query
                    return query.call(params, collection);
                }
            }
        };

        return collection;
    };
}();

var Items = new Collection("items");

if (Meteor.isServer) {
    // Bootstrap data -- server only
    Meteor.startup(function () {
        if (Items.find().count() === 0) {
            Items.insert({title: "item #01", enabled: true, processed: true});
            Items.insert({title: "item #02", enabled: false, processed: false});
            Items.insert({title: "item #03", enabled: false, processed: false});
            Items.insert({title: "item #04", enabled: false, processed: false});
            Items.insert({title: "item #05", enabled: false, processed: true});
            Items.insert({title: "item #06", enabled: true, processed: true});
            Items.insert({title: "item #07", enabled: false, processed: true});
            Items.insert({title: "item #08", enabled: true, processed: false});
            Items.insert({title: "item #09", enabled: false, processed: true});
            Items.insert({title: "item #10", enabled: true, processed: true});
            Items.insert({title: "item #11", enabled: true, processed: true});
            Items.insert({title: "item #12", enabled: true, processed: false});
            Items.insert({title: "item #13", enabled: false, processed: true});
            Items.insert({title: "item #14", enabled: true, processed: true});
            Items.insert({title: "item #15", enabled: false, processed: false});
        }
    });
}

Items.view("enabledItems", function (collection) {
    return collection.find({
        enabled: true,
        title: new RegExp(RegExp.escape(this.search1 || ""), "i")
    }, {
        sort: { title: 1 }
    });
}, function () {
    return {
        search1: Session.get("search1")
    };
});

Items.view("processedItems", function (collection) {
    return collection.find({
        processed: true,
        title: new RegExp(RegExp.escape(this.search2 || ""), "i")
    }, {
        sort: { title: 1 }
    });
}, function () {
    return {
        search2: Session.get("search2")
    };
});

if (Meteor.isClient) {
    // Client-only templating code

    Template.main.enabledItems = function () {
        return Items.get("enabledItems");
    };
    Template.main.processedItems = function () {
        return Items.get("processedItems");
    };

    // Basic search filtering
    Session.get("search1", "");
    Session.get("search2", "");

    Template.main.search1 = function () {
        return Session.get("search1");
    };
    Template.main.search2 = function () {
        return Session.get("search2");
    };
    Template.main.events({
        "keyup [name='search1']": function (event, template) {
            Session.set("search1", $(template.find("[name='search1']")).val());
        },
        "keyup [name='search2']": function (event, template) {
            Session.set("search2", $(template.find("[name='search2']")).val());
        }
    });
    Template.main.preserve([
        "[name='search1']",
        "[name='search2']"
    ]);
}

// Utility, shared across client/server, used for search
if (!RegExp.escape) {
    RegExp.escape = function (text) {
        return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
    };
}

的test.html

<head>
    <title>Collection View Test</title>
</head>

<body>
    {{> main}}
</body>

<template name="main">
    <h1>Collection View Test</h1>
    <div style="float: left; border-right: 3px double #000; margin-right: 10px; padding-right: 10px;">
        <h2>Enabled Items</h2>
        <input type="text" name="search1" value="{{search1}}" placeholder="search this column" />
        <ul>
            {{#each enabledItems}}
                <li>{{title}}</li>
            {{/each}}
        </ul>
    </div>
    <div style="float: left;">
        <h2>Processed Items</h2>
        <input type="text" name="search2" value="{{search2}}" placeholder="search this column" />
        <ul>
            {{#each processedItems}}
                <li>{{title}}</li>
            {{/each}}
        </ul>
    </div>
</template>