Публикация/подписка на несколько подмножеств одной и той же серверной коллекции

EDIT: этот вопрос, некоторые ответы и некоторые комментарии содержат много дезинформации. См. как работают коллекции, публикации и подписки Meteor для точного понимания публикации и подписки на несколько подмножеств одной и той же серверной коллекции.


Как можно публиковать разные подмножества (или "представления" ) одной коллекции на сервере как несколько коллекций на клиенте?

Вот какой-то псевдокод, который поможет проиллюстрировать мой вопрос:

items коллекция на сервере

Предположим, что у меня есть коллекция items на сервере с миллионами записей. Пусть также предположим, что:

  • 50 записей имеют свойство enabled, установленное на true, и;
  • 100 записей имеют свойство processed, установленное на true.

Все остальные установлены на 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 записями. В этой фиктивной серверной базе данных содержится более 458 миллионов записей, и клиенту не нужно знать обо всех этих (фактически, отправка их всех вниз, вероятно, займет несколько часов в этом примере):

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_items и items для 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");

С помощью патча обезьяны это будет работать. Но перейдите в автономный режим, и изменения сразу не появятся на клиенте - нам нужно будет подключиться к серверу, чтобы увидеть изменения.

Какой правильный подход?


EDIT: Я только что просмотрел эту тему, и я понимаю, что, поскольку это так, мои вопросы и ответы и множество комментариев содержат много дезинформации.

Что происходит, так это то, что я неправильно понял отношения публикации-подписки. Я думал, что когда вы опубликуете курсор, он приземлится на клиенте как отдельная коллекция из других опубликованных курсоров, которые возникли из той же коллекции серверов. Это просто не так, как это работает. Идея состоит в том, что и клиент, и сервер имеют одни и те же коллекции, но это то, что находится в коллекциях, которые отличаются. Контракты pub-sub согласовывают, какие документы заканчиваются на клиенте. Ответ Тома технически правилен, но не хватает нескольких деталей, чтобы повернуть мои предположения. Я ответил на аналогичный вопрос на мой вопрос в другом SO-потоке, основанном на объяснении Тома, но имея в виду мое первоначальное непонимание Meteor pub-sub: Meteor публикует/подписывает стратегии для уникальных коллекций на стороне клиента

Надеюсь, это поможет тем, кто сталкивается с этой нитью и уходит больше, чем что-то путать!

Ответ 1

Не могли бы вы просто использовать одну и ту же клиентскую сторону запроса, если хотите посмотреть на элементы?

В каталоге 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();
};

Если вы думаете об этом, то лучше, как если бы вы вставляли (локально) элемент, который был включен и обработан, он может отображаться в обоих списках (в отличие от двух разных коллекций).

Примечание

Я понял, что я был неясным, поэтому я немного расширил его, надеюсь, что это поможет.

Ответ 2

вы могли бы сделать две отдельные публикации вроде этого.

Серверные публикации

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");

Ответ 3

Мне удалось достичь некоторых перспективных предварительных результатов, приблизившись к проблеме с одной публикацией/подпиской на коллекцию и используя $or в запросе find.

Идея состоит в том, чтобы предоставить оболочку вокруг Meteor.Collection, которая позволяет добавлять "представления", которые в основном называются курсорами. Но то, что на самом деле происходит, заключается в том, что эти курсоры не запускаются индивидуально... их селекторы извлекаются, $or'd вместе и запускаются как один запрос и на один 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"); };
});

Параметры задаются как объекты, потому что они представляют собой единую публикацию/подписку $or'd вместе, мне нужен способ получить правильные параметры, так как они смешиваются.

И фактически использовать его в шаблоне:

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>