Hi, everyone.
I’m trying to find out the right way to implement “multi-type” field to store different data types as values. I guess the closest example is store’s Product
.
class Product(models.Model):
product_type = ForeignKey(ProductType)
title = CharField()
specs = models.ManyToManyField(
ProductSpec,
through="ItemSpecValue",
blank=True
)
price = PositiveIntegerField()
.... some other fields ...
Each of Product
types might have different specs like color, size, weight etc.
Most of guides implements a separate model ProductSpec
and Product
through model ProductSpecValue
which has additional value
field to maintain flexibility of products parameters.
class ProductSpec(models.Model):
name = CharField()
product_type = ForeignKey(ProductType)
class ProductSpecValue(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
spec = models.ForeignKey(ProductSpec, on_delete=models.CASCADE)
value = # field type?
So the question is: what field type to be set for value
field and what is the common approach for such design ? Is there any better one?
Appreciate any help.
Personally I would use a JSONField
here and implement a ProductSpecValue.clean
method to validate the type of value
.
You could also add constraints to ensure your data is of the right type depending on the spec and you’ll get validation for free
class ProductSpec(models.Model):
name = CharField(unique=True)
product_type = ForeignKey(ProductType)
class JSONTypeOf(models.Func):
function = "jsonb_typeof"
output_field = models.TextField()
class ProductSpecValue(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
spec = models.ForeignKey(
ProductSpec, on_delete=models.CASCADE, to_field="name"
)
value = JSONField()
class Meta:
constraints = [
models.CheckConstraint(
condition=Case(
When(
spec="color", then=Exact(
JSONTypeOf("value"), Value("string")
),
),
When(
spec__in=("size", "weigth"), then=Exact(
JSONTypeOf("value"), Value("number")
),
),
)
)
]
Thank you for reply.
That looks reasoning.
And what about JSON field approach to avoid conditional checks with custom Func
? :
class ProductType(models.Model):
name = models.CharField()
spec_schema = JSONField(
default=dict
)
class Product(models.Model):
product_type = ForeignKey(ProductType)
title = CharField()
specs = JSONField(
default=dict,
blank=True,
)
price = PositiveIntegerField()
def clean(self):
schema = self.product_type.spec_schema or {}
try:
jsonschema.validate(instance=self.spec, schema=schema)
except jsonschema.ValidationError as exc:
raise ValidationError({"specs": f"Schema error: {exc.message}"})
Using a JSON schema will work fine at the Python level but be aware that model validation is not run when using the ORM directly so it you perform any form of backfill (e.g. usage of bulk_create
) or call save
or create
directly your clean
method won’t be called.
Looks like there exists solutions on Postgres if you want to enforce JSON schema at the database level as well but they’ll likely require a trigger in your case as CHECK
constraints cannot refer other tables fields and in your case the schema lives in a different table.