本文设计在线商城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模型设计的各个知识点。下一篇开始讲解模型与数据库的映射。