web-dev-qa-db-fra.com

Publication / abonnement de plusieurs sous-ensembles de la même collection de serveurs

EDIT: cette question, certaines des réponses et certains des commentaires, contiennent beaucoup de désinformation. Voir comment fonctionnent les collections, publications et abonnements Meteor pour une compréhension précise de la publication et de l'abonnement à plusieurs sous-ensembles de la même collection de serveurs.


Comment procéder pour publier différents sous-ensembles (ou "vues") d'une même collection sur le serveur en tant que plusieurs collections sur le client?

Voici un pseudo-code pour illustrer ma question:

items collection sur le serveur

Supposons que j'ai une collection items sur le serveur avec des millions d'enregistrements. Supposons également que:

  1. 50 enregistrements ont la propriété enabled définie sur true, et;
  2. 100 enregistrements ont la propriété processed définie sur true.

Tous les autres sont définis sur 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
}

Code serveur

Publions deux "vues" de la même collection de serveurs. L'un enverra un curseur avec 50 enregistrements, et l'autre enverra un curseur avec 100 enregistrements. Il y a plus de 458 millions d'enregistrements dans cette base de données fictive côté serveur, et le client n'a pas besoin de les connaître tous (en fait, tous les envoyer prendrait probablement plusieurs heures dans cet exemple):

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

Code client

Afin de prendre en charge la technique de compensation de latence, nous sommes obligés de déclarer une seule collection Items sur le client. Il devrait apparaître où se trouve la faille: comment différencier Items pour enabled_items et Items pour 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());
});

Ma solution actuelle implique _publishCursor de correction de singe pour permettre au nom d'abonnement d'être utilisé au lieu du nom de la collection. Mais cela ne fera aucune compensation de latence. Chaque écriture doit aller-retour au serveur:

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

Avec le patch singe en place, cela fonctionnera. Mais passez en mode hors ligne et les modifications n'apparaîtront pas immédiatement sur le client - nous devrons être connectés au serveur pour voir les modifications.

Quelle est la bonne approche?


EDIT: Je viens de revoir ce fil et je me rends compte qu'en l'état, mes questions et réponses et la pléthore de commentaires contiennent beaucoup de désinformation.

Cela revient à dire que j'ai mal compris la relation publication-abonnement. Je pensais que lorsque vous publiez un curseur, il atterrirait sur le client en tant que collection distincte des autres curseurs publiés provenant de la même collection de serveurs. Ce n'est tout simplement pas ainsi que cela fonctionne. L'idée est que le client et le serveur ont les mêmes collections, mais c'est ce qui est in les collections qui diffèrent. Les contrats pub-sub négocient quels documents aboutissent chez le client. La réponse de Tom est techniquement correcte, mais il manquait quelques détails pour inverser mes hypothèses. J'ai répondu à une question similaire à la mienne dans un autre SO fil basé sur l'explication de Tom, mais en gardant à l'esprit ma méconnaissance d'origine du pub-sub de Meteor: Meteor publier/souscrire des stratégies pour un client unique) -side collections

J'espère que cela aide ceux qui traversent ce fil et en ressortent plus confus qu'autre chose!

36
matb33

Ne pourriez-vous pas simplement utiliser la même requête côté client lorsque vous souhaitez consulter les éléments?

Dans un répertoire lib:

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

Sur le serveur:

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

Sur le client

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

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

Si vous y réfléchissez, il vaut mieux ainsi que si vous insérez (localement) un élément qui est à la fois activé et traité, il peut apparaître dans les deux listes (contrairement à si vous aviez deux collections distinctes).

REMARQUE

J'ai réalisé que je n'étais pas clair, alors j'ai un peu élargi cela, j'espère que cela aide.

34
Tom Coleman

vous pourriez faire deux publications distinctes comme celle-ci ..

Publications serveur

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();
    });
});

Abonnements clients

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

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

J'ai réussi à obtenir des résultats préliminaires prometteurs en abordant le problème avec une publication/abonnement unique par collection et en tirant parti de $or dans la requête find.

L'idée est de fournir un wrapper autour de Meteor.Collection qui vous permet d'ajouter des "vues", qui sont essentiellement nommées curseurs. Mais ce qui se passe vraiment, c'est que ces curseurs ne sont pas exécutés individuellement ... leurs sélecteurs sont extraits, $ or'd ensemble et exécutés comme une seule requête et sur un seul pub-sub.

Ce n'est pas parfait, car un décalage/limite ne fonctionnera pas avec cette technique, mais pour le moment minimongo ne le prend pas en charge de toute façon.

Mais en fin de compte, cela vous permet de déclarer ce qui ressemble à différents sous-ensembles de la même collection, mais sous le capot, ils sont le même sous-ensemble. Il y a juste un peu d'abstraction devant pour les faire se sentir bien séparés.

Exemple:

// 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 } });
});

Ou si vous voulez passer des paramètres:

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

Les paramètres sont donnés sous forme d'objets, car il s'agit d'une seule publication/souscription $ ou ensemble, j'avais besoin d'un moyen d'obtenir les bons paramètres car ils sont mélangés.

Et pour l'utiliser réellement dans un modèle:

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

En bref, je profite d'avoir le même code en cours d'exécution sur le serveur et le client, et si le serveur ne fait rien, le client le fera, ou vice versa.

Et surtout, seuls les enregistrements qui vous intéressent sont envoyés au client. Tout cela est réalisable sans couche d'abstraction en faisant simplement le $ ou vous-même, mais ce $ ou deviendra assez laid à mesure que de nouveaux sous-ensembles seront ajoutés. Cela permet simplement de le gérer avec un minimum de code.

J'ai écrit ceci rapidement pour le tester, excuses pour la longueur et le manque de documentation:

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>
1
matb33