Skip to content

Commit 6157aa8

Browse files
committed
docs(how-to): add "Resolve relations to brains without waking objects"
Diataxis how-to for the new zoid/oid query operator: the wake-free intid -> KeyReference.oid -> zoid -> catalog(zoid=[...]) recipe for related-items / back-reference resolution, with a full example and notes on security composition, no-reindex, empty-matches-nothing, and order preservation. Registered in the how-to toctree; adds zoid/oid/intid/IOBTree/… to the vale vocabulary.
1 parent 20b1940 commit 6157aa8

3 files changed

Lines changed: 124 additions & 0 deletions

File tree

docs/.vale/styles/config/vocabularies/Project/accept.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,10 @@ roundtrips
250250
[Uu]npickl(e|ed|es|ing)
251251
truthy
252252
unsortable
253+
[Zz]oid[s]?
254+
[Oo]id[s]?
255+
[Ii]ntid[s]?
256+
IOBTree
257+
KeyReferenceToPersistent
258+
RelationList
259+
findRelations

docs/sources/how-to/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,6 @@ titlesonly: true
3838
---
3939
write-custom-translator
4040
query-raw-sql
41+
resolve-relations-by-zoid
4142
use-with-distribution
4243
```
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<!-- diataxis: how-to -->
2+
3+
# Resolve relations to brains without waking objects
4+
5+
## Problem
6+
7+
You hold a set of ZODB object ids — typically from `zc.relation` /
8+
`zope.intid` (a `RelationList` field, or back-references from
9+
`catalog.findRelations(...)`) — and you want the catalog **brains** for
10+
those objects: their metadata, filtered by the current user's `View`
11+
permission.
12+
13+
The naive approach wakes every object:
14+
15+
```python
16+
from zope.component import getUtility
17+
from zope.intid.interfaces import IIntIds
18+
19+
intids = getUtility(IIntIds)
20+
objs = [intids.queryObject(intid) for intid in relation_intids] # loads each!
21+
```
22+
23+
On a listing/aside tile this is an object-load N+1: each `queryObject`
24+
loads the object (and, for an anonymous request, walks the acquisition
25+
chain to check `View`). A page with a few dozen related items can cost
26+
hundreds of ZODB loads.
27+
28+
## Solution
29+
30+
pgcatalog stores every record in `object_state` keyed by its `zoid` (the
31+
BIGINT primary key — the catalog `rid` *is* the zoid). The built-in
32+
`zoid` / `oid` query operator lets you fetch the security-filtered brains
33+
for a set of object ids directly, **without waking the objects**:
34+
35+
```python
36+
brains = catalog(zoid=[123, 456, 789])
37+
```
38+
39+
Getting the zoids from relations is wake-free — the intid utility's
40+
`KeyReferenceToPersistent` already stores the object's oid, and
41+
`oid → zoid` is just `int.from_bytes(...)`:
42+
43+
```python
44+
from zope.component import getUtility
45+
from zope.intid.interfaces import IIntIds
46+
47+
intids = getUtility(IIntIds)
48+
refs = intids.refs # IOBTree: intid -> KeyReferenceToPersistent
49+
50+
zoids = []
51+
for intid in relation_intids:
52+
kr = refs.get(intid)
53+
if kr is not None:
54+
zoids.append(int.from_bytes(kr.oid, "big")) # no getObject()
55+
56+
brains = catalog(zoid=zoids) # security-filtered, no wakes
57+
```
58+
59+
If you happen to hold raw 8-byte ZODB oids instead of ints, pass them to
60+
`oid=` and pgcatalog converts them for you:
61+
62+
```python
63+
brains = catalog(oid=[obj._p_oid for obj in ...])
64+
```
65+
66+
## Full example: related-items resolution
67+
68+
```python
69+
from Acquisition import aq_inner
70+
from zc.relation.interfaces import ICatalog
71+
from zope.component import getUtility
72+
from zope.intid.interfaces import IIntIds
73+
74+
def related_brains(context, catalog):
75+
intids = getUtility(IIntIds)
76+
relcat = getUtility(ICatalog)
77+
refs = intids.refs
78+
79+
# direct references (a RelationList field) + back-references
80+
tids = [rv.to_id for rv in getattr(context, "related_items", []) or []]
81+
tids += [
82+
rel.from_id
83+
for rel in relcat.findRelations(
84+
{"to_id": intids.getId(aq_inner(context)),
85+
"from_attribute": "related_items"}
86+
)
87+
]
88+
89+
zoids = []
90+
for tid in dict.fromkeys(tids): # de-dup, keep order
91+
kr = refs.get(tid)
92+
if kr is not None:
93+
zoids.append(int.from_bytes(kr.oid, "big"))
94+
95+
return catalog(zoid=zoids) # brains, view-filtered, no wakes
96+
```
97+
98+
To preserve the relation order, build a `{brain zoid: brain}` map and
99+
iterate your `zoids` list — a plain SQL `IN`/`ANY` does not guarantee
100+
row order.
101+
102+
## Notes
103+
104+
- **Security.** `zoid` / `oid` compose with the normal query and with the
105+
`allowedRolesAndUsers` filter that `searchResults` injects, so
106+
`catalog(zoid=...)` returns only brains the current user may view.
107+
Use `unrestrictedSearchResults(zoid=...)` to bypass the filter.
108+
- **No storage, no reindex.** The operator filters the existing primary-key
109+
column; there is nothing to index and no migration for existing sites.
110+
- **Empty set matches nothing.** `catalog(zoid=[])` returns no rows (it
111+
never degrades to "match the whole table").
112+
- **Accepted values.** `zoid` takes ints or digit strings; `oid` takes raw
113+
8-byte oids; both accept a scalar or a list. Non-coercible values are
114+
dropped (ZCatalog-lenient).
115+
- **Composes.** `catalog(zoid=[...], portal_type="Document")` ANDs the two
116+
filters like any other query.

0 commit comments

Comments
 (0)