掌握Django(二)

本文设计在线商城storefront的数据模型,这是项目开发的第一步。


数据模型介绍

我们要创建的项目是一个在线商城,很明显有以下数据模型:

  • 产品(Product)
  • 类别(Collection)
  • 购物车(Cart)
  • 订单(Order)
  • 顾客(Customer)

购物车(Cart)与产品(Product)之间的关系是多对多,一般需要增加关系类,拆成两个一对多的关系,这里可以增加关系类 CartItem。

同样,订单(Order)与产品(Product)之间的关系也是多对多,也折成两个一对多的关系,增加关系类 OrderItem。

考虑给相关产品增加标签,可以添加一个Tag类。

这样数据模型就是如下这样:


在应用中组织模型

或许我们能想到两种方式。

第一种是将所有模型都集中到一个应用store中,这样做的问题是以后增加功能,会变得越来越难以理解。

第二种方法,考虑分成几个应用,比如是这样。但我们看到,各个应用之间还是紧密依赖的,使用某个应用还要安装其他依赖。这个也不可取。

应用中模型的组织要做到高内聚,外部低耦合。我们看到Product、Customer、Cart、Order是紧密相关的,分开来意义不大。只有Tag是不相关的,我们可以对商城物品进行标识,也可以对博客、文章进行打标签。所以合适的组方式如下:

应用中模型理清之后,我们可以写代码了,首先添加2个应用:store 和 tags。

python manage.py startapp store

python manage.py startapp tags

再按上一节的方法在settings.py中的INSTALL_APPS 这一节中注册这两个应用。

INSTALLED_APPS = [

    …

    ‘store’,

    ‘tags’,

]


创建模型

模型的字段类型可以用谷歌搜索 “django field types”,查看相关说明。

下面来创建模型,首先是Product,store–>models.py添加内容:

class Product(models.Model):
    title = models.CharField(max_length=255)
    description = models.TextField()
    price = models.DecimalField(max_digits=6, decimal_places=2)
    inventory = models.IntegerField()
    last_update = models.DateTimeField(auto_now=True)
    # auto_now表示每次更新Product都会自动更新时间,
    # 还有一个auto_now_add=True表示只有添加时自动更新

再是 Customer:

class Customer(models.Model):
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)
    email = models.EmailField(unique=True)
    phone = models.CharField(max_length=255)
    birth_date = models.DateField(null=True)
    # null=True表示允许空
    # Django会为每个Model自动创建主键ID,如果想自己设主键的话,可以创建:
    # sku = models.CharField(max_length=10, private_key=True)

有时需要限制字段只能是某些值,比如客户身份,B、S、G分别表示铜牌、银牌、金牌。我们完善Customer。

class Customer(models.Model):
    MEMBERSHIP_BRONZE = 'B'
    MEMBERSHIP_SILVER = 'S'
    MEMBERSHIP_GOLD = 'G'
    MEMBERSHIP_CHOICES = [
        (MEMBERSHIP_BRONZE, 'Bronze'),
        (MEMBERSHIP_SILVER, 'Silver'),
        (MEMBERSHIP_GOLD, 'Gold'),
    ]
    ...
    
    membership = models.CharField(max_length=1,
        choices=MEMBERSHIP_CHOICES,
        default=MEMBERSHIP_BRONZE)

再设计Order模型:

class Order(models.Model):
    PAYMENT_STATUS_PENDING = 'P'
    PAYMENT_STATUS_COMPLETE = 'C'
    PAYMENT_STATUS_FAILED = 'F'
    PAYMENT_STATUS_CHOICES = [
        (PAYMENT_STATUS_PENDING, 'Pending'),
        (PAYMENT_STATUS_COMPLETE, 'Complete'),
        (PAYMENT_STATUS_FAILED, 'Failed'),
    ]
    placed_at = models.DateTimeField(auto_now_add=True)
    payment_status = models.CharField(
        max_length=1, choices=PAYMENT_STATUS_CHOICES,
        default=PAYMENT_STATUS_PENDING)

设计一对一关系

我们假设每个顾客只有一个地址,可以设计Address与Customer为一对一关系:

class Address(models.Model):
    street = models.CharField(max_length=255)
    city = models.CharField(max_length=255)
    customer = models.OneToOneField(
        Customer, on_delete=models.CASCADE, primary_key=True)

一对一关系使用models.OneToOneField 类,这里设置父类为Customer,表示有Customer后才有Address类,primary_key=True 表示这是主键,否则会自动生成主键ID,这样就变成一对多了。

问题:我们是否还要为Customer创建一对一关系?

不用的!Django会自动创建。


设计一对多关系

有些顾客有多个地址,这显然更常见,我们就要设计一对多关系了。很简单,只需将OneToOneField 改成 ForeignKey即可,另外也取消primary_key=True 这个参数项。

class Address(models.Model):
    ...
    customer = models.ForeignKey(
        Customer, on_delete=models.CASCADE)

同样的,按上述方法,我们设计以下一对多关系:

  • Collection和Product,on_delete使用PROTECT
  • Customer–Order,on_delete使用PROTECT
  • Order–OrderItem,on_delete使用CASCADE
  • Cart–CartItem,on_delete使用CASCADE

很简单,不具体说明,至此,形成的store–>models.py的代码如下:

from django.db import models


# Create your models here.
class Collection(models.Model):
    title = models.CharField(max_length=255)


class Product(models.Model):
    title = models.CharField(max_length=255)
    description = models.TextField()
    price = models.DecimalField(max_digits=6, decimal_places=2)
    inventory = models.IntegerField()
    last_update = models.DateTimeField(auto_now=True)
    collection = models.ForeignKey(Collection, on_delete=models.PROTECT)


class Customer(models.Model):
    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=255)
    last_name = models.CharField(max_length=255)
    email = models.EmailField(unique=True)
    phone = models.CharField(max_length=255)
    birth_date = models.DateField(null=True)
    # null=True表示允许空
    # Django会为每个Model自动创建主键ID,如果想自己设主键的话,可以创建:
    # sku = models.CharField(max_length=10, private_key=True)

    membership = models.CharField(max_length=1,
                                  choice=MEMBERSHIP_CHOICES,
                                  default=MEMBERSHIP_BRONZE)


class Order(models.Model):
    PAYMENT_STATUS_PENDING = 'P'
    PAYMENT_STATUS_COMPLETE = 'C'
    PAYMENT_STATUS_FAILED = 'F'
    PAYMENT_STATUS_CHOICES = [
        (PAYMENT_STATUS_PENDING, 'Pending'),
        (PAYMENT_STATUS_COMPLETE, 'Complete'),
        (PAYMENT_STATUS_FAILED, 'Failed'),
    ]
    placed_at = models.DateTimeField(auto_now_add=True)
    payment_status = models.CharField(
        max_length=1, choice=PAYMENT_STATUS_CHOICES,
        default=PAYMENT_STATUS_PENDING)
    customer = models.ForeignKey(Customer, on_delete=models.PROTECT)


class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.PROTECT)
    product = models.ForeignKey(Product, on_delete=models.PROTECT)
    quantity = models.PositiveSmallIntegerField()
    unit_price = models.DecimalField(max_digits=6, decimal_places=2)


class Address(models.Model):
    street = models.CharField(max_length=255)
    city = models.CharField(max_length=255)
    customer = models.ForeignKey(
        Customer, on_delete=models.CASCADE)


class Cart(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)


class CartItem(models.Model):
    cart = models.ForeignKey(Cart, on_delete=models.CASCADE)
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    quantity = models.PositiveSmallIntegerField()

设计多对多关系

我们设计一个促销类Promotion,促销可以有多个产品,产品也可以参加多个促销,属于多对多的关系。

先添加促销类:

class Promotion(models.Model):
    description = models.CharField(max_length=255)
    discount = models.FloatField()

可以在任何一个类中定义多对多关系,另一个类自动创建反向关系。这里在Product中实现,这样可以很好地知道该产品参加哪些促销活动。

class Product(models.Model):
     ...
    promotions = models.ManyToManyField(Promotion)

Product添加了一个字段promotions,同时会自动在Promotion下创建product_set字段,用于多对多关系,如果觉得名字丑,想改成如products,可以用related_name手动定义:

promotions = models.ManyToManyField(Promotion, related_name=’products’)

这里暂保持默认,也就是相当于在Promotion模型下自动增加了product_set字段。


解决循环依赖关系

在模型设计图,我们看到,Product和Collection是互相依赖的。

我们来添加Collection的特色产品字段 featured_product.

class Collection(models.Model):
    ...
    ...
    featured_product = models.ForeignKey(
        'Product', on_delete=models.SET_NULL, null=True)

注意这里的Product模型是加引号的,因为Collection定义在Product前面,不能识别Product模型,所以加上引号来解决循环依赖不识别的问题。

但这里还是有错,Django在创建关系时,会自动在另一端创建相应的关系,比如Collection加了featured_product,Product会自动创建名为collection的字段连接关系。而原来在Product中,collection字段已经存在,导致创建失败。

有两种解决办法,一是改变默认的名字,二是不用创建反向关系。

...
featured_product = models.ForeignKey(
        'Product', on_delete=models.SET_NULL, null=True, 
        related_name='+')
...

related_name可以改变默认的命名,命名为 ‘+’ 表示不用创建反向关系。这里用的就是第二种解决办法。


通用关联关系

我们设计了两个应用 store 和 tags,我们期望 tags应用 是通用的,我们可以将tags应用设计为由两个模型组成:Tag 和 TaggedItem,其中TaggedItem用于标记具体的项目。

我们来编辑tags—>models.py文件。

class Tag(models.Model):
    label = models.CharField(max_length=255)

class TaggedItem(models.Model):
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    product = models.ForeignKey(Product)

这段代码是粗暴丑陋的,因为需要导入另一个应用的Product模型,后续还要标记其他应用的模型时,也需不停导入,极不灵活。

这是个糟糕的设计,如何解决?

我们要寻找一种通用的依赖关系,只需知道两个信息:一是类型type,标记是Product、Video或article,另一个信息是ID。

有了这两个信息,就可以标记任何模型的任何记录。

我们来修改下:

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
...
...
class TaggedItem(models.Model):
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    content_type = models.ForeignKey(ContentType,on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey()

Django默认已安装了contenttypes应用,该应用专门为了通用关系关联。

层次结构中的contrib是贡献的简写。

我们导入该应用的ContentType类用于标记哪个模型, 导入 GenericForeignKey类用于实际的标识内容。

再增加一个object_id字段用于指定模型的具体记录,一般用数字ID,如果主键用GUID将是无效的。

这个通用关联是很有用的,比如我们要创建一个应用likes,添加模型LikedItem,再利用Django的自带模型User(django.contrib.auth.models模块下),防照前面的4个字段,就可以实现like任何模型的任何记录了。


小结

本文详细介绍了数据模型设计的整个过程,基本涉及了各个Django模型设计的各个知识点。下一篇开始讲解模型与数据库的映射。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注