I have a model called "Phones" which has: screen size, RAM, etc. I have another one called "Laptops" which has: screen size, RAM, and Keyboard (QWERTZ, QWERTY, etc.). I could make a main model with basic fields like Name and Price. I want to just select a "Laptop" or a "Phone", without having unnecessary fields (e.g.: keyboard type for phones, or rear camera for laptops).
Should I make all fields and leave unneeded ones empty (would look silly to have "RAM" and "Keyboard type" and "Rear camera mpx" for a Mug)? Or should I make separate models for each? But then how could I combine query results (search for "Xiaomi" returning from the different models like phones, laptops, bikes, vacuum cleaners, etc.)?
CodePudding user response:
Have a look at abstract classes. What you are describing is explained in the official documentation: https://docs.djangoproject.com/en/4.1/topics/db/models/#abstract-base-classes
CodePudding user response:
I'm not sure what is bad practice, but I'll throw you some potential ideas on how you could do this:
#1 Abstract Model
class BaseProduct(models.Model):
name = models.CharField(max_length=200)
cost = models.DecimalField(max_digits=10, decimal_places=2, default=0)
class Meta:
abstract = True
# all models below will have a name cost attibute
# django might even throw them in the save table in the backend (not 100% sure)
class Phone(BaseProduct):
rear_camera_mpx = models.CharField(max_length=200)
# ..etc
class Laptop(BaseProduct):
ram = models.CharField(max_length=200)
# ..etc
###############################
# Example Query:
Laptop.objects.filter(name__icontains='MSI', ram='8gb')
# Filter Multiple Products
from itertools import chain
queryset_chain = chain(
Phone.objects.filter(name__icontains=query),
Laptop.objects.filter(name__icontains=query),
)
for i in queryset_chain
if type(i) == Laptop:
print(i.ram)
# elif
# .. etc
#2 Foreign Key Pointing back from Attributes
class BaseProduct(models.Model):
name = models.CharField(max_length=200)
cost = models.DecimalField(max_digits=10, decimal_places=2, default=0)
# could add a type
product_type = models.CharField(max_length=2, choices=PRODUCTTYPE_CHOICES, default='NA')
# Extra attributes, points back to base
class Phone(models.Model):
product = models.ForeignKey(BaseProduct, on_delete=models.PROTECT)
rear_camera_mpx = models.CharField(max_length=200)
# ..etc
class Laptop(models.Model):
product = models.ForeignKey(BaseProduct, on_delete=models.PROTECT)
ram = models.CharField(max_length=200)
# ..etc
###############################
# Example Query:
Laptop.objects.filter(product__name__icontains='MSI', ram='8gb')
# Search All Products
BaseProduct.objects.filter(name__icontains='MSI')
# then when you click on, use type to grab the correct full class based on "product_type"
if product_type == '01':
return Laptop.objects.filter(product__pk=clickedOnDetailPk).first()
#3 GenericForeign Key Pointing to Attributes
- Note: I find generic keys very clumsy and hard to use (that's just me tho)
class BaseProduct(models.Model):
name = models.CharField(max_length=200)
cost = models.DecimalField(max_digits=10, decimal_places=2, default=0)
# could add a type
product_type = models.CharField(max_length=2, choices=PRODUCTTYPE_CHOICES, default='NA')
# Below the mandatory fields for generic relation
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey()
# Extra attributes, points back to base
class Phone(models.Model):
rear_camera_mpx = models.CharField(max_length=200)
# ..etc
class Laptop(models.Model):
ram = models.CharField(max_length=200)
# ..etc
###############################
# Example Query:
# Search All Products
l = BaseProduct.objects.filter(name__icontains='MSI')
for i in l:
print(i.name, i.cost)
print('Generic Key Obj:', i.content_object)
print('Generic Key PK:', i.content_id)
print('Generic Key Type:', i.content_type) # is number / can change if re-creating db (not fun)
if i.product_type == '01': # Laptop Type / could also go by content_type with some extra logic
print('RAM:', i.content_object.ram)
# to do stuff like \/ you need extra stuff (never sat down to figure this out)
BaseProduct.objects.filter(content_object__ram='8gb')
#4 Json Field Cram it all into a Single Table
- Requires newer version of DBs Django
- This could be abstracted out with
proxy
models Managers pretty crazily. I've done it for a table for a similar use case, except imagine creating a laptop & including all the components that themselves are products :D. Not sure if it's bad practice, it's ALOT of custom stuff, but I've really liked my results.
class BaseProduct(models.Model):
name = models.CharField(max_length=200)
cost = models.DecimalField(max_digits=10, decimal_places=2, default=0)
# could add a type
product_type = models.CharField(max_length=2, choices=PRODUCTTYPE_CHOICES, default='NA')
# cram all the extra stuff as JSON
attr = models.JSONField(null=True)
###############################
# laptop search
l = BaseProduct.objects.filter(name__icontains='MSI', attr__ram='8gb')
for i in l:
print(i.name, i.cost, i.attr['ram'])
Overall
Overall I think #1 or #2 are the ways to go..
Unless you want to go wild and pretty much write everything, forms, admins, etc, then go #4