-
Notifications
You must be signed in to change notification settings - Fork 0
/
13s-advanced-publications.md.erb
190 lines (128 loc) · 10.3 KB
/
13s-advanced-publications.md.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
---
title: Advanced Publications
slug: advanced-publications
date: 0013/01/02
number: 13.5
level: book
sidebar: true
photoUrl: http://www.flickr.com/photos/ikewinski/8390558986/
photoAuthor: Mike Lewinski
contents: Learn more advanced patterns for manipulating publications.|See just how flexible publications and subscriptions can get.
paragraphs: 36
---
By now you should have a good grasp of how publications and subscriptions interact. So let's get rid of the training wheels and examine a few more advanced scenarios.
### Publishing a Collection Multiple Times
In [our first sidebar about publications](/chapter/publications-and-subscriptions/), we saw some of the more common publication and subscription patterns, and we learned how the `_publishCursor` function made them very easy to implement for our own sites.
First, let's recall what `_publishCursor` does for us exactly: it takes all the documents that match a given cursor, and pushes them down into the client-side collection *of the same name*. Notice that the name of the _publication_ is in no way involved.
This means we can have _more than one publication_ linking the client and server versions of any collection.
We've already encountered this pattern in our [pagination chapter](/chapter/pagination/), when we published a paginated subset of all the posts in addition to the currently displayed post.
Another similar use case is to publish an *overview* of a large set of documents, as well as the full details of a single item:
<%= diagram "doublecollection", "Publishing a collection twice", "pull-center" %>
~~~js
Meteor.publish('allPosts', function() {
return Posts.find({}, {fields: {title: true, author: true}});
});
Meteor.publish('postDetail', function(postId) {
return Posts.find(postId);
});
~~~
Now when the client subscribes to those two publications, its `'posts'` collection gets populated from two sources: a list of titles and author's names from the first subscription, and the full details of a post from the second.
You may realize that the post published by `postDetail` is also being published by `allPosts` (although with only a subset of its properties). However, Meteor takes care of the overlap by merging the fields and ensuring there is no duplicate post.
This is great, because now when we render the list of post summaries, we are dealing with data objects that have just enough data for us to show what we need. However, when we render out the page for a single post, we have everything we need to show it. Of course, we need to take care on the client to not expect all fields to be available on all posts in this case -- this is a common gotcha!
It should be noted that you're not limited to varying document properties. You could very well publish the same properties in both publications, but order items differently.
~~~js
Meteor.publish('newPosts', function(limit) {
return Posts.find({}, {sort: {submitted: -1}, limit: limit});
});
Meteor.publish('bestPosts', function(limit) {
return Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit});
});
~~~
<%= caption "server/publications.js" %>
### Subscribing to a Publication Multiple Times
We've just seen how you can publish a single collection more than once. It turns out you can accomplish a very similar result with another pattern: creating a single publication, but *subscribing* to it multiple times.
In Microscope, we subscribe to the `posts` publication multiple times, but Iron Router sets up and tears down each subscription for us. Yet there's no reason why we couldn't subscribe multiple times *simultaneously*.
For example, let's say we wanted to load both the newest and best posts in memory at the same time:
<%= diagram "subscribetwice", "Subscribing twice to one publication", "pull-center" %>
We're setting up a single publication:
~~~js
Meteor.publish('posts', function(options) {
return Posts.find({}, options);
});
~~~
And we then subscribe to this publication multiple times. In fact this is more or less exactly what we're doing in Microscope:
~~~js
Meteor.subscribe('posts', {submitted: -1, limit: 10});
Meteor.subscribe('posts', {baseScore: -1, submitted: -1, limit: 10});
~~~
So what's happening here exactly? Each browser is opening up *two* different subscriptions, each connecting to the *same* publication on the server.
Each subscription provides different arguments to that publication, but fundamentally, each time a (different) set of documents is being plucked from the `posts` collection and sent down the wire to the client-side collection.
You can even subscribe to the same publication twice with *the same arguments!* It's hard to think of many scenarios where that would be useful, but the flexibility might be useful one day!
### Multiple Collections in a Single Subscription
Unlike more traditional relational databses like MySQL which make use of *joins*, NoSQL databases like Mongo are all about *denormalizing* and *embedding*. Let's see how that works in the context of Meteor.
Let's look at a concrete example. We've added comments to our posts, and so far, we've been happy to only publish the comments on the single post that the user is looking at.
However, suppose we wanted to show comments on *all* the posts on the front page (keeping in mind that these posts will change as we paginate through them). This use case presents a good reason for embedding comments in posts, and in fact is what pushed us to denormalize comment *counts*.
Of course we could always just embed comments in posts, getting rid of the `Comments` collection altogether. But like we previously saw in the *Denormalization* chapter, by doing so we would be losing some of the extra benefits of working with separate collections.
But it turns out there's a trick involving subscriptions that makes it possible to embed our comments while preserving separate collections.
Let's suppose that along with our front-page list of posts, we want to subscribe to a list of the top 2 comments for each post.
It would be difficult to accomplish this with an independent comments publication, especially if the list of posts was limited in some way (say, the 10 most recent). We'd have to write a publication that looked something like this:
<%= diagram "multiplecollections", "Two collections in one subscription", "pull-center" %>
~~~js
Meteor.publish('topComments', function(topPostIds) {
return Comments.find({postId: {$in: topPostIds}});
});
~~~
This would be a problem from a performance standpoint, as the publication would need to get torn down and re-established each time the list of `topPostIds` changed.
There is a way around this though. We just use the fact that we can not only have more than one *publication* per *collection*, but we can also have more than one *collection* per *publication*:
~~~js
Meteor.publish('topPosts', function(limit) {
var sub = this, commentHandles = [], postHandle = null;
// send over the top two comments attached to a single post
function publishPostComments(postId) {
var commentsCursor = Comments.find({postId: postId}, {limit: 2});
commentHandles[postId] =
Mongo.Collection._publishCursor(commentsCursor, sub, 'comments');
}
postHandle = Posts.find({}, {limit: limit}).observeChanges({
added: function(id, post) {
publishPostComments(id);
sub.added('posts', id, post);
},
changed: function(id, fields) {
sub.changed('posts', id, fields);
},
removed: function(id) {
// stop observing changes on the post's comments
commentHandles[id] && commentHandles[id].stop();
// delete the post
sub.removed('posts', id);
}
});
sub.ready();
// make sure we clean everything up (note `_publishCursor`
// does this for us with the comment observers)
sub.onStop(function() { postHandle.stop(); });
});
~~~
Note that we aren't returning anything in this publication, as we manually send messages to the `sub` ourselves (via `.added()` and friends). So we don't need to ask `_publishCursor` to do it for us by returning a cursor.
Now, every time we publish a post we also automatically publish the top two comments attached to it. And all with a single subscription call!
Although Meteor doesn't make this approach very straightforward yet, you can also look into the `publish-with-relations` package on Atmosphere, which aims to make this pattern easier to use.
### Linking different collections
What else can our newfound knowledge of the flexibility of subscriptions give us? Well, if we don't use `_publishCursor`, we don't need to follow the constraint that the source collection on the server needs to have the same name as the target collection on the client.
<%= diagram "linkedcollections", "One collection for two subscriptions", "pull-center" %>
One reason why we would want to do this is *Single Table Inheritance*.
Suppose that we wanted to reference various types of objects from our posts, each of which stored common fields but also differed slightly in content. For example, we could be building a Tumblr-like blogging engine where each post possesses the usual ID, timestamp, and title; but in addition can also feature an image, video, link, or just text.
We could store all these objects in a single `'resources'` collection, using a `type` attribute to indicate which sort of object they are. (`video`, `image`, `link`, etc.).
And although we'd have a single `Resources` collection on the server, we could transform that single collection into multiple `Videos`, `Images`, etc. collections on the client with the following bit of magic:
~~~js
Meteor.publish('videos', function() {
var sub = this;
var videosCursor = Resources.find({type: 'video'});
Mongo.Collection._publishCursor(videosCursor, sub, 'videos');
// _publishCursor doesn't call this for us in case we do this more than once.
sub.ready();
});
~~~
We are telling `_publishCursor` to publish our videos (just like returning) the cursor would do, but rather than publish to the `resources` collection on the client, instead we are publishing from `'resources'` to `'videos'`.
Another similiar idea is to use publish to a client side collection where there's *no server side collection at all!* For instance, you might grab the data from a 3rd party service, and publish them into a client-side collection.
Thanks to the flexibility of the publish API, the possibilities are endless.