Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions performance_test/create_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@
ObjectRecordFactory as _ObjectRecordFactory,
ObjectTypeFactory,
)
from objects.token.tests.factories import TokenAuthFactory
from objects.token.constants import PermissionModes
from objects.token.tests.factories import PermissionFactory, TokenAuthFactory

object_type = ObjectTypeFactory.create(
service__api_root="http://localhost:8001/api/v2/",
uuid="f1220670-8ab7-44f1-a318-bd0782e97662",
)

token = TokenAuthFactory(token="secret", is_superuser=True)
token = TokenAuthFactory(token="secret", is_superuser=False)
PermissionFactory.create(
object_type=object_type,
mode=PermissionModes.read_only,
token_auth=token,
use_fields=False,
)


class ObjectRecordFactory(_ObjectRecordFactory):
Expand All @@ -31,13 +38,15 @@ def add_timestamp(obj, create, extracted, **kwargs):
ObjectRecordFactory.create_batch(
5000,
object__object_type=object_type,
_object_type=object_type,
start_at="2020-01-01",
version=1,
data={"identifier": "63f473de-a7a6-4000-9421-829e146499e3", "foo": "bar"},
add_timestamp=True,
)
ObjectRecordFactory.create(
object__object_type=object_type,
_object_type=object_type,
start_at="2020-01-01",
version=1,
data={"identifier": "ec5cde18-40a0-4135-8d97-3500d1730e60", "foo": "bar"},
Expand Down
22 changes: 22 additions & 0 deletions performance_test/test_objects_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,25 @@ def make_request():
assert result.json()["count"] == 1

benchmark_assertions(mean=1, max=1)


@pytest.mark.benchmark(max_time=60, min_rounds=5)
def test_objects_api_list_filter_by_object_type(benchmark, benchmark_assertions):
"""
Regression test for maykinmedia/objects-api#677
"""
params = {
"pageSize": 100,
"type": "http://localhost:8001/api/v2/objecttypes/f1220670-8ab7-44f1-a318-bd0782e97662",
"ordering": "-record__data__nested__timestamp",
}

def make_request():
return requests.get((BASE_URL / "objects").set(params), headers=AUTH_HEADERS)

result = benchmark(make_request)

assert result.status_code == 200
assert result.json()["count"] == 5001

benchmark_assertions(mean=1, max=1)
8 changes: 5 additions & 3 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ django-axes==6.5.1
# via open-api-framework
django-cors-headers==4.4.0
# via open-api-framework
django-csp==3.8
django-csp==4.0
# via open-api-framework
django-filter==24.2
# via
Expand Down Expand Up @@ -249,14 +249,16 @@ notifications-api-common==0.7.3
# via
# -r requirements/base.in
# commonground-api-common
open-api-framework==0.12.0
open-api-framework==0.13.0
# via -r requirements/base.in
orderedmultidict==1.0.1
# via furl
oyaml==1.0
# via commonground-api-common
packaging==25.0
# via kombu
# via
# django-csp
# kombu
phonenumberslite==8.13.30
# via django-two-factor-auth
prometheus-client==0.20.0
Expand Down
5 changes: 3 additions & 2 deletions requirements/ci.txt
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ django-cors-headers==4.4.0
# via
# -c requirements/base.txt
# -r requirements/base.txt
django-csp==3.8
django-csp==4.0
# via
# -c requirements/base.txt
# -r requirements/base.txt
Expand Down Expand Up @@ -460,7 +460,7 @@ notifications-api-common==0.7.3
# -c requirements/base.txt
# -r requirements/base.txt
# commonground-api-common
open-api-framework==0.12.0
open-api-framework==0.13.0
# via
# -c requirements/base.txt
# -r requirements/base.txt
Expand All @@ -478,6 +478,7 @@ packaging==25.0
# via
# -c requirements/base.txt
# -r requirements/base.txt
# django-csp
# kombu
# pytest
# sphinx
Expand Down
5 changes: 3 additions & 2 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ django-cors-headers==4.4.0
# via
# -c requirements/ci.txt
# -r requirements/ci.txt
django-csp==3.8
django-csp==4.0
# via
# -c requirements/ci.txt
# -r requirements/ci.txt
Expand Down Expand Up @@ -556,7 +556,7 @@ notifications-api-common==0.7.3
# -c requirements/ci.txt
# -r requirements/ci.txt
# commonground-api-common
open-api-framework==0.12.0
open-api-framework==0.13.0
# via
# -c requirements/ci.txt
# -r requirements/ci.txt
Expand All @@ -575,6 +575,7 @@ packaging==25.0
# -c requirements/ci.txt
# -r requirements/ci.txt
# build
# django-csp
# kombu
# pytest
# sphinx
Expand Down
2 changes: 1 addition & 1 deletion src/objects/api/kanalen.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def get_kenmerken(
data = data or {}
return {
kenmerk: (
data.get("type") or obj.object.object_type.url
data.get("type") or obj._object_type.url
if kenmerk == "object_type"
else data.get(kenmerk, getattr(obj, kenmerk))
)
Expand Down
8 changes: 5 additions & 3 deletions src/objects/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class ObjectSerializer(DynamicFieldsMixin, serializers.HyperlinkedModelSerialize
type = ObjectTypeField(
min_length=1,
max_length=1000,
source="object.object_type",
source="_object_type",
queryset=ObjectType.objects.all(),
help_text=_("Url reference to OBJECTTYPE in Objecttypes API"),
validators=[IsImmutableValidator()],
Expand All @@ -119,7 +119,9 @@ class Meta:

@transaction.atomic
def create(self, validated_data):
object_data = validated_data.pop("object")
object_data = validated_data.pop("object", {})
if object_type := validated_data.pop("_object_type"):
object_data["object_type"] = object_type
object = Object.objects.create(**object_data)

validated_data["object"] = object
Expand Down Expand Up @@ -156,7 +158,7 @@ def update(self, instance, validated_data):
logger.info(
"object_updated",
object_uuid=str(record.object.uuid),
objecttype_uuid=str(record.object.object_type.uuid),
objecttype_uuid=str(record._object_type.uuid),
objecttype_version=record.version,
token_identifier=token_auth.identifier,
token_application=token_auth.application,
Expand Down
2 changes: 1 addition & 1 deletion src/objects/api/v2/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def clean(self):

class ObjectRecordFilterSet(FilterSet):
type = ObjectTypeFilter(
field_name="object__object_type",
field_name="_object_type",
help_text=_("Url reference to OBJECTTYPE in Objecttypes API"),
queryset=ObjectType.objects.all(),
min_length=1,
Expand Down
19 changes: 11 additions & 8 deletions src/objects/api/v2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,16 @@
class ObjectViewSet(
ObjectNotificationMixin, SearchMixin, GeoMixin, viewsets.ModelViewSet
):
queryset = ObjectRecord.objects.select_related(
"object",
"object__object_type",
"object__object_type__service",
"correct",
"corrected",
).order_by("-pk")
queryset = (
ObjectRecord.objects.select_related(
"_object_type",
"_object_type__service",
"correct",
"corrected",
)
.prefetch_related("object")
.order_by("-pk")
)
serializer_class = ObjectSerializer
filterset_class = ObjectRecordFilterSet
filter_backends = [FilterBackend, OrderingBackend]
Expand All @@ -105,7 +108,7 @@ def get_queryset(self):
# prefetch permissions for DB optimization. Used in DynamicFieldsMixin
base = base.prefetch_related(
models.Prefetch(
"object__object_type__permissions",
"_object_type__permissions",
queryset=Permission.objects.filter(token_auth=token_auth),
to_attr="token_permissions",
),
Expand Down
12 changes: 5 additions & 7 deletions src/objects/api/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ def __call__(self, attrs, serializer):

# create
if not instance:
object_type = attrs.get("object", {}).get("object_type")
object_type = attrs.get("_object_type")
version = attrs.get("version")
data = attrs.get("data", {})

# update
else:
object_type = (
attrs.get("object", {}).get("object_type")
if "object" in attrs
else instance.object.object_type
attrs.get("_object_type")
if "_object_type" in attrs
else instance._object_type
)
version = attrs.get("version") if "version" in attrs else instance.version
data = attrs.get("data", {}) if "data" in attrs else instance.data
Expand Down Expand Up @@ -124,9 +124,7 @@ class GeometryValidator:

def __call__(self, attrs, serializer):
instance = getattr(serializer, "instance", None)
object_type = (
attrs.get("object", {}).get("object_type") or instance.object.object_type
)
object_type = attrs.get("_object_type") or instance._object_type
geometry = attrs.get("geometry")

if not geometry:
Expand Down
24 changes: 24 additions & 0 deletions src/objects/core/migrations/0032_objectrecord__object_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.2.3 on 2025-09-29 09:54

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("core", "0031_object_created_on_object_modified_on_and_more"),
]

operations = [
migrations.AddField(
model_name="objectrecord",
name="_object_type",
field=models.ForeignKey(
blank=True,
help_text="OBJECTTYPE in Objecttypes API",
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="core.objecttype",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Generated by Django 5.2.3 on 2025-09-29 08:19

import os

from django.db import connection, migrations

from structlog import get_logger

logger = get_logger(__name__)


BATCH_SIZE = int(os.getenv("OBJECTRECORD_BATCH_SIZE", 200_000))


def backfill_object_type_batch(apps, cursor, batch_size):
cursor.execute(
"""
WITH batch AS (
SELECT r.id
FROM core_objectrecord r
WHERE r._object_type_id IS NULL
LIMIT %s
)
UPDATE core_objectrecord r
SET _object_type_id = o.object_type_id
FROM core_object o, batch
WHERE r.id = batch.id
AND r.object_id = o.id;
""",
[batch_size],
)
return cursor.rowcount


def forward(apps, schema_editor):
with connection.cursor() as cursor:
while True:
num_updated = backfill_object_type_batch(apps, cursor, BATCH_SIZE)
if num_updated == 0:
break

logger.info("backfilled_object_type_for_records", num_records=num_updated)


class Migration(migrations.Migration):
dependencies = [
("core", "0032_objectrecord__object_type"),
]

operations = [migrations.RunPython(forward, migrations.RunPython.noop)]
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 5.2.6 on 2025-10-02 09:37

import django.db.models.deletion
from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations, models


class Migration(migrations.Migration):
atomic = False
dependencies = [
("core", "0033_objectrecord__backfill_denormalized_fields"),
]

operations = [
migrations.AlterField(
model_name="objectrecord",
name="_object_type",
field=models.ForeignKey(
help_text="OBJECTTYPE in Objecttypes API",
on_delete=django.db.models.deletion.PROTECT,
to="core.objecttype",
),
),
AddIndexConcurrently(
model_name="objectrecord",
index=models.Index(
fields=["_object_type_id", "-index"], name="idx_objectrecord_type_index"
),
),
AddIndexConcurrently(
model_name="objectrecord",
index=models.Index(
fields=["_object_type_id", "id"], name="idx_objectrecord_type_id"
),
),
AddIndexConcurrently(
model_name="objectrecord",
index=models.Index(
fields=["_object_type_id", "start_at", "end_at", "object", "-index"],
name="idx_type_start_end_object_idx",
),
),
]
Loading