From e13746746663fdb1f0ba8a57bb780b0c28857c92 Mon Sep 17 00:00:00 2001 From: ScriptSmith Date: Thu, 29 Jan 2026 11:28:11 +1000 Subject: [PATCH 1/2] Cache the dynamic gql schema --- rdrf/rdrf/models/definition/models.py | 23 +++++++++++++++++++++++ rdrf/rdrf/patients/query_data.py | 4 ++-- rdrf/rdrf/urls.py | 4 ++-- rdrf/rdrf/views/patients_listing.py | 5 +++-- rdrf/registry/groups/models.py | 9 +++++++++ rdrf/report/report_builder.py | 8 +++----- rdrf/report/schema_cache.py | 25 +++++++++++++++++++++++++ 7 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 rdrf/report/schema_cache.py diff --git a/rdrf/rdrf/models/definition/models.py b/rdrf/rdrf/models/definition/models.py index 7cd1b5125..3fb61f0bb 100644 --- a/rdrf/rdrf/models/definition/models.py +++ b/rdrf/rdrf/models/definition/models.py @@ -1087,6 +1087,16 @@ def registry_form_definition_changed(sender, instance, **kwargs): clear_prefetched_form_data_cache(all_forms) +@receiver([post_save, post_delete], sender=Registry) +@receiver([post_save, post_delete], sender=RegistryForm) +@receiver([post_save, post_delete], sender=Section) +@receiver([post_save, post_delete], sender=CommonDataElement) +def schema_definition_changed(sender, instance, **kwargs): + from report.schema_cache import invalidate_schema_cache + + invalidate_schema_cache() + + class RegistryFormTranslation(models.Model): language = models.OneToOneField(Language, on_delete=models.CASCADE) translated_forms = models.ManyToManyField( @@ -2520,3 +2530,16 @@ class LongitudinalFollowup(models.Model): def __str__(self): return self.name + + +# Additional signal handlers for GraphQL schema cache invalidation. +# These are defined at the end of the file because they reference models +# that are defined after the schema_definition_changed handler above. +@receiver([post_save, post_delete], sender=ContextFormGroup) +@receiver([post_save, post_delete], sender=ContextFormGroupItem) +@receiver([post_save, post_delete], sender=ConsentSection) +@receiver([post_save, post_delete], sender=ConsentQuestion) +def schema_definition_changed_deferred(sender, instance, **kwargs): + from report.schema_cache import invalidate_schema_cache + + invalidate_schema_cache() diff --git a/rdrf/rdrf/patients/query_data.py b/rdrf/rdrf/patients/query_data.py index d8f41e025..f726ba165 100644 --- a/rdrf/rdrf/patients/query_data.py +++ b/rdrf/rdrf/patients/query_data.py @@ -2,7 +2,7 @@ from gql_query_builder import GqlQuery from graphql import GraphQLError -from report.schema import create_dynamic_schema +from report.schema_cache import get_cached_schema logger = logging.getLogger(__name__) @@ -96,7 +96,7 @@ def build_all_patients_query( def execute_query(request, query, variable_values=None): - schema = create_dynamic_schema() + schema = get_cached_schema() result = schema.execute( query, context_value=request, variable_values=variable_values ) diff --git a/rdrf/rdrf/urls.py b/rdrf/rdrf/urls.py index 68cb25b21..cfbf843ad 100644 --- a/rdrf/rdrf/urls.py +++ b/rdrf/rdrf/urls.py @@ -6,7 +6,7 @@ from django.urls import include, path, re_path from django.views.generic.base import TemplateView from django.views.i18n import JavaScriptCatalog -from report.schema import create_dynamic_schema +from report.schema_cache import get_cached_schema from report.TrrfGraphQLView import TrrfGraphQLView from two_factor import views as twv @@ -110,7 +110,7 @@ path( "graphql", lambda request: TrrfGraphQLView.as_view( - schema=create_dynamic_schema(), graphiql=True + schema=get_cached_schema(), graphiql=True )(request), ), ] diff --git a/rdrf/rdrf/views/patients_listing.py b/rdrf/rdrf/views/patients_listing.py index 7b97bc19b..a68685a1c 100644 --- a/rdrf/rdrf/views/patients_listing.py +++ b/rdrf/rdrf/views/patients_listing.py @@ -9,7 +9,8 @@ from django.utils.translation import gettext as _ from django.views.generic.base import View from registry.patients.models import Patient -from report.schema import create_dynamic_schema, to_camel_case +from report.schema import to_camel_case +from report.schema_cache import get_cached_schema from rdrf.db.contexts_api import RDRFContextManager from rdrf.forms.progress.form_progress import FormProgress @@ -246,7 +247,7 @@ def _query_all_patients( registry, ["total", patient_query], query_input, operation_input ) - schema = create_dynamic_schema() + schema = get_cached_schema() result_all = schema.execute( build_all_patients_query(registry, ["total"]), context_value=request diff --git a/rdrf/registry/groups/models.py b/rdrf/registry/groups/models.py index 6368de036..640cd1724 100644 --- a/rdrf/registry/groups/models.py +++ b/rdrf/registry/groups/models.py @@ -14,6 +14,7 @@ from django.core import validators from django.db import models from django.db.models import Q +from django.db.models.signals import post_delete, post_save from django.dispatch import receiver from django.utils import timezone from django.utils.module_loading import import_string @@ -587,3 +588,11 @@ class EmailChangeRequest(models.Model): ) history = HistoricalRecords() + + +@receiver([post_save, post_delete], sender=WorkingGroupType) +@receiver([post_save, post_delete], sender=WorkingGroup) +def working_group_changed(sender, instance, **kwargs): + from report.schema_cache import invalidate_schema_cache + + invalidate_schema_cache() diff --git a/rdrf/report/report_builder.py b/rdrf/report/report_builder.py index a0ac9ad6e..0ed4d98d3 100644 --- a/rdrf/report/report_builder.py +++ b/rdrf/report/report_builder.py @@ -20,10 +20,8 @@ from report.clinical_data_csv_util import ClinicalDataCsvUtil from report.models import ReportCdeHeadingFormat -from report.schema import ( - create_dynamic_schema, - get_schema_field_name, -) +from report.schema import get_schema_field_name +from report.schema_cache import get_cached_schema from report.utils import ( get_flattened_json_path, get_graphql_result_value, @@ -39,7 +37,7 @@ def __init__(self, report_design): self.report_config = load_report_configuration()["demographic_model"] self.report_fields_lookup = self.__init_report_fields_lookup() self.patient_filters = self.__init_patient_filters() - self.schema = create_dynamic_schema() + self.schema = get_cached_schema() def __init_report_fields_lookup(self): return { diff --git a/rdrf/report/schema_cache.py b/rdrf/report/schema_cache.py new file mode 100644 index 000000000..4119cd36d --- /dev/null +++ b/rdrf/report/schema_cache.py @@ -0,0 +1,25 @@ +import logging + +logger = logging.getLogger(__name__) + +_schema_cache = None + + +def get_cached_schema(): + global _schema_cache + + if _schema_cache is None: + from report.schema import create_dynamic_schema + + logger.info("Creating dynamic GraphQL schema (cache miss)") + _schema_cache = create_dynamic_schema() + + return _schema_cache + + +def invalidate_schema_cache(): + global _schema_cache + + if _schema_cache is not None: + logger.info("Invalidating GraphQL schema cache") + _schema_cache = None From 9a5ee0dda3c7bad912dca5b42ab412ba903eed84 Mon Sep 17 00:00:00 2001 From: ScriptSmith Date: Thu, 29 Jan 2026 14:49:33 +1000 Subject: [PATCH 2/2] Use string references for signal models --- rdrf/rdrf/models/definition/models.py | 25 ++++++++----------------- rdrf/registry/groups/models.py | 4 ++-- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/rdrf/rdrf/models/definition/models.py b/rdrf/rdrf/models/definition/models.py index 3fb61f0bb..43aee369a 100644 --- a/rdrf/rdrf/models/definition/models.py +++ b/rdrf/rdrf/models/definition/models.py @@ -1087,10 +1087,14 @@ def registry_form_definition_changed(sender, instance, **kwargs): clear_prefetched_form_data_cache(all_forms) -@receiver([post_save, post_delete], sender=Registry) -@receiver([post_save, post_delete], sender=RegistryForm) -@receiver([post_save, post_delete], sender=Section) -@receiver([post_save, post_delete], sender=CommonDataElement) +@receiver([post_save, post_delete], sender="rdrf.Registry") +@receiver([post_save, post_delete], sender="rdrf.RegistryForm") +@receiver([post_save, post_delete], sender="rdrf.Section") +@receiver([post_save, post_delete], sender="rdrf.CommonDataElement") +@receiver([post_save, post_delete], sender="rdrf.ContextFormGroup") +@receiver([post_save, post_delete], sender="rdrf.ContextFormGroupItem") +@receiver([post_save, post_delete], sender="rdrf.ConsentSection") +@receiver([post_save, post_delete], sender="rdrf.ConsentQuestion") def schema_definition_changed(sender, instance, **kwargs): from report.schema_cache import invalidate_schema_cache @@ -2530,16 +2534,3 @@ class LongitudinalFollowup(models.Model): def __str__(self): return self.name - - -# Additional signal handlers for GraphQL schema cache invalidation. -# These are defined at the end of the file because they reference models -# that are defined after the schema_definition_changed handler above. -@receiver([post_save, post_delete], sender=ContextFormGroup) -@receiver([post_save, post_delete], sender=ContextFormGroupItem) -@receiver([post_save, post_delete], sender=ConsentSection) -@receiver([post_save, post_delete], sender=ConsentQuestion) -def schema_definition_changed_deferred(sender, instance, **kwargs): - from report.schema_cache import invalidate_schema_cache - - invalidate_schema_cache() diff --git a/rdrf/registry/groups/models.py b/rdrf/registry/groups/models.py index 640cd1724..d814efdf9 100644 --- a/rdrf/registry/groups/models.py +++ b/rdrf/registry/groups/models.py @@ -590,8 +590,8 @@ class EmailChangeRequest(models.Model): history = HistoricalRecords() -@receiver([post_save, post_delete], sender=WorkingGroupType) -@receiver([post_save, post_delete], sender=WorkingGroup) +@receiver([post_save, post_delete], sender="groups.WorkingGroupType") +@receiver([post_save, post_delete], sender="groups.WorkingGroup") def working_group_changed(sender, instance, **kwargs): from report.schema_cache import invalidate_schema_cache