📚 Django - Data Modelling

📚 Django - Data Modelling
Photo by Faisal / Unsplash

Create Model

  • 會自動產生 primary key,因為繼承自 models.Model。若想要自訂 primary key,可以參考 sku 的寫法
# playground/models.py
from django.db import models

# Create your models here.
class Product(models.Model):
    #sku = models.CharField(max_length=10, primary_key=True) # use this primary key
    title = models.CharField(max_length=300)
    description = models.TextField()
    price = models.DecimalField(max_digits=6, decimal_places=2)
    inventory = models.IntegerField()
    last_updated = models.DateTimeField(auto_now=True) # update every time

Choices field

class Customer(models.Model):
    # best practice to prevent hardcode
    MEMBERSHIP_BRONZE = 'B'
    MEMBERSHIP_SILVER = 'S'
    MEMBERSHIP_GOLD = 'G'

    MEMBERSHIP_CHOICES = [
        (MEMBERSHIP_BRONZE, 'Bronze'),
        (MEMBERSHIP_SILVER, 'Silver'),
        (MEMBERSHIP_GOLD, 'Gold'),
    ]
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    email = models.EmailField(max_length=100,unique=True)
    phone = models.CharField(max_length=15, unique=True)
    birth_date = models.DateTimeField(null=True, blank=True)
    membership = models.CharField(max_length=1, choices=MEMBERSHIP_CHOICES, default=MEMBERSHIP_BRONZE)

One-to-One relationship

class Address(models.Model):
    street = models.CharField(max_length=255)
    city = models.CharField(max_length=255)
    # address is the child, customer is the parent
    customr = models.OneToOneField(
        Customer, on_delete=models.CASCADE, primary_key=True)
  • on_delete=models.CASCADE (層級刪除): 當父模型被刪除時,與之相關聯的子模型也會被刪除
  • on_delete=models.PROTECT (保護模式): 防止刪除父模型,除非先刪除所有與之相關的子模型。
  • on_delete=models.SET_NULL (設為空值): 當父模型被刪除時,與之相關聯的子模型的外鍵欄位(即OneToOneField)會被設為 NULL。
    • 範例:一個簡單的部落格系統,如果一篇文章被作者刪除,該文章的作者欄位會被設為 NULL。
  • on_delete=models.SET_DEFAULT (設為預設值): 當父模型被刪除時,與之相關聯的子模型的外鍵欄位會被設為預設值。
    • 範例:一個簡單的部落格系統,如果一篇文章被作者刪除,該文章的作者欄位會被設為 NULL。
  • on_delete=models.SET() (設定為特定值): 當父模型被刪除時,與之相關聯的子模型的外鍵欄位會被設為特定的值(通常是另一個存在的模型實例)。
    • 範例:一個論壇系統,如果一個用戶帳號被刪除,與之相關聯的文章的作者欄位會被設為論壇的預設用戶,這個預設用戶的 ID 為 1。
class Child(models.Model):
    parent = models.OneToOneField(Parent, on_delete=models.CASCADE)
    ...
class Child(models.Model):
    parent = models.OneToOneField(Parent, on_delete=models.PROTECT)
    ...
class Child(models.Model):
    parent = models.OneToOneField(Parent, on_delete=models.SET_NULL, null=True)
    ...
class Child(models.Model):
    parent = models.OneToOneField(Parent, on_delete=models.SET_DEFAULT, default=1)
    ...
class Child(models.Model):
    parent = models.OneToOneField(Parent, on_delete=models.SET(DefaultParent.objects.get(pk=1)))
    ...

One-to-many relationship

one collection can have many products

  • Use foreignkey

  • if you can not modify the order of declaration for the collection model, you can employ a string argument within the ForeignKey field:

    collection = models.ForeignKey('Collection', on_delete=models.PROTECT)

class Collection(models.Model):
    title = models.CharField(max_length=300)

class Product(models.Model):
    ...
    collection = models.ForeignKey(Collection, on_delete=models.PROTECT)

Many-to-Many relationship

  • Use ManyToManyField
  • to customize the name of the reverse relation. Instead of the default product_set, you can use the related_name field
class Promotion(models.Model):
    description = models.CharField(max_length=255)
    discount = models.FloatField()
    # product_set

class Product(models.Model):
    ...
    collection = models.ForeignKey('Collection', on_delete=models.PROTECT)
    #promotions = models.ManyToManyField(Promotion, related_name='products')
    promotions = models.ManyToManyField(Promotion)

Generic Relationship

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.auth.models import User

class LikeItem(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    # type (Table)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    # ID (record)
    object_id = models.PositiveSmallIntegerField()
    # Pointer
    content_object = GenericForeignKey()
  1. ContentType (內容類型): 在 Django 中,ContentType 是一個模型,它用來表示其他模型的類型。在這個程式碼中,content_type 欄位是一個外鍵,關聯到 ContentType 模型。這麼做的好處是,它允許你動態地參照其他模型,而不是在程式碼中直接寫死模型的名稱。這樣的設計可以增加彈性,讓你的程式碼更容易擴展和修改,而不需要改動太多程式碼。

  2. GenericForeignKey (通用外鍵): GenericForeignKey 是一個用於關聯任意模型的特殊欄位。在這個例子中,content_object 欄位使用了 GenericForeignKey。這意味著 LikeItem 模型可以與任何其他模型建立關聯,而不僅僅是固定的一個或幾個模型。通過 content_type 和 object_id 這兩個欄位,你可以動態地建立與不同模型的關聯。

  3. LikeItem 模型: LikeItem 是一個用戶點贊的模型,它關聯到了使用者 (user) 和其他模型 (content_type 和 object_id 確定的模型)。這樣的設計允許你在不確定具體模型的情況下,保存用戶對不同類型物件的喜愛。

在 LikeItem 模型中,content_type 是一個外鍵欄位,關聯到 ContentType 模型,用來指定被點贊物件的類型(例如,是一篇文章還是一張圖片等等)。而 object_id 是一個正整數欄位,用來指定具體的被點贊物件的 ID。

GenericForeignKey 的作用就是把 content_type 和 object_id 這兩個欄位關聯到一個具體的模型實例上,這樣你就可以在 LikeItem 中使用 content_object 這個屬性,直接訪問被點贊的具體物件,而不需要關心它是哪種模型。

例如,如果 content_type 指定為文章,object_id 指定為某篇文章的 ID,那麼 content_object 就可以直接訪問該篇文章的所有屬性和方法。這樣,你可以在不確定具體模型的情況下,動態地建立關聯,使得程式碼更加靈活和可擴展。

你可以將 content_object 想像成程式語言中的指標 pointer。在這個上下文中,content_object 實際上是一個通用指標,指向了應用中的任何一個模型的特定實例。這樣的設計使得你可以動態地指向和訪問不同模型的物件,而不需要知道具體是哪個類型的物件。

就像指標在程式語言中可以指向不同的變數或數據結構,content_object 允許你在 LikeItem 模型中指向任何類型的物件,從而實現彈性的資料模型。這樣的抽象設計使得程式碼更具通用性,可以應對多種不同的情況,同時也使得程式碼更容易擴展和維護。


Generic Relationship - Example

假設你的對象是 Article(文章)模型,你可以使用 GenericForeignKey 來建立 LikeItem 與 Article 之間的關聯。以下是一個簡單的範例:

首先,定義你的 Article 模型:

from django.db import models

class Article(models.Model):
    # 文章的屬性,例如標題、內容等等
    title = models.CharField(max_length=100)
    content = models.TextField()

    def __str__(self):
        return self.title

然後,定義你的 LikeItem 模型,使用 GenericForeignKey 來建立通用外鍵關聯:

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
from django.db import models

class LikeItem(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

在這個例子中,LikeItem 模型可以關聯到任何模型的實例,包括 Article。當你想要為某篇文章建立一個 LikeItem 時,你可以這樣做:

# 創建一篇文章
article = Article.objects.create(title="標題", content="文章內容")

# 創建一個 LikeItem 關聯到這篇文章
like_item = LikeItem.objects.create(user=user, content_type=ContentType.objects.get_for_model(article), object_id=article.id)

# 使用 GenericForeignKey 訪問被點贊的物件
liked_article = like_item.content_object
print(liked_article.title)  # 輸出: 標題

在這個例子中,content_type 指定為 Article 模型的類型,object_id 指定為具體文章的 ID。通過 GenericForeignKey,你可以使用 content_object 直接訪問到被點贊的文章的屬性。這樣,你可以動態地建立與不同模型的關聯,而不需要為每種模型都定義一個單獨的外鍵。


Reverse Relationship - Example

在 Django 中,反向關係(reverse relationship) 通常是自動產生的,無論是在一對一(One-to-One)、一對多(One-to-Many)還是多對多(Many-to-Many)的關係中,Django 都會自動為你建立反向關係。這意味著你可以從一個模型對象訪問與之相關聯的其他模型對象。

假設你有兩個模型,一個是Author,另一個是Book。Book模型有一個外鍵欄位指向Author模型:

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

在這個例子中,Book模型有一個外鍵欄位author指向Author模型。Django 會自動為Author模型生成一個反向關係。這意味著你可以透過Author模型來訪問所有與之相關聯的書籍,即使你並沒有在Author模型中定義相關的欄位。

例如,如果你有一個Author的實例author,你可以這樣獲取所有與這個作者相關的書籍:

books = author.book_set.all()

在這裡,book_set就是 Django 自動為Author模型生成的反向關係。這個名稱的格式是小寫的模型名稱_set,它可以讓你訪問到所有與這個模型相關的對象。這種反向關係的自動生成使得在 Django 中進行模型之間的查詢和操作更加方便。


然而,有一個特殊的情況是如果你已經使用了 related_name 參數來定義了自定義的反向關係名稱,反向關係的名稱就不再是默認的 小寫的模型名稱_set 格式。在這種情況下,你必須通過自定義的名稱來訪問反向關係。

舉例來說:

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=100)

class Passport(models.Model):
    number = models.CharField(max_length=20)
    person = models.OneToOneField(Person, on_delete=models.CASCADE, related_name='passport')

在這個例子中,Passport 模型有一個一對一的外鍵關係,並且使用了 related_name='passport' 參數。這樣,在 Person 模型中就不會自動生成默認的反向關係。要訪問這個關係,你需要使用 passport 這個自定義的名稱:

person = Person.objects.get(pk=1)
passport = person.passport  # 訪問一對一關係的反向關係

在其他情況下(包括一對多和多對多關係),Django 都會自動生成默認的反向關係,你可以直接使用模型的名稱(小寫)作為反向關係的名稱。

related_name field in ForeignKey

在一對多關係中,related_name 可以被用來定義反向關係的名稱,使你能夠更具意義地訪問相關對象。在多對多關係中,related_name 可以被用來定義通過中間表訪問相關對象時的名稱。

以下是一個一對多和多對多關係中 related_name 的使用示例:

一對多(One-to-Many)關係:

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')

在這個例子中,Book 模型的 author 欄位定義了外鍵關係,並且使用了 related_name='books' 參數。這樣,你就可以透過 author.books.all() 來訪問一位作者的所有書籍。

多對多(Many-to-Many)關係:

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=100)
    groups = models.ManyToManyField('Group', related_name='memberships')

class Group(models.Model):
    name = models.CharField(max_length=100)
    # 其他欄位和方法

在這個例子中,Person 模型和 Group 模型之間建立了多對多的關係。Person 模型的 groups 欄位使用了 related_name='memberships' 參數。這樣,你可以透過 group.memberships.all() 來訪問一個群組中的所有成員。