web-dev-qa-db-ja.com

同じサーバーコレクションの複数のサブセットをパブリッシュ/サブスクライブする

編集:この質問、いくつかの回答、およびいくつかのコメントには、多くの誤った情報が含まれています。同じサーバーコレクションの複数のサブセットのパブリッシュとサブスクライブを正確に理解するには、 Meteorコレクション、パブリケーション、サブスクリプションの仕組み を参照してください。


サーバー上の単一のコレクションの異なるサブセット(または「ビュー」)をクライアント上の複数のコレクションとして公開するにはどうすればよいですか?

これが私の質問を説明するのに役立ついくつかの疑似コードです:

サーバー上のitemsコレクション

何百万ものレコードを持つサーバーにitemsコレクションがあるとします。また、次のことも想定します。

  1. 50のレコードでは、enabledプロパティがtrueに設定されています。
  2. 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
}

サーバーコード

同じサーバーコレクションの2つの「ビュー」を公開しましょう。 1つは50レコードのカーソルを送信し、もう1つは100レコードのカーソルを送信します。この架空のサーバー側データベースには4億5800万以上のレコードがあり、クライアントはそれらすべてを知る必要はありません(実際、これらをすべて送信するのに、この例ではおそらく数時間かかります)。

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を宣言する必要があります。欠陥がどこにあるかが明らかになるはずです:enabled_itemsItemsprocessed_itemsItemsはどのように区別されますか?

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

サルパッチを配置すると、これは機能します。ただし、オフラインモードにすると、変更はすぐにはクライアントに表示されません。変更を確認するには、サーバーに接続する必要があります。

正しいアプローチは何ですか?


編集:私はこのスレッドを再検討したところ、現状では、私の質問と回答、および大量のコメントには多くの誤った情報が含まれていることに気づきました。

結論は、私がパブリッシュ/サブスクライブ関係を誤解しているということです。カーソルを公開すると、同じサーバーコレクションから発信された他の公開されたカーソルとは別のコレクションとしてクライアントに表示されると思いました。これは単にそれがどのように機能するかではありません。クライアントとサーバーの両方が同じコレクションを持っているという考えですが、コレクションのinが異なるのです。 pub-subコントラクトは、クライアントに送信されるドキュメントを交渉します。トムの答えは技術的には正しいですが、私の仮定を変えるためにいくつかの詳細が欠けていました。 Tomの説明に基づいて、別のSOスレッドで私のものと同様の質問に答えましたが、Meteorのpub-subに関する私の元の誤解を覚えておいてください: Meteor publish/subscribe戦略ユニークなクライアント側コレクション

これがこのスレッドに出くわし、何より混乱している人を助けることを願っています!

36
matb33

アイテムを確認するときに、クライアント側で同じクエリを使用できませんか?

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つの別個のコレクションがある場合とは対照的)ので、この方法の方が適しています。

注意

私は不明瞭であることに気づいたので、これを少し広げました。

34
Tom Coleman

このように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");
6
Lloyd

コレクションごとに1つのパブリッシュ/サブスクライブを使用して問題に取り組み、findクエリで$orを活用することにより、いくつかの有望な予備結果を達成することができました。

アイデアは、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"); };
});

パラメータはオブジェクトとして与えられます。これは、1つのパブリッシュ/サブスクライブが$ or'dされているため、適切なパラメータを取得するための方法が必要でした。

テンプレートで実際に使用するには:

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

つまり、サーバーとクライアントの両方で同じコードを実行することを利用しています。サーバーが何かを実行していない場合は、クライアントが実行するか、またはその逆です。

そして最も重要なのは、関心のあるレコードだけがクライアントに送信されることです。これは、単純に$ orを自分で実行することにより、抽象化レイヤーなしですべて達成できますが、サブセットが追加されると、$ orはかなり醜くなります。これは、最小限のコードで管理するのに役立ちます。

私はそれをテストするためにこれを素早く書きました、長さとドキュメントの欠如に対する謝罪:

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