36 votes

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 fausses informations. Voir comment fonctionnent les collections, les publications et les abonnements de Meteor pour bien comprendre comment publier et s'abonner à plusieurs sous-ensembles de la même collection de serveurs.


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

Voici un pseudo-code pour illustrer ma question :

items collection sur le serveur

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

  1. 50 enregistrements ont le enabled la propriété est réglée sur true et ;
  2. 100 enregistrements ont le processed la propriété est réglée sur true .

Tous les autres sont réglés 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 du serveur

Publions deux "vues" de la même collection de serveurs. L'une enverra un curseur avec 50 enregistrements, et l'autre 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 tous les connaître (en fait, les envoyer tous 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 supporter la technique de compensation de latence, nous sommes obligés de déclarer une seule collection Items sur le client. La faille devrait devenir évidente : comment faire la différence entre Items para enabled_items y Items para 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 monkey-Parcheando _publishCursor pour permettre d'utiliser le nom de l'abonnement au lieu du nom de la collection. B

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

Avec le "monkey-patch" en place, ça va marcher. Mais si vous passez en mode hors ligne, les changements n'apparaîtront pas immédiatement sur le client - il faudra être connecté au serveur pour les voir.

Quelle est l'approche correcte ?


EDIT : Je viens de revoir ce fil de discussion et je me rends compte qu'en l'état actuel des choses, ma question, mes réponses et la pléthore de commentaires véhiculent beaucoup d'informations erronées.

En fait, j'ai mal compris la relation entre la publication et l'abonnement. Je pensais que lorsque vous publiiez un curseur, il atterrissait sur le client comme une collection distincte des autres curseurs publiés provenant de la même collection du serveur. Ce n'est tout simplement pas comme cela que cela fonctionne. L'idée est que le client et le serveur ont les mêmes collections, mais c'est ce qui est le plus important. sur les collections qui diffèrent. Les contrats pub-sub négocient quels documents finissent chez le client. La réponse de Tom est techniquement correcte, mais il manquait quelques détails pour renverser mes hypothèses. J'ai répondu à une question similaire à la mienne dans un autre fil de discussion de SO en me basant sur l'explication de Tom, mais en gardant à l'esprit mon incompréhension initiale de la pub-sub de Meteor : Stratégies Meteor de publication/abonnement pour les collections uniques côté client

J'espère que cela aidera ceux qui sont tombés sur ce fil et qui en ressortent plus confus qu'autre chose !

34voto

Tom Coleman Points 2145

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, c'est mieux ainsi car si vous insérez (localement) un élément qui est à la fois activé et traité, il peut apparaître dans les deux listes (contrairement à ce qui se passerait si vous aviez deux collections distinctes).

NOTE

Je me suis rendu compte que je n'étais pas très clair, alors je l'ai un peu développé, j'espère que ça aidera.

6voto

Lloyd Points 3597

Vous pourriez faire deux publications séparées comme ceci

Publications du 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 des clients

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

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

1voto

matb33 Points 1859

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

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

Ce n'est pas parfait, dans la mesure où un décalage/une limite ne fonctionnera pas avec cette technique, mais pour le moment, minimongo ne la prend pas en charge.

Mais en fin de compte, ce qu'il vous permet de faire, c'est de déclarer ce qui ressemble à différents sous-ensembles de la même collection, mais sous le capot, il s'agit du même sous-ensemble. Il y a juste un peu d'abstraction à l'avant pour les faire paraître proprement séparés.

Ejemplo:

// 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'un seul publish/subscribe $or'd ensemble, j'avais besoin d'un moyen d'obtenir les bons paramètres puisqu'ils sont mélangés.

Et de 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, j'ai l'avantage de faire tourner le même code dans le serveur et le client, et si le serveur ne fait pas quelque chose, le client le fera, ou vice versa.

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

J'ai écrit ceci rapidement pour le tester, je m'excuse 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>

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X