Using Python Dataclass in Django Models

11.5k views Asked by At

My goal is to create a struct-like object in Django PostgreSQL, such as:

specs.coordinate.x
specs.coordinate.y
specs.coordinate.z

x,y,z should therefore be subclasses of coordinate. They should be me mutable as well, wherefore named tuples cannot be used.

I have tried it using the new dataclass:

from django.db import models
from dataclasses import dataclass

class Specs(models.Model):
    name = models.CharField(max_length=80)
    age = models.IntegerField()

    @dataclass
    class coordinate:
        x: float
        y: float
        z: float

However the coordinate x,y,z items are not visible in pgAdmin:

enter image description here

What am I doing wrong? Is there any better approach for this than with dataclass?

3

There are 3 answers

1
Kound On BEST ANSWER

If your goal is simply to use the syntax: specs.coordinate.x in your datasheet you can simply use a OneToOne Relation.

This also allows for the special case, that either all Coordinates values have to be set or none.

from django.db import models

class Coordinate(models.Model):
    x = models.FloatField()
    y = models.FloatField()
    z = models.FloatField()


class Specs(models.Model):
    name = models.CharField(max_length=80)
    age = models.IntegerField()
    coordinate = models.OneToOneField(
        Coordinate, 
        null=True, 
        on_delete=models.SET_NULL
    )

Now you can use Specs just like the a Dataclass.

1
ed_ On

I would use django's built-in PointField. It stores Point objects.

so you would use

from django.contrib.gis.db import models


class Specs(models.Model):
    coordinate = models.PointField

    

In django you could access it as you would with a normal django model field:

spec.coordinate

And since is a Point object, you could also do:

spec.coordinate.x
spec.coordinate.y
spec.coordinate.z
0
Viljami On

Define DataClassField

This solution uses dacite library to achieve support to nested dataclasses.

from dacite import from_dict
from django.db import models
from dataclasses import dataclass, asdict
import json

"""Field that maps dataclass to django model fields."""
class DataClassField(models.CharField):
    description = "Map python dataclasses to model."

    def __init__(self, dataClass, *args, **kwargs):
        self.dataClass = dataClass
        if not kwargs['max_length'] 
            kwargs['max_length'] = 1024
        super().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        kwargs['dataClass'] = self.dataClass
        return name, path, args, kwargs

    def from_db_value(self, value, expression, connection):
        if value is None:
            return value
        obj = json.loads(value)
        return from_dict(data_class=self.dataClass, data=obj)

    def to_python(self, value):
        if isinstance(value, self.dataClass):
            return value
        if value is None:
            return value
        obj = json.loads(value)
        return from_dict(data_class=self.dataClass, data=obj)

    def get_prep_value(self, value):
        if value is None:
            return value
        return json.dumps(asdict(value)

Example usage

@dataclass
class MyDataclass1:
    foo: str

@dataclass
class MyDataclass2:
    bar: MyDataclass1

class MyModel(models.Model)
    dc1 = DataClassField(dataClass=MyDataclass1, null=True)
    dc2 = DataClassField(dataClass=MyDataclass2, null=True)

instance = MyModel(dc1=MyDataclass1(foo="foo"))
instance.save()
instance.dc1.foo # Should return "foo"
instance.dc1 # Should return <MyDataclass1 .... at ....>

Update

To add support for custom datatypes (for example datetimes) you need to specify JSON decoding and encoding for these types as represented below:

def decode_datetime(dt: datetime) -> str:
    return dt.strftime('%Y-%m-%dT%H:%M:%S%z')

def encode_datetime(dt: str) -> datetime:
    return datetime.strptime(dt, '%Y-%m-%dT%H:%M:%S%z')
    
def default(o):
    if isinstance(o, (datetime.datetime)):
        return decode_datetime(o)
    else:
        print(o)

class DataclassField(models.CharField):
    ...
    def from_db_value(self, value, expression, connection):
        ...
        obj = json.loads(value)
        return from_dict(data_class=self.data_class, data=obj,
                         config=Config(type_hooks={datetime.datetime: encode_datetime}))

    def to_python(self, value):
        ...
        return from_dict(data_class=self.data_class, data=obj,
                         config=Config(type_hooks={datetime.datetime: encode_datetime}))

    def get_prep_value(self, value):
        ...
        return json.dumps(asdict(value), sort_keys=True, indent=1, default=default)