社交Web程序允许用户之间相互联系,在程序中,这种关系成为关注者、好友、联系人、联络人或伙伴,但不管使用哪个名字,其功能都是一样的,而且都要记录两个用户之间的定向联系,在数据库查询中也要使用这种联系
再论数据库关系
之前我们说过,数据库使用关系建立记录之间的联系,其中,一对多关系是最常用的关系类型,它把一个记录和一组相关的记录联系在一起,实现这种关系时,要在“多”这个侧加入一个外键,指向“一”这一侧联结的记录,目前我们的程序现在包含两个一对多关系:一个把用户角色和一组用户联系起来,另一个把用户和发布的博客文章联系起来
大部分的其他关系类型都可以从一对多类型中衍生,多对一关系从“多”这一侧看就是一对多关系,一对一关系类型是简化版的一对多关系,限制多这一侧最多只能有一个记录,唯一不能从一对多关系中简单演化出来的类型是多对多关系这种关系的两侧都有多个记录
多对多关系
一对多关系、多对一关系和一对一关系至少都有一侧是单个实体,所以记录之间的联系通过外键实现,让外键指向这个实体,但是我们要怎么实现两侧都是“多”的关系呢
下面以一个典型的多对多关系为例,即一个记录学生和他们所选课程的数据库,很显然,你不能在学生表中加入一个指向课程的外键,因为一个学生可以选择多个课程,一个外键不够用,同样,你也不能在课程表中加入一个指向学生的外键,因为一个课程有多个学生的选择,两侧都需要一组外键
这种问题的解决方法是添加第三张表,这个表称为关联表,现在,多对多的关系可以分解成原表和关联表之间的两个一对多关系,下图描绘了学生和课程之间的多对多关系
这个例子中的关联表是registrations,表中每一行都表示一个学生注册的一个课程
查询多对多关系要分成两步,若想知道某位学生选择了哪些课程,要先从学生和注册之间的一对多关系开始,获取这位学生在registrations表中的所有记录,然后再按照多到一的方向遍历课程和注册之间的一对多关系,找到这位学生在registrations表中个记录所对应的课程,同样,若想找到选择了某门课程的所有学生,要先从课程表中开始,获取其在registrations表中的记录,再获取这些记录联接的学生
通过遍历两个关系来获取查询结果的做法听起来有难度,不过像前例这种简单关系,SQLAlchemy就可以完成大部分操作,多对多关系使用的代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
多对多关系仍使用定义一对多关系的db.relationship()
方法进行定义,但在多对多关系中,必须把secondary参数设为关联表,多对多关系可以在任何一个类中定义,backref参数会处理好关系的另一侧,关联表就是一个简单的表,不是模型,SQLAlchemy会自动接管这个表,classes关系使用列表语义,这样处理多对多关系特别简单,假设学生是s,课程是c,学生注册课程的代码是:
- 1
- 2
- 1
- 2
列出学生s注册的课程以及注册了课程c的学生也很简单:
- 1
- 2
- 1
- 2
Class模型中的students关系由参数db.backref()
定义,注意这个关系中还指定了lazy='dynamic'
参数,所以关系两侧返回的查询都可接受额外的过滤器
如果后来学生s决定不选课程c了,那么可使用下面的代码更新数据库:
- 1
- 1
自引用关系
多对多关系可用于实现用户之间的关注,但存在一个问题,在学生和课程的例子中,关联表联接的是两个明确的实体,但是表示用户关注其他用户时,只有用户一个实体,没有第二个实体
如果关系中的两侧都在同一个表中,这种关系成为自引用关系,在关注中,关系的左侧是用户实体,可以称为“关注者”,关系的右侧也是用户实体,但这些是”被关注者“,从概念上来看,自引用关系和普通关系没什么区别,只是不易理解,下图是自引用关系的数据库图解,表示用户之间的关注:
本例的关联表是follows,其中每一行都表示一个用户关注了另一个用户,图中左边表示的一对多关系把用户和follows表中的一组记录联系起来,用户是关注者,图中右边表示的一对多关系把用户和follows表中的一组记录联系起来,用户是被关注者
高级多对多关系
自引用多对多关系可在数据库中表示用户之间的关注,但却有个限制,使用多对多关系时,往往需要存储所联两个实体之间的额外信息,对用户之间的关注来说,可以存储用户关注另一个用户的日期,这样就能按照时间顺序列出所有的关注者,这种信息只能存储在关联表中,但是在之前实现的学生和课程之间的关系中,关联表完全是由SQLAlchemy掌控的内部表
为了能在关系中处理自定义的数据,我们必须提升关联表的地位,使其变成程序可访问的模型,新的关联表如下,使用Follow模型表示:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
SQLAlchemy不能直接使用这个关联表,因为如果这么做程序就无法访问其中的自定义字段,相反地,要把这个多对多关系的左右两侧拆分成两个基本的一对多关系,而且要定义成标准的关系,代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
在这段代码中,followed
和followers
关系都定义为单独的一对多关系,为了消除外键间的歧异,定义关系时必须使用可选参数foreign_keys
指定的外键,而且,db.backref()
参数并不是指定这两个关系之间的引用关系,而是回引Follow模型
回引中的lazy
参数指定为joined
,这个lazy
模式可以实现立即从联结查询中加载相关对象,例如如果某个用户关注了100个用户,调用user.followed.all()
后会返回一个列表,其中包含100个Follow实例,每一个实例的follower和followed回引属性都指向相应的用户,设定为lazy='joined'
模式,就可在一次数据库查询中完成这些操作,如果把lazy
设为默认值select
,那么首次访问follower
和followed
属性时才会加载对应的用户,而且每个属性都需要一个单独的查询,这就意味着获取全部被关注用户时需要增加100次额外的数据库查询
cascade
参数配置在父对象上执行的操作对相关对象的影响,比如层叠选项可设定为:将用户添加到数据库会话后,要自动把所有关系的对象都添加到会话中,层叠选项的默认值能满足大多数情况的需求,但对这个多对多关系来说却不合用,删除对象时,默认的层叠行为是把对象联接的所有相关对象的外键设为空值,但在关联表中,删除记录后正确的行为应该是把指向该记录的实体也删除,因为这样能有效销毁联接,这就是层叠选项值delete-orphan
的作用
cascade参数的值是一组由逗号分隔的层叠选项,这看起来可能让人有点困惑,但all表示除了delete-orphan之外的所有层叠选项,设为all,delete-orphan的意思是启用所有默认层叠选项,还要删除孤儿记录
程序现在要处理两个一对多关系,以便实现多对多关系,由于这些操作经常需要重复执行,所以最好在User模型中为所有可能的操作定义辅助方法,用于控制关系的4个新方法如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
follow()
方法手动把Follow实例插入关联表,从而把关注者和被关注者联接起来,并让程序有机会设定自定义字段的值,联接在一起的两个用户被手动传入Follow类的构造器,创建一个Follow新实例,然后像往常一样,把这个实例对象添加到数据库会话中,注意,这里无需手动设定timestamp
字段,因为定义字段时已经指定了默认值,即当前日期和时间,unfollow()
方法使用followed关系找到联接用户和被关注用户的Follow实例,若要销毁这两个用户之间的联接,只需删除这个Follow对象即可,is_following()
方法和is_followed_by()
方法分别在左右两边的一对多关系中搜索指定用户,如果找到了就返回True