From 07f13d4ece7aca218975aaf1dadb24a818e226e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabor=20K=C3=B6rber?= Date: Thu, 23 Jan 2025 16:31:32 +0100 Subject: [PATCH] docs: some more documentation --- README.md | 16 ++++++++++++++-- docs/clean_architecture.md | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 docs/clean_architecture.md diff --git a/README.md b/README.md index 0167e62..a205e26 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ An example is to create dataclass instances as entities for a clean architecture Main target is support of dataclass and pydantic, and allow building readonly representations of the data. +A more in-depth explanation, why you would use this module is described [in the documentation](./docs/clean_architecture.md) + ## 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. @@ -68,11 +70,21 @@ Just like Django Expressions can be used to annotate keys in the model that are - `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 +## Testing & Developing + +### Install prerequisites + +If you have `just` installed, you can use + +`just install` + +to install uv, python, and build a virtual env. + +> Note: Windows users should install uv manually first. ### Built-In Tests -... +`just test` should run the unit tests. ### Integration Test: Examples Project diff --git a/docs/clean_architecture.md b/docs/clean_architecture.md new file mode 100644 index 0000000..77ab962 --- /dev/null +++ b/docs/clean_architecture.md @@ -0,0 +1,39 @@ +# Clean Architecture + +If you look at the records() function, and ask yourself, why one would even define this abstraction, I want to explain in this document a bit the background to it. + +In some architectural patterns, you work with so called "Entities", which are supposed to hold simple and plain objects (value objects), without any attached functionality, often represented as structs, or "DTO"s so data-transfer objects, which are also supposed to be pure. + +The idea is, to reduce side effects, by always handling data in the layer independently, and also make the code portable, so that you could exchange the database engine behind it. + +> As many well versed architectural experts note at this point in time, is, that in reality, this is a very abstract thought, as exchanging your ORM Framework for example is even more unlikely, than exchanging your database engine once you are in production, which is I think the main category of thought Django programmers usually follow - especially since changing Database with django becomes a configuration question. But let's put this aside. + +Let's assume, achieving such layers is your goal, and you agree that the business layer of your application becomes "django independent", and you only want to see plain python objects there. + +In such cases, you would most likely create repository facades, because in Django, the Repository is carried around on the model instance as the `objects` keyword by default, and is called a `Manager` (and managers cannot be used independently) + +In these facades, you would do your querysets, but in the end produce custom objects through factories, but since the model instance itself should not leak, you would transform model data into some readonly structure. + +But the overhead of creating a model instance in this case would become a hassle, so you would probably opt to retrieve instead dictionaries with `values()`, and feed those into dataclasses or pydantic objects instead. + +This is where `records()` tries to chime in. It will take over the values() call, and produce your plain objects directly, and you can even modify it's data sources, so that it can even instantiate it as purely readonly object. + +## But why? + +Especially in refactoring, but also in creating new code, what you can do now, is simply using your models, as long as they are very similar to your dataclasses, and move your querysets into a facade, maybe even as a separate refactoring step, and for now, use standard querysets and return django models. + +Once you are ready to isolate the code even further, all you will have to do, is add `records()` into your queryset calls in the facade. + +## Where the semi-active record pattern of Django will influence this + +Your new repository facade probably has to give you a save(), create(), update(), delete() and refresh_from_db() alternative for your entities (well refresh might be just getting the object again, and the mutators might even go DDD style into a separate facade...) + +> I am not advocating, that you should use django like this, but if you do, django-records was designed to help out. + +As with most of the non active record pattern libraries, you will quickly see, that using entities often changes save() calls to update() calls. + +## Some benefits + +For one, as long as data transfer objects have similar fields, transforming them becomes easy. you can also very simply reduce calls to only use the fields of your dataclass. + +Additionally, as `records()` itself produces an Iterator, any function like `first()` or `last()` are supported.