Is this database design good?

Hello,

I am somewhat new to Django, I am creating a website for a lighting company just to showcase their products.
The main part of the website is the products tab where the user can look at “Categories” and choose one of its “Subcategories”, then he will be redirected to a page with all the "Products"under that “subcategory” there the user can use a filter on the left side to only show products that match specific “Attribute” values.

I had to design my database structure 3 times because it didn’t fit the website

Database schema 1 (Login required)

Database schema 2 (Login required)

Then I redesigned it because I wasn’t able to present it in Django admin using nested inlines.
Database schema 3 (Current):

Models.py:

from django.core.validators import FileExtensionValidator
from django.db import models

# Create your models here.
class String(models.Model):
    value = models.CharField(max_length=50, unique=True, blank=False, null=False)

    def __str__(self):
        return f"{self.value}"

class Integer(models.Model):
    value = models.IntegerField(unique=True, blank=False, null=False)

    def __str__(self):
        return f"{self.value}"

class Decimal(models.Model):
    value = models.DecimalField(unique=True, blank=False, null=False, max_digits=10, decimal_places=2)

    def __str__(self):
        return f"{self.value}"

class RangedDecimal(models.Model):
    min_value = models.DecimalField(max_digits=10, decimal_places=2, null=False)
    max_value = models.DecimalField(max_digits=10, decimal_places=2, null=False)

    def values_range(self):
        values_range = (self.min_value, self.max_value)
        return values_range

    class Meta:
        unique_together = ('min_value', 'max_value')

    def __str__(self):
        return f"{self.min_value}-{self.max_value}"


class RangedInteger(models.Model):
    min_value = models.IntegerField(blank=False, null=False)
    max_value = models.IntegerField(blank=False, null=False)

    def values_range(self):
        values_range = (self.min_value, self.max_value)
        return values_range

    class Meta:
        unique_together = ('min_value', 'max_value')

    def __str__(self):
        return f"{self.min_value} - {self.max_value}"

class Category(models.Model):
    name = models.CharField(max_length=50, unique=True, blank=False, null=False)

    def __str__(self):
        return f"{self.name}"

class Attribute(models.Model):
    name = models.CharField(max_length=50, unique=True, blank=False, null=False)

    def __str__(self):
        return f"{self.name}"

class Subcategory(models.Model):
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    name = models.CharField(max_length=50, unique=True, blank=False, null=False)
    attribute = models.ManyToManyField(
        Attribute,
        through='SubcategoryAttribute',
        related_name='subcategory'
    )

    class Meta:
        unique_together = ('category', 'name')


    def __str__(self):
        return f"{self.name}"

class Media(models.Model):
    brand = models.OneToOneField(
        'Brand',
        on_delete=models.SET_NULL,
        null=True,  
        blank=True,  
        related_name='media'  
    )
    category = models.OneToOneField(
        'Category',
        on_delete=models.SET_NULL,
        null=True,  
        blank=True,  
        related_name='media'  
    )
    subcategory = models.OneToOneField(
        'Subcategory',
        on_delete=models.SET_NULL,
        null=True,  
        blank=True,  
        related_name='media'  
    )
    image = models.ImageField(
        upload_to='products/',
        blank=True,
        null=True,
        default='products/default.png'
    )
    pdf = models.FileField(
        upload_to='pdfs/',
        blank=True,
        null=True,
        validators=[FileExtensionValidator(['pdf'])],
        help_text='Upload product specification PDF'
    )
    name = models.CharField(max_length=50)

    class Meta:
        unique_together = [
            ('image', 'pdf', 'name'),
            ('image', 'pdf'),
        ]

    def __str__(self):
        return f"{self.name}"

class Brand(models.Model):
    name = models.CharField(max_length=50, unique=True, blank=False, null=False)

    def __str__(self):
        return f"{self.name}"

class SubcategoryAttribute(models.Model):
    attribute = models.ForeignKey(Attribute, on_delete=models.CASCADE)
    subcategory = models.ForeignKey(Subcategory, on_delete=models.CASCADE)

    class Meta:
        unique_together = ('attribute', 'subcategory')

    def __str__(self):
        return f"{self.subcategory} - {self.attribute}"

class Product(models.Model):
    brand = models.ForeignKey(Brand, on_delete=models.CASCADE)
    subcategory = models.ForeignKey(Subcategory, on_delete=models.CASCADE)
    name = models.CharField(max_length=50, unique=True, blank=False, null=False)

    class Meta:
        unique_together = [
            ('brand', 'subcategory', 'name'),
            ('brand', 'subcategory'),
        ]

    def __str__(self):
        return f"{self.brand} - {self.subcategory} - {self.name}"

class Model(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    model_letter = models.CharField(max_length=10, blank=True, null=False)
    model_number = models.IntegerField(blank=False, null=False)
    media = models.ManyToManyField(Media, through="ModelMedia")

    string_values = models.ManyToManyField(String, through="SubcategoryAttributeModelString")
    integer_values = models.ManyToManyField(Integer, through="SubcategoryAttributeModelInteger")
    decimal_values = models.ManyToManyField(Decimal, through="SubcategoryAttributeModelDecimal")
    ranged_integer_value = models.ManyToManyField(RangedInteger, through="SubcategoryAttributeModelRangedInteger")
    ranged_decimal_value = models.ManyToManyField(RangedDecimal, through="SubcategoryAttributeModelRangedDecimal")

    def value(self):
        values = 0
        attribute_value = None
        value_attributes = [self.string_values, self.integer_values, self.decimal_values, self.ranged_integer_value,
                            self.ranged_decimal_value]
        for value in value_attributes:
            if value is not None:
                values += values
                attribute_value = value
        if values != 1:
            raise ValueError("A subcategory attribute cannot have less than or more than 1 value")
        else:
            return attribute_value

    value = value

    unique_together = [
        ('model_number', 'model_letter', 'product', 'value'),
        ('model_number', 'model_letter', 'product'),
        ('model_number', 'model_letter'),
    ]

    def __str__(self):
        return f"{self.product} - {self.model_letter}{self.model_number}"  # i removed  - {self.value} here becaue i got eror, find solution

class ModelMedia(models.Model):
    model = models.ForeignKey(Model, on_delete=models.CASCADE)
    media = models.ForeignKey(Media, on_delete=models.CASCADE)

    class Meta:
        unique_together = ('media', 'model')

class SubcategoryAttributeModelString(models.Model):
    subcategory_attribute = models.ForeignKey(SubcategoryAttribute, on_delete=models.CASCADE)
    model = models.ForeignKey(Model, on_delete=models.CASCADE)
    string = models.ForeignKey(String, on_delete=models.CASCADE)

    class Meta:
        unique_together = [
            ('subcategory_attribute','string'),
            ('subcategory_attribute', 'string', 'model')
        ]

    def __str__(self):
        return f"{self.subcategory_attribute} - {self.model} - {self.string}"

class SubcategoryAttributeModelInteger(models.Model):
    subcategory_attribute = models.ForeignKey(SubcategoryAttribute, on_delete=models.CASCADE)
    model = models.ForeignKey(Model, on_delete=models.CASCADE)
    integer = models.ForeignKey(Integer, on_delete=models.CASCADE)

    class Meta:
        unique_together = [
            ('subcategory_attribute','integer'),
            ('subcategory_attribute', 'integer', 'model')
        ]

    def __str__(self):
        return f"{self.subcategory_attribute} - {self.model} - {self.integer}"

class SubcategoryAttributeModelDecimal(models.Model):
    subcategory_attribute = models.ForeignKey(SubcategoryAttribute, on_delete=models.CASCADE)
    model = models.ForeignKey(Model, on_delete=models.CASCADE)
    decimal = models.ForeignKey(Decimal, on_delete=models.CASCADE)

    class Meta:
        unique_together = [
            ('subcategory_attribute','decimal'),
            ('subcategory_attribute', 'decimal', 'model')
        ]

    def __str__(self):
        return f"{self.subcategory_attribute} - {self.model} - {self.decimal}"

class SubcategoryAttributeModelRangedInteger(models.Model):
    subcategory_attribute = models.ForeignKey(SubcategoryAttribute, on_delete=models.CASCADE)
    model = models.ForeignKey(Model, on_delete=models.CASCADE)
    ranged_integer = models.ForeignKey(RangedInteger, on_delete=models.CASCADE)

    class Meta:
        unique_together = [
            ('subcategory_attribute','ranged_integer'),
            ('subcategory_attribute', 'ranged_integer', 'model')
        ]

    def __str__(self):
        return f"{self.subcategory_attribute} - {self.model} - {self.ranged_integer}"

class SubcategoryAttributeModelRangedDecimal(models.Model):
    subcategory_attribute = models.ForeignKey(SubcategoryAttribute, on_delete=models.CASCADE)
    model = models.ForeignKey(Model, on_delete=models.CASCADE)
    ranged_decimal = models.ForeignKey(RangedDecimal, on_delete=models.CASCADE)

    class Meta:
        unique_together = [
            ('subcategory_attribute','ranged_decimal'),
            ('subcategory_attribute', 'ranged_decimal', 'model')
        ]

    def __str__(self):
        return f"{self.subcategory_attribute} - {self.model} - {self.ranged_decimal}"

Admin.py:

import nested_admin
from django.contrib import admin
from .models import *

class MediaInline(admin.TabularInline):
    model = Media
    can_delete = False
    max_num = 1         #(OneToOne)
    fields = ('image', 'name')

class ModelMediaInline(nested_admin.NestedTabularInline):
    model = ModelMedia
    extra = 1

class SubcategoryAttributeModelStringInline(nested_admin.NestedStackedInline):
    model = SubcategoryAttributeModelString
    extra = 1

class SubcategoryAttributeModelIntegerInline(nested_admin.NestedStackedInline):
    model = SubcategoryAttributeModelInteger
    extra = 1

class SubcategoryAttributeModelDecimalInline(nested_admin.NestedStackedInline):
    model = SubcategoryAttributeModelDecimal
    extra = 1

class SubcategoryAttributeModelRangedIntegerInline(nested_admin.NestedStackedInline):
    model = SubcategoryAttributeModelRangedInteger
    extra = 1

class SubcategoryAttributeModelRangedDecimalInline(nested_admin.NestedStackedInline):
    model = SubcategoryAttributeModelRangedDecimal
    extra = 1

class AttributeInline(admin.TabularInline):
    model = SubcategoryAttribute

class ModelInline(nested_admin.NestedStackedInline):
    model = Model
    inlines = [
        ModelMediaInline,
        SubcategoryAttributeModelStringInline,
        SubcategoryAttributeModelIntegerInline,
        SubcategoryAttributeModelDecimalInline,
        SubcategoryAttributeModelRangedIntegerInline,
        SubcategoryAttributeModelRangedDecimalInline,
    ]
    extra = 1

@admin.register(Brand)
class BrandAdmin(admin.ModelAdmin):
    inlines = [MediaInline]  # Add the inline
    list_display = ('name',)

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    inlines = [MediaInline]  # Add the inline
    list_display = ('name',)

@admin.register(Subcategory)
class CategoryAdmin(admin.ModelAdmin):
    inlines = [MediaInline, AttributeInline]  # Add the inline
    list_display = ('name', 'category')

@admin.register(Media)
class MediaAdmin(admin.ModelAdmin):
    # Optional: Hide 'brand' in standalone Media admin
    exclude = ('brand', 'category', 'subcategory')
    list_display = ('image', 'pdf')

@admin.register(Attribute)
class AttributeAdmin(admin.ModelAdmin):
    def get_model_perms(self, request):
        return {}


@admin.register(Product)
class ProductAdmin(nested_admin.NestedModelAdmin):
    inlines = [ModelInline]

@admin.register(Integer)
class IntegerAdmin(admin.ModelAdmin):
    def get_model_perms(self, request):
        """
        Return empty perms dict thus hiding the model from admin index.
        """
        return {}

@admin.register(Decimal)
class DecimalAdmin(admin.ModelAdmin):
    def get_model_perms(self, request):
        return {}

@admin.register(String)
class StringAdmin(admin.ModelAdmin):
    def get_model_perms(self, request):
        return {}

@admin.register(RangedDecimal)
class RangedDecimalAdmin(admin.ModelAdmin):
    def get_model_perms(self, request):
        return {}

@admin.register(RangedInteger)
class RangedIntegerAdmin(admin.ModelAdmin):
    def get_model_perms(self, request):
        return {}

I feel like the data base have some flaws. One problem that I found Is that Iam allowed to add any “attribute” for a model (even the attributes that are not connected to the subcategory of the product of the model. For example the category “Led frame” have a color and a size attribute while the category “led spot light” have a “wat” and “Volt” attribute, but I’ am allowed to add a wat value for an Led frame which shouldn’t be allowed by my Django admin interface or the database itself. If Iam not careful when inserting data i could make this mistake.

Given my schema, admin.py, models.py, the fact that I want to create a product filter, and the fact that I want the website to support English and Arabic language (wich unlike most languages, it goes from right to left so i have to think about how to do this for the website); Is this database good? If so what improvements I can make?

Your feedbacks are appreciated.

Thank you.

Creating a model called Model is not a very good idea. (in my opinion). Use something similar to vehicle_model, light_model ect.

What is

suppose to mean?

Each “Attribute” (like Wat, Color, Length, Volt) for a “Model” will have a value. But each “Attribute” can have only one value type, like string(color for example), decimal(Length for example), integer(Wat for example), or maybe even a range or two integers or decimals (Like volt for example).

So in the beginning I wanted to create a Nullable field for each one in the “Model” table or to a table linked to the “Model” Table, but then I came to the problem that each model attribute can have only one Value type (for example a model color can only be a string, it cannot be a string and a decimal) but I didn’t know how to do this because of Ranged valued who have two valued, a min value and a max value.

This lead to me creating a table for each value type that i think i will need like “String”, “Integer”, etc(I know this might not be the best approach but I didn’t know what else to do)

Why it can’t be like this?

# Create your models here.
class Light(models.Model):
    Wat = models.CharField(max_length=50, unique=True, blank=False, null=False)
    Colour = models.CharField(max_length=50, unique=True, blank=False, null=False)
    Length = models.CharField(max_length=50, unique=True, blank=False, null=False)

    def __str__(self):
        return f"{self.value}" 

?

Because some Subcategories(like Frame) of products(Like Led Frame) don’t have all the attributes(for example the Led Frame don’t have wat, volt, lumen)

Also I don’t want to hardcode attributes, I want the user to be able to add attributes on his own using the admin interface, so that in the future if a new product comes and it have an attribute that have never been used before in the database (like a check box for example to indicate if the product light brightness are adjustable or not)

Also a product can have multiple values for the same attribute. For example a light bulb can maybe have 3 colors like white, green, blue.

Iam using this website as a reference to what I wanna achieve.

What about this?

# Create your models here.
class Light_properties(models.Model):
    Name = models.CharField(max_length=50, unique=True, blank=False, null=False)
    Value = models.CharField(max_length=50, unique=True, blank=False, null=False)
  

    def __str__(self):
        return f"{self.value}" 

The attributes (wat, volt, etc) should be Connected to a subcategory. why?
because when I create the filter I only want to show/loop over filter fields that are relevant to each subcategory page (as each subcategory will have a page displaying its products).

when the user first create a subcategory he can create all its attributes before even adding products, and when he add a “Model” for a product he can then choose the attribute and write its value(or choose it if it have already been used before maybe).

also one subcategory can have multiple attributes and one attribute can be used by multiple subcategories (many to many relationship)

I have had somewhat similar approach to this before but i removed it because of those issues mentioned:

class Category(models.Model):
    name = models.CharField(max_length=50, unique=True, blank=False)

    def __str__(self):
        return self.name

class Brand(models.Model):
    name = models.CharField(max_length=50, unique=True, blank=False)

    def __str__(self):
        return self.name

class SubCategory(models.Model):
    name = models.CharField(max_length=50, unique=True, blank=False)
    category = models.ForeignKey(Category, on_delete=models.CASCADE, blank=False)

    class Meta:
        unique_together = ('category', 'name')

    def __str__(self):
        return f"{self.category} - {self.name}"

class SubCategoryProperty(models.Model):
    DATA_TYPES = [
        ('STRING', 'String'),
        ('INTEGER', 'Integer'),
        ('DECIMAL', 'Decimal'),
        ('MULTIPLE_STRINGS', 'multiple_strings'),
        ('MULTIPLE_INTEGERS', 'multiple_integers'),
        ('MULTIPLE_DECIMALS', 'multiple_decimals'),
    ]

    category = models.ForeignKey(
        SubCategory,
        on_delete=models.CASCADE,
        related_name='properties'
    )

    name = models.CharField(max_length=50)
    data_type = models.CharField(max_length=20, choices=DATA_TYPES, default='STRING')
    ranged = models.BooleanField(default=False)

    class Meta:
        unique_together = ('category', 'name')

    def __str__(self):
        return f"{self.name}"


class Product(models.Model):
    name = models.CharField(max_length=200, blank=False)
    category = models.ForeignKey(
        SubCategory,
        on_delete=models.CASCADE,
        related_name='products'
    )
    brand = models.ForeignKey(
        Brand,
        on_delete=models.CASCADE,
        related_name='products'
    )
    image = models.ImageField(
        upload_to='products/',
        blank=True,
        default='products/default.png'
    )
    pdf = models.FileField(
        upload_to='pdfs/',
        blank=True,
        validators=[FileExtensionValidator(['pdf'])],
        help_text='Upload product specification PDF'
    )

    class Meta:
        unique_together = ('category', 'name')

    def __str__(self):
        return f"{self.brand} - {self.category} - {self.name} "


class ProductModel(models.Model):
    product = models.ForeignKey(
        Product,
        on_delete=models.CASCADE,
        related_name='models'
    )
    name = models.CharField(max_length=50)

    class Meta:
        unique_together = ('product', 'name')

    def __str__(self):
        return f"{self.product.brand.name} {self.name}"

class ModelProperty(models.Model):
    model = models.ForeignKey(
        ProductModel,
        on_delete=models.CASCADE,
        related_name='properties'
    )
    property = models.ForeignKey(
        SubCategoryProperty,
        on_delete=models.CASCADE
    )

    value = model.CharField(max_length=255, default='')


    class Meta:
        unique_together = ('model', 'property')

    def __str__(self):
        return f"{self.model} - {self.property.name}"

    def clean(self):
        super().clean()
        if self.property.category != self.model.product.category:
            raise ValueError("Category must be equal to product.category")

    def save(self, *args, **kwargs):
        self.full_clean()  # Enforces validation
        super().save(*args, **kwargs)

I understand your problems. But it’s not very nice to ask other people to redesign your db completely for free. Rather ask actual problems in small scope. Thank you.

Is this database design good?

Apparently no. I could be wrong.

@KenWhitesell, if you can give any insight here, since I’m not very experienced in this forum.

1 Like

Thank you for the feedback, I don’t expect a full database redesign, this database is working for me now, I just wanted to know if it is solid for the future or if I should change it.

It looks unnecessary to create a table for every object property. It’s always can be simplified as in my opinion.

Also it’s not very intuitive to name tables like Integer, Decimal. Table names should represent the meaning of the data in the tables.

2 Likes

We don’t directly discourage any Django-specific question. However, the posting of a question doesn’t necessarily demand an answer, either.

There’s always the chance of finding another member who has a sufficient interest in a topic to be willing to dive into it.

So at most, the advice here is to point out that a more limited question is more likely to get the desired response.

1 Like