Skip to content

Commit bf6683c

Browse files
jeonghanjooclaude
andauthored
Phase 3: Fields and References Async Support (#3)
* Add Phase 3 planning: Fields and References async support - Detailed implementation plan for ReferenceField async support - AsyncReferenceProxy design for lazy loading in async contexts - LazyReferenceField async fetch methods - GridFS async operations for file handling - Cascade operations async support - Comprehensive usage examples and testing strategy 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Update GridFS implementation to use PyMongo's native async API - Replace Motor references with PyMongo's gridfs.asynchronous module - Use AsyncGridFSBucket from gridfs.asynchronous - Add note about PyMongo's built-in async support - Include example of direct PyMongo GridFS usage Thanks to user feedback about current PyMongo API 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Add PyMongo GridFS async API tutorial documentation - Comprehensive guide for PyMongo's native GridFS async support - Covers AsyncGridFS and AsyncGridFSBucket usage - Includes practical examples and best practices - Documents key differences between legacy and modern APIs This documentation supports Phase 3 implementation. 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Implement Phase 3: Fields and References async support - ReferenceField async support with AsyncReferenceProxy - Async dereferencing via fetch() method - Connection type detection in __get__ - _async_lazy_load_ref() for database lookups - LazyReferenceField async_fetch() method - Async version of fetch() for manual dereferencing - Maintains compatibility with sync behavior - GridFS async operations - AsyncGridFSProxy for file operations - FileField async_put() and async_get() methods - Support for file metadata and custom collections - Streaming support for large files - Comprehensive test coverage - 8 tests for async reference fields - 9 tests for async GridFS operations - All tests passing with proper error handling Known limitations: - ListField with ReferenceField doesn't auto-convert to AsyncReferenceProxy - This is tracked as a future enhancement 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * docs: Update progress tracking for Phase 3 completion - Mark all Phase 3 tasks as completed in PROGRESS_FIELDS.md - Update PROGRESS.md with Phase 3 completion summary - Document deferred items moved to Phase 4 - Note known limitations and test status * chore: Remove PROGRESS_FIELDS.md after Phase 3 completion Phase 3 has been successfully completed with all tasks either done or deferred to Phase 4 * docs: Add Phase 3 learnings and GridFS tutorial - Add Phase 3 implementation learnings to CLAUDE.md - Include GridFS async tutorial documentation - Document design decisions and known limitations - Provide guidance for future development --------- Co-authored-by: Claude <[email protected]>
1 parent 1f2d060 commit bf6683c

File tree

8 files changed

+1252
-10
lines changed

8 files changed

+1252
-10
lines changed

CLAUDE.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,56 @@ When working on async support implementation, follow this workflow:
235235
#### Migration Strategy
236236
- Projects can use both sync and async QuerySets in same codebase
237237
- Connection type determines which methods are available
238-
- Clear error messages guide users to correct method usage
238+
- Clear error messages guide users to correct method usage
239+
240+
### Phase 3 Implementation Learnings
241+
242+
#### ReferenceField Async Design
243+
- **AsyncReferenceProxy Pattern**: In async context, ReferenceField returns a proxy object requiring explicit `await proxy.fetch()`
244+
- This prevents accidental sync operations in async code and makes async dereferencing explicit
245+
- The proxy caches fetched values to avoid redundant database calls
246+
247+
#### Field-Level Async Methods
248+
- Async methods should be on the field class, not on proxy instances:
249+
```python
250+
# Correct - call on field class
251+
await AsyncFileDoc.file.async_put(file_obj, instance=doc)
252+
253+
# Incorrect - don't call on proxy instance
254+
await doc.file.async_put(file_obj)
255+
```
256+
- This pattern maintains consistency and avoids confusion with instance methods
257+
258+
#### GridFS Async Implementation
259+
- Use PyMongo's native `gridfs.asynchronous` module instead of Motor
260+
- Key imports: `from gridfs.asynchronous import AsyncGridFSBucket`
261+
- AsyncGridFSProxy handles async file operations (read, delete, replace)
262+
- File operations return sync GridFSProxy for storage in document to maintain compatibility
263+
264+
#### LazyReferenceField Enhancement
265+
- Added `async_fetch()` method directly to LazyReference class
266+
- Maintains same caching behavior as sync version
267+
- Works seamlessly with existing passthrough mode
268+
269+
#### Error Handling Patterns
270+
- GridFS operations need careful error handling for missing files
271+
- Stream position management critical for file reads (always seek(0) after write)
272+
- Grid ID extraction from different proxy types requires type checking
273+
274+
#### Testing Async Fields
275+
- Use separate test classes for each field type for clarity
276+
- Test both positive cases and error conditions
277+
- Always clean up GridFS collections in teardown to avoid test pollution
278+
- Verify proxy behavior separately from actual async operations
279+
280+
#### Known Limitations
281+
- **ListField with ReferenceField**: Currently doesn't auto-convert to AsyncReferenceProxy
282+
- This is a complex case requiring deeper changes to ListField
283+
- Documented as limitation - users need manual async dereferencing for now
284+
- Could be addressed in future enhancement
285+
286+
#### Design Decisions
287+
- **Explicit over Implicit**: Async dereferencing must be explicit via `fetch()` method
288+
- **Proxy Pattern**: Provides clear indication when async operation needed
289+
- **Field-Level Methods**: Consistency with sync API while maintaining async safety
290+
- **Native PyMongo**: Leverage PyMongo's built-in async support rather than external libraries

PROGRESS.md

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,12 @@ async def async_run_in_transaction():
153153
- [x] async_create(), async_update(), async_delete() 벌크 작업
154154
- [x] 비동기 커서 관리 및 최적화
155155

156-
### Phase 3: 필드 및 참조 (2-3주)
157-
- [ ] ReferenceField에 async_fetch() 메서드 추가
158-
- [ ] AsyncReferenceProxy 구현
159-
- [ ] LazyReferenceField 비동기 지원
160-
- [ ] GridFS 비동기 작업 (async_put, async_get)
161-
- [ ] 캐스케이드 작업 비동기화
156+
### Phase 3: 필드 및 참조 (2-3주)**완료** (2025-07-31)
157+
- [x] ReferenceField에 async_fetch() 메서드 추가
158+
- [x] AsyncReferenceProxy 구현
159+
- [x] LazyReferenceField 비동기 지원
160+
- [x] GridFS 비동기 작업 (async_put, async_get)
161+
- [ ] 캐스케이드 작업 비동기화 (Phase 4로 이동)
162162

163163
### Phase 4: 고급 기능 (3-4주)
164164
- [ ] 하이브리드 신호 시스템 구현
@@ -304,4 +304,32 @@ author = await post.author.async_fetch()
304304
- `async_aggregate()`, `async_distinct()` - 고급 집계 기능
305305
- `async_values()`, `async_values_list()` - 필드 프로젝션
306306
- `async_explain()`, `async_hint()` - 쿼리 최적화
307-
- 이들은 기본 인프라 구축 후 필요시 추가 가능
307+
- 이들은 기본 인프라 구축 후 필요시 추가 가능
308+
309+
### Phase 3: Fields and References Async Support (2025-07-31 완료)
310+
311+
#### 구현 내용
312+
- **ReferenceField 비동기 지원**: AsyncReferenceProxy 패턴으로 안전한 비동기 참조 처리
313+
- **LazyReferenceField 개선**: LazyReference 클래스에 async_fetch() 메서드 추가
314+
- **GridFS 비동기 작업**: PyMongo의 native async API 사용 (gridfs.asynchronous.AsyncGridFSBucket)
315+
- **필드 레벨 비동기 메서드**: async_put(), async_get(), async_read(), async_delete(), async_replace()
316+
317+
#### 주요 성과
318+
- 17개 새로운 async 테스트 추가 (참조: 8개, GridFS: 9개)
319+
- 총 54개 async 테스트 모두 통과 (Phase 1: 23, Phase 2: 14, Phase 3: 17)
320+
- PyMongo의 native GridFS async API 완벽 통합
321+
- 명시적 async dereferencing으로 안전한 참조 처리
322+
323+
#### 기술적 세부사항
324+
- AsyncReferenceProxy 클래스로 async context에서 명시적 fetch() 필요
325+
- FileField.__get__이 GridFSProxy 반환, async 메서드는 field class에서 호출
326+
- async_read() 시 stream position reset 처리
327+
- GridFSProxy 인스턴스에서 grid_id 추출 로직 개선
328+
329+
#### 알려진 제한사항
330+
- ListField 내 ReferenceField는 AsyncReferenceProxy로 자동 변환되지 않음
331+
- 이는 low priority로 문서화되어 있으며 필요시 향후 개선 가능
332+
333+
#### Phase 4로 이동된 항목
334+
- 캐스케이드 작업 (CASCADE, NULLIFY, PULL, DENY) 비동기화
335+
- 복잡한 참조 관계의 비동기 처리

mongoengine/async_utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Async utility functions for MongoEngine async support."""
22

3+
import contextvars
4+
35
from mongoengine.connection import (
46
DEFAULT_CONNECTION_NAME,
57
ConnectionFailure,
@@ -10,6 +12,9 @@
1012
is_async_connection,
1113
)
1214

15+
# Context variable for async sessions
16+
_async_session_context = contextvars.ContextVar('mongoengine_async_session', default=None)
17+
1318

1419
async def get_async_collection(collection_name, alias=DEFAULT_CONNECTION_NAME):
1520
"""Get an async collection for the given name and alias.
@@ -49,6 +54,22 @@ def ensure_sync_connection(alias=DEFAULT_CONNECTION_NAME):
4954
)
5055

5156

57+
async def _get_async_session():
58+
"""Get the current async session if any.
59+
60+
:return: Current async session or None
61+
"""
62+
return _async_session_context.get()
63+
64+
65+
async def _set_async_session(session):
66+
"""Set the current async session.
67+
68+
:param session: The async session to set
69+
"""
70+
_async_session_context.set(session)
71+
72+
5273
async def async_exec_js(code, *args, **kwargs):
5374
"""Execute JavaScript code asynchronously in MongoDB.
5475
@@ -94,4 +115,6 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
94115
'ensure_sync_connection',
95116
'async_exec_js',
96117
'AsyncContextManager',
118+
'_get_async_session',
119+
'_set_async_session',
97120
]

mongoengine/base/datastructures.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"BaseList",
1212
"EmbeddedDocumentList",
1313
"LazyReference",
14+
"AsyncReferenceProxy",
1415
)
1516

1617

@@ -472,3 +473,35 @@ def __getattr__(self, name):
472473

473474
def __repr__(self):
474475
return f"<LazyReference({self.document_type}, {self.pk!r})>"
476+
477+
async def async_fetch(self, force=False):
478+
"""Async version of fetch()."""
479+
if not self._cached_doc or force:
480+
self._cached_doc = await self.document_type.objects.async_get(pk=self.pk)
481+
if not self._cached_doc:
482+
raise DoesNotExist("Trying to dereference unknown document %s" % (self))
483+
return self._cached_doc
484+
485+
486+
class AsyncReferenceProxy:
487+
"""Proxy object for async reference field access.
488+
489+
This proxy is returned when accessing a ReferenceField in an async context,
490+
requiring explicit async dereferencing via fetch() method.
491+
"""
492+
493+
__slots__ = ("field", "instance", "_cached_value")
494+
495+
def __init__(self, field, instance):
496+
self.field = field
497+
self.instance = instance
498+
self._cached_value = None
499+
500+
async def fetch(self):
501+
"""Explicitly fetch the referenced document."""
502+
if self._cached_value is None:
503+
self._cached_value = await self.field.async_fetch(self.instance)
504+
return self._cached_value
505+
506+
def __repr__(self):
507+
return f"<AsyncReferenceProxy: {self.field.name} (unfetched)>"

0 commit comments

Comments
 (0)