refactor: renaming to MappedValue for more clear intent, more documentation with better insights
This commit is contained in:
parent
fe1ce08c93
commit
ac1645d162
@ -77,8 +77,8 @@ Just like Django Expressions can be used to annotate keys in the model that are
|
|||||||
> The keys used in the kwargs to records() primarily represent the keys passed to the dataclass.
|
> 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.
|
- `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))`
|
- `MappedValue` allows to use a callable as argument, which gets called when setting the field on the model. e.g. `.records(data=MappedValue(lambda entry: 'x' in entry))`
|
||||||
- `MutValueNotNone` same as `MutValue` but only applies the callable if the database value is not None (shortcut).
|
- `MappedOptionalValue` same as `MappedValue` 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.
|
- `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.
|
- `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.
|
- `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.
|
||||||
|
@ -26,14 +26,28 @@ Once you are ready to isolate the code even further, all you will have to do, is
|
|||||||
|
|
||||||
## Where the semi-active record pattern of Django will influence this
|
## 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...)
|
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 "service" facade...)
|
||||||
|
|
||||||
> I am not advocating, that you should use django like this, but if you do, django-records was designed to help out.
|
> 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
|
## 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.
|
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.
|
Additionally, as `records()` itself produces an Iterator, any function like `first()` or `last()` are supported.
|
||||||
|
|
||||||
|
This really makes it easy to put in entities as an afterthought, and also keep the code close to a "djangoesque" solution.
|
||||||
|
|
||||||
|
## Some curiosities
|
||||||
|
|
||||||
|
As with most of the non active record pattern libraries, you will quickly see, that using entities often changes `save()`'s "insert" calls to `update()` calls. That is because you would have to fetch the model first, to call `save()` on it.
|
||||||
|
|
||||||
|
## Some downsides
|
||||||
|
|
||||||
|
The first one is, that django does not allow to simply add new variables to the queryset, as it would get lost by cloning, so you have to override `_clone()` itself, if you want to carry your own build data across the chain. For the user of this library it means, that you have to design your own manager class. That is actually not a real downside, as from experience, I would advocate you do that anyway.
|
||||||
|
|
||||||
|
A major disadvantage was in practice also, that understanding adjuncts requires everyone to learn a new tool, and it requires sometimes, especially in complex references between models, to carry data in a field during the queryset, that is not used to instantiate the final object, but to be reused by another adjunct or a postprocess call, so you also have to teach exactly what you are doing (which is easier learned, if everyone writes their own values-to-dataclass converter), to understand the query fully.
|
||||||
|
|
||||||
|
The inline nature of packing lots of lazy resolvers (effectively becoming side effects) into the query may however not scare functional programmers that much. Originally I designed it to use lambdas inline for small data manipulation. This might not fly well if you use tools like Sonar and fully follow their guidelines, which try to guide you more to explicitness, and may be the spirit of pythonicism, however personally, I rather have a concise queryset, and i have no issues reading a small lambda, and rather dislike if i have to scroll around to see a one-line function defined elsewhere.
|
||||||
|
|
||||||
|
I think, one should not shy away from writing explicit functions to create really complex transformations, adjuncts were supposed to solve small differences between model and value object in mind.
|
||||||
|
@ -6,7 +6,7 @@ from django.db.models import F
|
|||||||
from django.test.testcases import TestCase
|
from django.test.testcases import TestCase
|
||||||
from django.test.utils import tag
|
from django.test.utils import tag
|
||||||
|
|
||||||
from django_records.adjuncts import MutValue, FixedValue, PostProcess
|
from django_records.adjuncts import MappedValue, FixedValue, PostProcess
|
||||||
from django_records.handlers import RecordDictHandler
|
from django_records.handlers import RecordDictHandler
|
||||||
|
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ class TestQueryBuilder(TestCase):
|
|||||||
self.assertEqual(len(entities), len(self.planets))
|
self.assertEqual(len(entities), len(self.planets))
|
||||||
self.assertIsInstance(entities.first(), dict)
|
self.assertIsInstance(entities.first(), dict)
|
||||||
|
|
||||||
def test_MutValue(self):
|
def test_MappedValue(self):
|
||||||
|
|
||||||
# this tests whether our own celestial type or the celestial type of what we orbit is correct for being a moon. parameter is a dictionary.
|
# this tests whether our own celestial type or the celestial type of what we orbit is correct for being a moon. parameter is a dictionary.
|
||||||
is_moon = lambda entry: True if 5 > (entry.get('celestial_type') or 0) > 1 and 5 > (entry.get('orbits_type') or 0) > 1 else False
|
is_moon = lambda entry: True if 5 > (entry.get('celestial_type') or 0) > 1 and 5 > (entry.get('orbits_type') or 0) > 1 else False
|
||||||
@ -67,8 +67,8 @@ class TestQueryBuilder(TestCase):
|
|||||||
'celestial_type', # we include the key celestial_type into our query.
|
'celestial_type', # we include the key celestial_type into our query.
|
||||||
id=FixedValue(None), # we blank out id to test FixedValue working.
|
id=FixedValue(None), # we blank out id to test FixedValue working.
|
||||||
orbits_name=F('orbits__name'), # we set our custom orbits_name to a related field value
|
orbits_name=F('orbits__name'), # we set our custom orbits_name to a related field value
|
||||||
orbits_type=F('orbits__celestial_type'), # our MutValue needs this data.
|
orbits_type=F('orbits__celestial_type'), # our MappedValue needs this data.
|
||||||
is_moon=MutValue(is_moon)) # MutValue over result
|
is_moon=MappedValue(is_moon)) # MappedValue over result
|
||||||
|
|
||||||
self.assertEqual(len(entities), len(self.celestials))
|
self.assertEqual(len(entities), len(self.celestials))
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ class FixedValue(Adjunct):
|
|||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
class MutValue(Adjunct):
|
class MappedValue(Adjunct):
|
||||||
"""adjunct value that returns a field value with a callback.
|
"""adjunct value that returns a field value with a callback.
|
||||||
currently supports only 1 parameter (dbdata).
|
currently supports only 1 parameter (dbdata).
|
||||||
"""
|
"""
|
||||||
@ -63,8 +63,8 @@ class MutValue(Adjunct):
|
|||||||
if self.callback:
|
if self.callback:
|
||||||
return self.callback(dbdata)
|
return self.callback(dbdata)
|
||||||
|
|
||||||
class MutValueNotNone(MutValue):
|
class MappedOptionalValue(MappedValue):
|
||||||
"""MutValue that only calls the callback if the dbdata is not None
|
"""MappedValue that only calls the callback if the dbdata is not None
|
||||||
(convenience function)
|
(convenience function)
|
||||||
"""
|
"""
|
||||||
def resolve(self, model, dbdata):
|
def resolve(self, model, dbdata):
|
||||||
@ -85,7 +85,7 @@ class Ref(Adjunct):
|
|||||||
def __init__(self, key, adjunct: Adjunct | Callable | None = None):
|
def __init__(self, key, adjunct: Adjunct | Callable | None = None):
|
||||||
match adjunct:
|
match adjunct:
|
||||||
case Adjunct(adj): self.adjunct = adjunct
|
case Adjunct(adj): self.adjunct = adjunct
|
||||||
case callback if callable(callback): self.adjunct = MutValueNotNone(callback)
|
case callback if callable(callback): self.adjunct = MappedOptionalValue(callback)
|
||||||
case _: self.adjunct = None
|
case _: self.adjunct = None
|
||||||
self.key = key
|
self.key = key
|
||||||
|
|
||||||
|
@ -81,7 +81,9 @@ class RecordQuerySetMixin:
|
|||||||
# @deprecate: we might remove this
|
# @deprecate: we might remove this
|
||||||
logger.warning("Defining the target class in args might be soon deprecated: %s", handler)
|
logger.warning("Defining the target class in args might be soon deprecated: %s", handler)
|
||||||
else:
|
else:
|
||||||
handler = getattr(self, '_record', getattr(self, '_default_record', getattr(self.model, '_default_record', None)))
|
handler = getattr(self, '_record',
|
||||||
|
getattr(self, '_default_record',
|
||||||
|
getattr(self.model, '_default_record', None)))
|
||||||
if not handler:
|
if not handler:
|
||||||
raise RecordClassDefinitionError("Trying records() on a Queryset without destination class.")
|
raise RecordClassDefinitionError("Trying records() on a Queryset without destination class.")
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ from unittest import mock, TestCase
|
|||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
|
|
||||||
from . import handlers
|
from . import handlers
|
||||||
from .adjuncts import MutValue as Mut, FixedValue as Val, Skip, PostProcess, Ref
|
from .adjuncts import MappedValue as Mut, FixedValue as Val, Skip, PostProcess, Ref
|
||||||
from .records import RecordIterable, RecordQuerySetMixin
|
from .records import RecordIterable, RecordQuerySetMixin
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user