Skip to content

nautobot.apps.querysets

Nautobot QuerySet classes and utilities.

nautobot.apps.querysets.ConfigContextModelQuerySet

Bases: RestrictedQuerySet

QuerySet manager used by models which support ConfigContext (device and virtual machine).

Includes a method which appends an annotation of aggregated config context JSON data objects. This is implemented as a subquery which performs all the joins necessary to filter relevant config context objects. This offers a substantial performance gain over ConfigContextQuerySet.get_for_object() when dealing with multiple objects.

This allows the annotation to be entirely optional.

Source code in nautobot/extras/querysets.py
class ConfigContextModelQuerySet(RestrictedQuerySet):
    """
    QuerySet manager used by models which support ConfigContext (device and virtual machine).

    Includes a method which appends an annotation of aggregated config context JSON data objects. This is
    implemented as a subquery which performs all the joins necessary to filter relevant config context objects.
    This offers a substantial performance gain over ConfigContextQuerySet.get_for_object() when dealing with
    multiple objects.

    This allows the annotation to be entirely optional.
    """

    def annotate_config_context_data(self):
        """
        Attach the subquery annotation to the base queryset.

        Order By clause in Subquery is not guaranteed to be respected within the aggregated JSON array, which is why
        we include "weight" and "name" into the result so that we can sort it within Python to ensure correctness.

        TODO This method does not accurately reflect location inheritance because of the reasons stated in _get_config_context_filters()
        Do not use this method by itself, use get_config_context() method directly on ConfigContextModel instead.
        """
        from nautobot.extras.models import ConfigContext

        return self.annotate(
            config_context_data=Subquery(
                ConfigContext.objects.filter(self._get_config_context_filters())
                .order_by("weight", "name")
                .annotate(
                    _data=EmptyGroupByJSONBAgg(
                        JSONObject(
                            data=F("data"),
                            name=F("name"),
                            weight=F("weight"),
                        )
                    )
                )
                .values("_data")
            )
        ).distinct()

    def _get_config_context_filters(self):
        """
        This method is constructing the set of Q objects for the specific object types.
        Note that locations filters are not included in the method because the filter needs the
        ability to query the ancestors for a particular tree node for subquery and we lost it since
        moving from mptt to django-tree-queries https://github.com/matthiask/django-tree-queries/issues/54.
        """
        tag_query_filters = {
            "object_id": OuterRef(OuterRef("pk")),
            "content_type__app_label": self.model._meta.app_label,
            "content_type__model": self.model._meta.model_name,
        }
        base_query = Q(
            Q(platforms=OuterRef("platform")) | Q(platforms=None),
            Q(cluster_groups=OuterRef("cluster__cluster_group")) | Q(cluster_groups=None),
            Q(clusters=OuterRef("cluster")) | Q(clusters=None),
            Q(tenant_groups=OuterRef("tenant__tenant_group")) | Q(tenant_groups=None),
            Q(tenants=OuterRef("tenant")) | Q(tenants=None),
            Q(tags__pk__in=Subquery(TaggedItem.objects.filter(**tag_query_filters).values_list("tag_id", flat=True)))
            | Q(tags=None),
            is_active=True,
        )
        base_query.add((Q(roles=OuterRef("role")) | Q(roles=None)), Q.AND)
        if self.model._meta.model_name == "device":
            base_query.add((Q(device_types=OuterRef("device_type")) | Q(device_types=None)), Q.AND)
            base_query.add(
                (Q(device_redundancy_groups=OuterRef("device_redundancy_group")) | Q(device_redundancy_groups=None)),
                Q.AND,
            )
            # This is necessary to prevent location related config context to be applied now.
            # The location hierarchy cannot be processed by the database and must be added by `ConfigContextModel.get_config_context`
            base_query.add((Q(locations=None)), Q.AND)
        elif self.model._meta.model_name == "virtualmachine":
            # This is necessary to prevent location related config context to be applied now.
            # The location hierarchy cannot be processed by the database and must be added by `ConfigContextModel.get_config_context`
            base_query.add((Q(locations=None)), Q.AND)

        return base_query

annotate_config_context_data()

Attach the subquery annotation to the base queryset.

Order By clause in Subquery is not guaranteed to be respected within the aggregated JSON array, which is why we include "weight" and "name" into the result so that we can sort it within Python to ensure correctness.

TODO This method does not accurately reflect location inheritance because of the reasons stated in _get_config_context_filters() Do not use this method by itself, use get_config_context() method directly on ConfigContextModel instead.

Source code in nautobot/extras/querysets.py
def annotate_config_context_data(self):
    """
    Attach the subquery annotation to the base queryset.

    Order By clause in Subquery is not guaranteed to be respected within the aggregated JSON array, which is why
    we include "weight" and "name" into the result so that we can sort it within Python to ensure correctness.

    TODO This method does not accurately reflect location inheritance because of the reasons stated in _get_config_context_filters()
    Do not use this method by itself, use get_config_context() method directly on ConfigContextModel instead.
    """
    from nautobot.extras.models import ConfigContext

    return self.annotate(
        config_context_data=Subquery(
            ConfigContext.objects.filter(self._get_config_context_filters())
            .order_by("weight", "name")
            .annotate(
                _data=EmptyGroupByJSONBAgg(
                    JSONObject(
                        data=F("data"),
                        name=F("name"),
                        weight=F("weight"),
                    )
                )
            )
            .values("_data")
        )
    ).distinct()