From 26b5d8de158828be6d8f78d93a62f8bb2c38cece Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 24 Feb 2025 17:13:39 +0530 Subject: [PATCH] perf: better `_table_fieldnames` cache --- frappe/model/base_document.py | 35 ++++++++++++++++++++++++----------- frappe/model/document.py | 23 ++++++++++++----------- frappe/model/meta.py | 6 +++--- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 99a773cb2c..78ab0fbdad 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -53,9 +53,13 @@ DOCTYPE_TABLE_FIELDS = [ ] TABLE_DOCTYPES_FOR_DOCTYPE = {df["fieldname"]: df["options"] for df in DOCTYPE_TABLE_FIELDS} + +# child tables cannot have child tables +TABLE_DOCTYPES_FOR_DOCTYPE_TABLES = {} + DOCTYPES_FOR_DOCTYPE = {"DocType", *TABLE_DOCTYPES_FOR_DOCTYPE.values()} -UNPICKLABLE_KEYS = ("meta", "permitted_fieldnames", "_parent_doc", "_weakref") +UNPICKLABLE_KEYS = ("meta", "permitted_fieldnames", "_parent_doc", "_weakref", "_table_fieldnames") def get_controller(doctype): @@ -139,7 +143,6 @@ class BaseDocument: if d.get("doctype"): self.doctype = d["doctype"] - self._table_fieldnames = {df.fieldname for df in self._get_table_fields()} self.update(d) self.dont_update_if_missing = [] @@ -355,6 +358,16 @@ class BaseDocument: return value + @cached_property + def _table_fieldnames(self) -> dict: + if self.doctype == "DocType": + return TABLE_DOCTYPES_FOR_DOCTYPE + + if self.doctype in DOCTYPES_FOR_DOCTYPE: + return TABLE_DOCTYPES_FOR_DOCTYPE_TABLES + + return self.meta._table_doctypes + def _get_table_fields(self): """ To get table fields during Document init @@ -556,17 +569,17 @@ class BaseDocument: return frappe.as_json(self.as_dict()) def get_table_field_doctype(self, fieldname): - try: - return self.meta.get_field(fieldname).options - except AttributeError: - if self.doctype == "DocType" and (table_doctype := TABLE_DOCTYPES_FOR_DOCTYPE.get(fieldname)): - return table_doctype - - raise + return self._table_fieldnames.get(fieldname) def get_parentfield_of_doctype(self, doctype): - fieldname = [df.fieldname for df in self.meta.get_table_fields() if df.options == doctype] - return fieldname[0] if fieldname else None + return next( + ( + fieldname + for fieldname, child_doctype in self._table_fieldnames.items() + if child_doctype == doctype + ), + None, + ) def db_insert(self, ignore_if_duplicate=False): """INSERT the document (with valid columns) in the database. diff --git a/frappe/model/document.py b/frappe/model/document.py index 2a24bd82b1..bcb713e4cb 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -263,20 +263,20 @@ class Document(BaseDocument, DocRef): return self def load_children_from_db(self): - for df in self._get_table_fields(): + for fieldname, child_doctype in self._table_fieldnames.items(): # Make sure not to query the DB for a child table, if it is a virtual one. # During frappe is installed, the property "is_virtual" is not available in tabDocType, so # we need to filter those cases for the access to frappe.db.get_value() as it would crash otherwise. - if hasattr(self, "doctype") and not hasattr(self, "module") and is_virtual_doctype(df.options): - self.set(df.fieldname, []) + if hasattr(self, "doctype") and not hasattr(self, "module") and is_virtual_doctype(child_doctype): + self.set(fieldname, []) continue if self.doctype == "DocType": # This special handling is required because of bootstrapping code that doesn't # handle failures correctly. children = frappe.db.get_values( - df.options, - {"parent": self.name, "parenttype": self.doctype, "parentfield": df.fieldname}, + child_doctype, + {"parent": self.name, "parenttype": self.doctype, "parentfield": fieldname}, "*", as_dict=True, order_by="idx asc", @@ -290,14 +290,14 @@ class Document(BaseDocument, DocRef): AND `parenttype`= %(parenttype)s AND `parentfield`= %(parentfield)s ORDER BY `idx` ASC {for_update}""".format( - table_name=get_table_name(df.options, wrap_in_backticks=True), + table_name=get_table_name(child_doctype, wrap_in_backticks=True), for_update="FOR UPDATE" if self.flags.for_update else "", ), - {"parent": self.name, "parenttype": self.doctype, "parentfield": df.fieldname}, + {"parent": self.name, "parenttype": self.doctype, "parentfield": fieldname}, as_dict=True, ) - self.set(df.fieldname, children or []) + self.set(fieldname, children or []) return self @@ -539,6 +539,7 @@ class Document(BaseDocument, DocRef): if getattr(self.meta, "is_virtual", False): # Virtual doctypes manage their own children return + for df in self.meta.get_table_fields(): self.update_child_table(df.fieldname, df) @@ -1068,11 +1069,11 @@ class Document(BaseDocument, DocRef): children = [] - for df in self.meta.get_table_fields(): - if parenttype and df.options != parenttype: + for fieldname, child_doctype in self._table_fieldnames.items(): + if parenttype and child_doctype != parenttype: continue - if value := self.get(df.fieldname): + if value := self.get(fieldname): children.extend(value) return children diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 753acaa98e..5b067f14f3 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -307,9 +307,6 @@ class Meta(Document): return valid_fields - def get_table_field_doctype(self, fieldname): - return TABLE_DOCTYPES_FOR_DOCTYPE.get(fieldname) - def get_field(self, fieldname): """Return docfield from meta.""" @@ -535,6 +532,9 @@ class Meta(Document): else: self._table_fields = self.get("fields", {"fieldtype": ["in", table_fields]}) + # table fieldname: doctype map + self._table_doctypes = {field.fieldname: field.options for field in self._table_fields} + def sort_fields(self): """ Sort fields on the basis of following rules (priority descending):