Django Records
Go to file
2025-01-23 16:01:15 +01:00
examples/celestials feat: examples and tests 2025-01-23 16:01:03 +01:00
src/django_records feat: initial import from blackmesa project 2025-01-23 16:00:51 +01:00
.gitignore feat: initial import from blackmesa project 2025-01-23 16:00:51 +01:00
justfile feat: justfile 2025-01-23 16:01:15 +01:00
LICENSE Initial commit 2025-01-20 21:34:14 +01:00
pyproject.toml feat: initial import from blackmesa project 2025-01-23 16:00:51 +01:00
README.md feat: initial import from blackmesa project 2025-01-23 16:00:51 +01:00

Django Records

Django Records aims to provide a queryset extension facility, that allows to directly create structured data from querysets, other than Model Instances.

An example is to create dataclass instances as entities for a clean architecture setup, where model instances should not persist in the business layer and are just readonly representations of the fetched data, especially if you plan e.g. to use django-agnostic repository facades.

Main target is support of dataclass and pydantic, and allow building readonly representations of the data.

The .records() queryset function

Changed since 0.3: records is not supposed take an optional first parameter as record target, instead use record_into(), however support for it is kept.

records() fetches data from the database with a values() call, and uses that to directly create a dataclass, or any other such structure ("a record"). It completely skips instantiation of a django model instance, and comes with tools to make it easy to handle initialization data, so that you can automate even production of immutable or nested data structures (called Adjunct), that work similar to tools like Q() or F().

Out of the box, records assumes, you want to use dataclasses.dataclass or a pydantic.BaseModel.

Usage:

    SomeModel.objects.filter(...).records('field1', 'field2', annotation=F(...), adjunct=Adjunct(...))

Defining target default structure or a custom one with .record_into()

Unstable: record_into() might be renamed.

  • You can define the target class you want to create directly on the Queryset Manager (or Queryset)

    @dataclass
    class Entity:
        id: int
    
    MyManager = BaseManager.from_queryset(RecordQuerySet)
    MyManager._default_record = Entity
    
    ...
    
  • You can add it to your model (I have not found a standard way to expand Meta yet)

class MyModel(models.Model):
    _default_record = Entity
  • You could also add the Handler directly as _default_record, otherwise if you want to use another Handler than the default dataclasses handler, you can define _record_handler on the Queryset

  • Finally you can override this behaviour by explicitly chaining the target class into the queryset with record_into(), which takes either a RecordHandler object, or any type of class that can be wrapped by one.

    SomeModel.objects.filter(...).record_into(TargetDataClass).records('field1', 'field2')

Adjuncts

Just like Django Expressions can be used to annotate keys in the model that are retrieved, so can Adjuncts be used to circumvent this mechanic, and insert local data into your target class. You might want to use this, if e.g. the dataclass you create is immutable, or the dataclass you use has required fields, that need data when you create the class, but the data is not part of your database query.

Adjuncts do not influence the underlying SQL, except being able to add keys to the values() call.

The keys used in the kwargs to records() primarily represent the keys passed to the dataclass.

  • FixedValue simply carries some data and inserts it into every model. e.g. .records(data=FixedValue(1)) will set the field data always to 1.
  • MutValue allows to use a callable as argument, which gets called when setting the field on the model. e.g. .records(data=MutValue(lambda entry: 'x' in entry))
  • MutValueNotNone same as MutValue but only applies the callable if the database value is not None (shortcut).
  • Ref uses a different key to retrieve the data from values, and may apply an Adjunct to it. This probably is the most used Adjunct in real life examples.
  • Skip allows you to skip a field. This is needed, as records() would include all fields on a dataclass, without knowing if it is optional, and helpful if you rewrite the fields with a PostProcess.
  • PostProcess allows you to call a function as a callback at creation - if the callback returns anything else than None, it is used as initializer for the production of the object.

Testing

Built-In Tests

...

Integration Test: Examples Project

The celestial project in examples serves to demonstrate basic usage of records, as well as providing integration testing.