feat: Lazy loaded documents
https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance After first invokation, object's __dict__ will be used... then we can assume rest of the code works as expected (?)
This commit is contained in:
parent
4292fc9005
commit
eb77ddab69
3 changed files with 96 additions and 1 deletions
|
|
@ -1993,7 +1993,7 @@ import frappe._optimizations
|
|||
from frappe.cache_manager import clear_cache, reset_metadata_version
|
||||
from frappe.config import get_common_site_config, get_conf, get_site_config
|
||||
from frappe.core.doctype.system_settings.system_settings import get_system_settings
|
||||
from frappe.model.document import get_doc
|
||||
from frappe.model.document import get_doc, get_lazy_doc
|
||||
from frappe.model.meta import get_meta
|
||||
from frappe.realtime import publish_progress, publish_realtime
|
||||
from frappe.utils import get_traceback, mock, parse_json, safe_eval
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from functools import wraps
|
|||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeAlias, Union, overload
|
||||
|
||||
from typing_extensions import Self, override
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
import frappe
|
||||
|
|
@ -138,6 +139,13 @@ def get_doc_from_dict(data: dict[str, Any], **kwargs) -> "Document":
|
|||
raise ImportError(data["doctype"])
|
||||
|
||||
|
||||
def get_lazy_doc(doctype: str, name: str):
|
||||
controller = get_lazy_controller(doctype)
|
||||
if controller:
|
||||
return controller(doctype, name)
|
||||
raise ImportError(doctype)
|
||||
|
||||
|
||||
class Document(BaseDocument):
|
||||
"""All controllers inherit from `Document`."""
|
||||
|
||||
|
|
@ -1933,3 +1941,62 @@ def _document_values_generator(
|
|||
def unlock_document(doctype: str, name: str):
|
||||
frappe.get_doc(doctype, name).unlock()
|
||||
frappe.msgprint(frappe._("Document Unlocked"), alert=True)
|
||||
|
||||
|
||||
def get_lazy_controller(doctype):
|
||||
from frappe.model.document import LazyDocument
|
||||
|
||||
lazy_controllers = frappe.controllers.setdefault(f"lazy|{frappe.local.site}", {})
|
||||
if doctype not in lazy_controllers:
|
||||
original_controller = get_controller(doctype)
|
||||
|
||||
lazy_controller = type(f"Lazy{original_controller.__name__}", (LazyDocument, original_controller), {})
|
||||
meta = frappe.get_meta(doctype)
|
||||
for fieldname, child_doctype in meta._table_doctypes.items():
|
||||
setattr(lazy_controller, fieldname, LazyChildTable(fieldname, child_doctype))
|
||||
|
||||
lazy_controllers[doctype] = lazy_controller
|
||||
return lazy_controllers[doctype]
|
||||
|
||||
|
||||
class LazyDocument:
|
||||
"""Mixin for Document class that implments lazy loading for child tables."""
|
||||
|
||||
@override
|
||||
def load_children_from_db(self: Document):
|
||||
"""Override Document which eagerly loads child tables"""
|
||||
# This is a map of loaded children, it should get erased whenever load_children_from_db is
|
||||
# called to allow reloading lazily again.
|
||||
for fieldname in self._table_fieldnames:
|
||||
self.__dict__.pop(fieldname, None)
|
||||
|
||||
@override
|
||||
def get(self: Document, key, *args, **kwags):
|
||||
_ = getattr(self, key, None)
|
||||
return super().get(key, *args, **kwags)
|
||||
|
||||
|
||||
class LazyChildTable:
|
||||
__slots__ = ("doctype", "fieldname")
|
||||
|
||||
def __init__(self, fieldname: str, doctype: str) -> None:
|
||||
self.fieldname = fieldname
|
||||
self.doctype = doctype
|
||||
|
||||
def __get__(self, doc, objtype=None):
|
||||
# TODO: review cached_property magic
|
||||
children = frappe.db.sql(
|
||||
"""SELECT * FROM {table_name}
|
||||
WHERE `parent`= %(parent)s
|
||||
AND `parenttype`= %(parenttype)s
|
||||
AND `parentfield`= %(parentfield)s
|
||||
ORDER BY `idx` ASC""".format(
|
||||
table_name=get_table_name(self.doctype, wrap_in_backticks=True),
|
||||
),
|
||||
{"parent": str(doc.name), "parenttype": doc.doctype, "parentfield": self.fieldname},
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
# Update __dict__ and convert to Document objects
|
||||
doc.extend(self.fieldname, children or [])
|
||||
return doc.__dict__[self.fieldname] # Note: avoid any high level access here
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from unittest.mock import Mock, patch
|
|||
import frappe
|
||||
from frappe.app import make_form_dict
|
||||
from frappe.core.doctype.doctype.test_doctype import new_doctype
|
||||
from frappe.core.doctype.user.user import User
|
||||
from frappe.desk.doctype.note.note import Note
|
||||
from frappe.model.naming import make_autoname, parse_naming_series, revert_series_if_last
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
|
@ -524,6 +525,33 @@ class TestDocument(IntegrationTestCase):
|
|||
changed_val = frappe.db.get_single_value(c.doctype, key)
|
||||
self.assertEqual(val, changed_val)
|
||||
|
||||
def test_lazy_documents(self):
|
||||
# Warmup meta etc
|
||||
_ = frappe.get_lazy_doc("User", "Guest")
|
||||
eager_guest: User = frappe.get_doc("User", "Guest")
|
||||
|
||||
# Only one query for parent document
|
||||
with self.assertQueryCount(1):
|
||||
guest: User = frappe.get_lazy_doc("User", "Guest")
|
||||
self.assertEqual(guest.user_type, "Website User")
|
||||
|
||||
# Only one query for one table access
|
||||
with self.assertQueryCount(1):
|
||||
guest_role = guest.roles[0]
|
||||
self.assertEqual(guest_role.role, "Guest")
|
||||
self.assertIsInstance(guest_role, type(eager_guest.roles[0]))
|
||||
|
||||
# No queries for repeat access, same object
|
||||
with self.assertQueryCount(0):
|
||||
guest_role_repeat_access = guest.roles[0]
|
||||
self.assertIs(guest_role, guest_role_repeat_access)
|
||||
|
||||
with self.assertQueryCount(0):
|
||||
self.assertIs(guest.roles, guest.get("roles"))
|
||||
|
||||
# things accessing __dict__ by default should be updated too
|
||||
self.assertTrue(frappe.get_lazy_doc("User", "Guest").get("roles"))
|
||||
|
||||
|
||||
class TestDocumentWebView(IntegrationTestCase):
|
||||
def get(self, path, user="Guest"):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue