Django学习(8)模型的关联与约束

关联关系

不同对象之间的关联关系有三种。

一个对象单独对应另一个类的对象(在某些场景非常有用,比如与Django内置的用户模型相关联)。从概念上讲,这与 ForeignKey + unique=True 相似。

反向名称:
指定 related_name 参数,即可用 关联的对象.related_name 返回本对象。这在三种关联关系中都适用。
如果未指定 related_name 参数,则Django会将当前模型的小写名称用作默认值。
如果您希望Django不要创建反向关系,请将设置 related_name 为'+'或以'+'结束。
related_query_name 用法与 related_name 类似,只是使用时需要补写 _set [1]

from django.conf import settings
from django.db import models
class MySpecialUser(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
    supervisor = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='supervisor_of',

以上,将自定义的models类和内置后台管理员账户User做了关联。使用user.myspecialuseruser.supervisor_of皆可查询到这边的对象。

一个问题有多个选项,一个选项只属于一个问题,那么就给选项添加外键:

from django.db import models
class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')
class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

半路添加外键可能出问题,可以把数据库和迁移文件都删掉,再强制迁移。
更新外键,直接把对象赋值到外键字段:

>>> from blog.models import Blog, Entry
>>> entry = Entry.objects.get(pk=1)
>>> cheese_blog = Blog.objects.get(name="Cheddar Talk")
>>> entry.blog = cheese_blog
>>> entry.save()

一个用户可以买多个课程,一个课程可以被多个用户购买,这就是多对多关系。

class User(models.Model):
   bought = models.ManyToManyField(Course, related_name='purchasers')

ManyToMany字段可以存放多个其他表或自己表的对象。

多对多关系中的额外字段[2]

一个人与其所属的组之间存在多对多关系,因此您可以使用ManyToManyField表示这种关系。但是,您可能希望收集很多详细信息,例如该人借该书的日期。Django允许您指定将用于管理多对多关系的模型。然后,您可以在中间模型上放置额外的字段。中间模型与ManyToManyField使用 through参数关联以指向将充当中间模型的模型。

from django.db import models
class User(models.Model):
    name = models.CharField(max_length=20)
    borrow = models.ManyToManyField(Book, related_name='borrowers', through='BorrowShip')
class Book(models.Model):
    name = models.CharField(max_length=60)
class BorrowShip(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    person = models.ForeignKey(User, on_delete=models.CASCADE, related_name='borrow_set')
    borrow_time = models.DateTimeField()
    return_date = models.DateTimeField()

多关联的操作

多关联的添加[3]

对于多对多,使用add()方法,单参数或多参数都可以:

>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> entry.authors.add(joe)
>>> john = Author.objects.create(name="John")
>>> paul = Author.objects.create(name="Paul")
>>> george = Author.objects.create(name="George")
>>> ringo = Author.objects.create(name="Ringo")
>>> entry.authors.add(john, paul, george, ringo)

带额外字段的多对多关系,可以通过创建中间类来添加关系,也可以在原模型类直接创建。
创建中间类,创建时必要属性都要指定:

>>> ringo = Person.objects.create(name="Ringo Starr")
>>> paul = Person.objects.create(name="Paul McCartney")
>>> beatles = Group.objects.create(name="The Beatles")
>>> m1 = Membership(person=ringo, group=beatles,
...     date_joined=date(1962, 8, 16),
...     invite_reason="Needed a new drummer.")
>>> m1.save()
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>]>
>>> ringo.group_set.all()
<QuerySet [<Group: The Beatles>]>
>>> m2 = Membership.objects.create(person=paul, group=beatles,
...     date_joined=date(1960, 8, 1),
...     invite_reason="Wanted to form a band.")
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>]>

通过多对多关系添加,借助add(), create()set(),使用through_defaults设置中间类的属性:

# 添加现有的:
>>> beatles.members.add(john, through_defaults={'date_joined': date(1960, 8, 1)})
# 添加一个新创建的:
>>> beatles.members.create(name="George Harrison", through_defaults={'date_joined': date(1960, 8, 1)})
>>> beatles.members.set([john, paul, ringo, george], through_defaults={'date_joined': date(1960, 8, 1)})

多关联的查询

多对多关系中,每一个多对多字段都相当于存了一张数据列表,对象.字段可以得到objects

user = User.objects.get(pk=1)
courses = user.bought.all()

bought是一个多对多字段,产生的courses就是一个QuerySet,可以继续进行筛选或迭代操作。
以上是此表对彼表的查询。此表中设置的related_name参数,用法与多对一和一对一关系中的相似,代表在相关联的彼表的对象中可以用彼表对象.related_name,来查询与某彼表对象关联的此表对象列表。
对中间类额外字段的查询,可以先查到所有外键的对象,再从关联中间表查询键匹配的对象。若想从原模型类直接查到关联关系中间表的额外字段,需使用中间表设置的related_name

    user = User.objects.get(tempid=tempid)
    userBorrow1 = BorrowShip.objects.filter(person=user)
    userBorrow2 = user.borrow_set.filter(person=user)

多关联的移除

使用remove()方法移除一个关联,使用clear()方法清空所有多关联:

ringo = Person.objects.get(name='ringo')
beatles.members.remove(ringo) # 移除一个
beatles.members.clear() # 清空所有

使用remove()时需要注意的是,如果有多个ringo都在这个关联中,所有的ringo都会移除。若要精确移除,需要针对中间表操作,并添加限定条件:

book = Book.objects.get(id=data['bookid'])
user = User.objects.get(tempid=data['tempid'])
ships = BorrowShip.objects.all()
if data['opt']=='unfavor':
    ships.remove(book=book,person=user,borrow_state='favor')
elif data['opt']=='unbooking':
    ships.remove(book=book,person=user,borrow_state='booking')

约束 Meta.constraints

Meta用于设定模型中的非字段的信息,如排序方式、数据库表名、约束关系等。可用的约束关系有:

  • 检查约束:限制字段的值或大小
  • 唯一约束:实现联合唯一性约束,可以设置条件
    一般用法举例:
  • class BookingShip(models.Model):
        room = models.ForeignKey(Room, on_delete=models.CASCADE)
        person = models.ForeignKey(User, on_delete=models.CASCADE)
        date = models.DateTimeField()
        class Meta:
            constraints = [
                # 18岁以上才能订房间
                models.CheckConstraint(check=Q(age__gte=18), name='age_gte_18'),
                # 每个房间每天只能被一个人订
                models.UniqueConstraint(fields=['room', 'date', 'person'], name='unique_booking')
    

    唯一性约束UniqueConstraint可以添加condition参数实现比较复杂的逻辑约束:

    class Options(Answer):
        correct = models.BooleanField()
        question = models.ForeignKey(Question,models.CASCADE,verbose_name='所属问题',related_name='answers',null=True)
    # 题目有三种:单选题有一个正确答案和多个错误选项,多选题不限制,判断题有且仅有一个正确选项
        class Meta:
            constraints = [
                # 题目是单选且此选项正确,这样的选项只能有一个
                models.UniqueConstraint(
                    fields=['question'],name='single_unique_correct',
                    condition=(Q(correct=True)&Q(question__ques_type='single'))),
                # 如果题目是判断,题目和正误符合联合唯一性
                models.UniqueConstraint(
                    fields=['question','correct'],name='single_unique_correct',
                    condition=Q(question__ques_type='true_false'))
    

    功能更强大的UniqueConstraint用于全面代替unique_together,今后可能会将unique_together淘汰。