Skip to content

Commit a266136

Browse files
authored
Merge pull request #463 from share/query-docs
📝 Add query documentation
2 parents e29936c + 7c0a904 commit a266136

File tree

4 files changed

+215
-2
lines changed

4 files changed

+215
-2
lines changed

docs/document-history.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Document history
3-
nav_order: 8
3+
nav_order: 9
44
layout: default
55
---
66

docs/middleware/index.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ copy:
3333
`maxRetries` -- number
3434
> The maximum number of times to retry submitting the op
3535
36+
`channels` -- string[]
37+
> The [pub/sub]({{ site.baseurl }}{% link pub-sub.md %}) channels the op will publish to
38+
3639
---
3740

3841
# Middleware
@@ -132,6 +135,44 @@ This action has these additional `context` properties:
132135

133136
> The presence object being sent. Its shape depends on its [type]({{ site.baseurl }}{% link types/index.md %})
134137
138+
### `'query'`
139+
140+
A new query request is about to be submitted to the database
141+
142+
This action has these additional `context` properties:
143+
144+
`index` -- string
145+
146+
> The name of the query's collection or [projection]({{ site.baseurl }}{% link projections.md %})
147+
148+
`collection` -- string
149+
150+
> The name of the query's target collection
151+
152+
`projection` -- string
153+
154+
> The name of the query's [projection]({{ site.baseurl }}{% link projections.md %})
155+
156+
`fields` -- Object
157+
158+
> The query's projection [fields]({{ site.baseurl }}{% link api/backend.md %}#addprojection)
159+
160+
`channel` -- string
161+
162+
> The [Pub/Sub]({{ site.baseurl }}{% link adapters/pub-sub.md %}) channel the query will subscribe to. Defaults to its collection channel.
163+
164+
`query` -- Object
165+
166+
> The query being submitted to the database adapter
167+
168+
`options` -- Object
169+
170+
> The query [options]({{ site.baseurl }}{% link api/connection.md %}#createfetchquery)
171+
172+
`db` -- [DB]({{ site.baseurl }}{% link adapters/database.md %})
173+
174+
> The database the query will be run against
175+
135176
### `'readSnapshots'`
136177

137178
One or more snapshots were loaded from the database for a fetch or subscribe.

docs/presence.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Presence
3-
nav_order: 9
3+
nav_order: 10
44
layout: default
55
---
66

docs/queries.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
---
2+
title: Queries
3+
nav_order: 8
4+
layout: default
5+
---
6+
7+
# Queries
8+
{: .no_toc }
9+
10+
Some [database adapters]({{ site.baseurl }}{% link adapters/database.md %}) support queries. You can use queries to fetch or subscribe to many documents matching the provided query.
11+
12+
1. TOC
13+
{:toc}
14+
15+
## Fetch query
16+
17+
A fetch query will simply query the database for all the documents that match the given query, and return them. The query results will be passed into the provided callback, or can be retrieved by listening for the [`'ready'`]({{ site.baseurl }}{% link api/query.md %}#ready) event.
18+
19+
For example, when using `sharedb-mongo`, you can use Mongo queries. This will fetch all the documents whose `userId` is `1`:
20+
21+
```js
22+
const options = {}
23+
connection.createFetchQuery('my-collection', {userId: 1}, options, (error, results) => {
24+
// results is an array of Doc instances with their data populated
25+
})
26+
```
27+
28+
{: .info }
29+
See the [API documentation]({{ site.baseurl }}{% link api/connection.md %}#createfetchquery) for valid options.
30+
31+
[`createFetchQuery`]({{ site.baseurl }}{% link api/connection.md %}#createfetchquery) also returns a
32+
[`Query`]({{ site.baseurl }}{% link api/query.md %}) instance, which can be used instead:
33+
34+
```js
35+
const query = connection.createFetchQuery('my-connection', {userId: 1})
36+
query.on('ready', () => {
37+
// results are now available in query.results
38+
})
39+
```
40+
41+
## Subscribe query
42+
43+
A subscribe query acts similarly to a [fetch query](#fetch-query), except a subscribe query will update its own [`results`]({{ site.baseurl }}{% link api/query.md %}#results--array) in response to documents being [added]({{ site.baseurl }}{% link api/query.md %}#insert), [removed]({{ site.baseurl }}{% link api/query.md %}#remove) or [moved]({{ site.baseurl }}{% link api/query.md %}#move) (e.g. if the query is sorted).
44+
45+
{: .warn }
46+
A subscribe query will **not** subscribe the returned `Doc` instances. If you want the documents themselves to *also* receive updates, you will need to subscribe them manually using [`doc.subscribe()`]({{ site.baseurl }}{% link api/doc.md %}#subscribe).
47+
48+
Subscribe queries can be created similarly to fetch queries, but you may also be interested in [other events]({{ site.baseurl }}{% link api/query.md %}#events):
49+
50+
```js
51+
const query = connection.createSubscribeQuery('my-connection', {userId: 1})
52+
query.on('ready', () => {
53+
// The initial results are available in query.results
54+
})
55+
query.on('changed', () => {
56+
// This is a catch-all event that is fired when further changes are made.
57+
// It is called just after the 'insert', 'move', and 'remove' events.
58+
})
59+
```
60+
61+
## Performance
62+
63+
Arbitrary queries are not necessarily performant out-of-the-box. As with all database queries, some steps should be taken to keep queries fast.
64+
65+
### Indexing
66+
67+
As with all database queries, appropriate indexes should be set up to expedite common queries. The exact details of this will vary depending on the underlying database.
68+
69+
### Paging
70+
71+
If a query can potentially return a large number of results, you may want to consider limiting the number of results that can be returned, again similarly to how a "traditional" query might be altered.
72+
73+
For example, a `sharedb-mongo` limit might look like this:
74+
75+
```js
76+
const query = connection.createSubscribeQuery('my-connection', {userId: 1, $skip: 10, $limit: 10})
77+
```
78+
79+
{: .info }
80+
`sharedb-mongo` [queries](https://github.com/share/sharedb-mongo#queries) are not quite the same as MongoDB queries, and allow definition of some cursor functions directly in the query object.
81+
82+
One way subscription queries act differently to a direct database query is that pages will automatically be updated. For example, consider this collection:
83+
84+
```json
85+
{"userId": 1, "value": 5}
86+
{"userId": 1, "value": 7}
87+
{"userId": 1, "value": 3}
88+
{"userId": 1, "value": 1}
89+
{"userId": 2, "value": 2}
90+
```
91+
92+
Let's run a limited query:
93+
94+
```js
95+
const q = {userId: 1, $sort: {value: 1}, $limit: 3}
96+
const query = connection.createSubscribeQuery('values', q)
97+
query.on('ready', () => {
98+
// query.results looks like this:
99+
// [
100+
// {userId: 1, value: 1},
101+
// {userId: 1, value: 3},
102+
// {userId: 1, value: 5},
103+
// ]
104+
})
105+
```
106+
107+
Now let's insert a new document and listen for the change:
108+
109+
```js
110+
const newDoc = connection.get('values', id)
111+
newDoc.create({userId: 1, value: 0})
112+
113+
query.on('changed', () => {
114+
// query.results has been updated:
115+
// [
116+
// {userId: 1: value: 0},
117+
// {userId: 1: value: 1},
118+
// {userId: 1: value: 3},
119+
// ]
120+
})
121+
```
122+
123+
So, as per our query, the `query.results` will always contain the first 3 values, sorted in ascending order, regardless of what the original values were when first subscribing.
124+
125+
{: .warn }
126+
After they've been created, queries cannot be updated. If you want a new page of results, you'll have to create a new query.
127+
128+
### Subscription channels
129+
130+
Since ShareDB can't understand queries itself (that responsibility belongs to the [database adapter]({{ site.baseurl }}{% link adapters/database.md %})), it also doesn't understand which queries will care about which ops.
131+
132+
{: .warn }
133+
Queries will check if they need to be updated after an op is submitted to **any** document in their collection.
134+
135+
Chances are that your queries will only care about a small sub-set of your documents. For example, let's say we have a database of some blog posts:
136+
137+
```json
138+
{"userId": 1, "title": "Hello, World!"}
139+
{"userId": 1, "title": "11 Weird Tricks for Collaborative Editing"}
140+
{"userId": 2, "title": "Nana's Lasagne"}
141+
{"userId": 2, "title": "Tastiest Pesto in the World!"}
142+
```
143+
144+
If we want to show all the blog articles for a particular user, we may create a query:
145+
146+
```js
147+
const query = connection.createSubscribeQuery('posts', {userId: 1})
148+
```
149+
150+
However, we may have hundreds or thousands of other users creating posts, which our query won't care about. We don't want our query polling the database every time another user updates their blog.
151+
152+
We can solve this in ShareDB's [middleware]({{ site.baseurl }}{% link middleware/index.md %}):
153+
154+
```js
155+
backend.use('commit', (context, next) => {
156+
// Set ops to publish to our special user-specific pub/sub channel
157+
context.channels.push(userChannel(context))
158+
next()
159+
})
160+
161+
backend.use('query', (context, next) => {
162+
// Set our query to only listen for changes on our user-specific channel
163+
context.channel = userChannel(context)
164+
next()
165+
})
166+
167+
function userChannel(context) {
168+
// Assume the userId has been stored in agent.custom on connection
169+
const userId = context.agent.custom.userId
170+
return context.collection + userId
171+
}
172+
```

0 commit comments

Comments
 (0)