第十二章 关注者(一)

社交WEB应用允许用户之间相互联系,在本章我们将学到如何在Flasky中实现关注功能,让用户“关注”其它用户,并在首页只显示所关注用户发布的博客文章列表。本节将再论数据库关系,讲解多对多关系以及自引用关系在ORM模型中表示。

关系型数据库使用关系建立起记录之间的联系,其中“一对多”关系是最常用的关系模型,它把一个记录和一组相关的记录联系在一起。实现这种关系时,要在多这一侧加入一个外键,指向“一”这一侧连接的记录。多数其它关系类型都可以从一对多类型中衍生:

  1. 多对一:从多这一侧看就是“一对多关系”;
  2. 一对一:简化版的“一对多“关系,限制多这一侧最多只能有一个记录即可;

唯一不能从“一对多”衍生出来的关系是“多对多”关系,这种关系的两侧都有多个记录,接下来将详细讨论“多对多”关系。

一. 多对多关系:学生和课程

一个典型的多对多关系:学生表和课程表,一个学生可以选多门课程,一门课程也会被多个学生选,因此两侧都需要一组外键。解决办法是添加第三张表,这个表是关联表,将多对多关系分解成原表和关联表之间的两个一对多关系。

这个例子中的关联表是registrations,表中的每一行表示一个学生注册的一门课程。上图的多对多关系可使用如下代码表示:

from sqlalchemy import Integer, String, create_engine, \
    Column, Table, ForeignKey
from sqlalchemy.orm import sessionmaker, relationship, backref
from sqlalchemy.ext.declarative import declarative_base

engine = create_engine('sqlite:///demo.sqlite', echo=True)
Base = declarative_base(engine)
session = sessionmaker(bind=engine)()

registrations = Table(
    'registrations', 
    Base.metadata,
    Column('student_id', Integer, ForeignKey('students.id')),
    Column('classes_id', Integer, ForeignKey('classes.id'))
)


class Class(Base):
    __tablename__ = "classes"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    

class Student(Base):
    __tablename__ = "students"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    classes = relationship(
        Class, secondary=registrations,
        backref=backref('students', lazy='dynamic'),
        lazy='dynamic')


Base.metadata.drop_all()    
Base.metadata.create_all()

运行结果(运行上述代码,表格将自动创建):

    

多对多关系仍使用定义一对多关系的relationship()方法定义,但在多对多关系中,必须把secondary参数设为关联表。多对多关系可以在任何一个类中定义,backref参数会处理好关系的另一侧。关联表就是一个简单的表,不是模型,SQLAlchemy会自动接管这个表。

classes关系使用列表语义定义,处理对对多关系时将非常简单:

# 分别实例化课程实例和学生实体
class1 = Class(name='English')
class2 = Class(name='Chinese')
stu1 = Student(name='Alex')
stu2 = Student(name='Bob')

# 将实体关联起来
stu1.classes.append(class1)
stu1.classes.append(class2)
stu2.classes.append(class2)

# 插入表
session.add(stu1)
session.add(stu2)
session.add(class1)
session.add(class2)

# 提交数据库
session.commit()

运行结果:

表展示(依次为课程表、学生表、关系表记录):

    

列出学生stu1注册的课程:

stu1.classes.all()

SQLAlchemy查询过程:

查询结果:

Class模型中的students关系由函数backref()定义。注意此处还指定了lazy="dynamic"参数,所以关系两侧返回的查询都可接收额外的过滤器。

如果后来学生stu1决定不选课程c了,可以使用下面的代码跟新数据库:

将删除提交数据库:

再次查询,确认结果:

二. 多对多关系:帖子和类别标签

from sqlalchemy import DateTime, Table, Column, String, create_engine, Integer, ForeignKey
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
 
Base = declarative_base(engine)
engine = create_engine('sqlite:///demo.sqlite')
DataBase = sessionmaker(bind=engine)
 
post_tag = \
    Table('post_tag', Base.metadata, 
          Column('id', Integer, primary_key=True, autoincrement=True),
          Column('post_id', Integer, ForeignKey('posts.id')),
          Column('tag_id', Integer, ForeignKey('tags.id')))
 
 
class Post(Base):
    __tablename__ = 'posts'
    id = Column(Integer, primary_key=True, autoincrement=True)
    title = Column(String, nullable=False)
    content = Column(String, nullable=False)
    created_on = Column(DateTime, default=datetime.now)
    updated_on = Column(DateTime, default=datetime.now, onupdate=datetime.now)
    # 注意此处关系的2侧我们都没有定义lazy="dynamic",下面查询的时候注意和上例比较结果
    tags = relationship('Tag', backref='posts', secondary=post_tag)
 
    def __repr__(self):
        return '<Post: {} {} letters>'.format(self.title, len(self.content))
 
 
class Tag(Base):
    __tablename__ = 'tags'
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String, nullable=False)
 
    def __repr__(self):
        return '<Tag: {}>'.format(self.name)   

在数据库中创建上述定义的实体类对应的表:

Base.metadata.create_all(bind=engine)

插入数据:

session = DataBase()

p1 = Post(title='hello', content='world')
p2 = Post(title='football', content='world cup')

t1 = Tag(name='Tech')
t2 = Tag(name='Funny')
t3 = Tag(name='News')

# 关联实体对象
p1.tags.append(t1)
p1.tags.append(t2)
p1.tags.append(t3)
p2.tags.append(t2)
p2.tags.append(t3)

# 提交数据库
session.add(p1)
session.add(p2)
session.commit()

查询所有博客文章并获取其对应的标签:

查询所有标签分类,并获取每个标签下的所有博客文章:

三. 自引用关系

多对多关系可用于实现用户之间的关注,但存在一个问题。在上述例子中关联表连接的都是2个不同的实体,表示用户关注其它用户时,只有一个用户实体。

如果关系中的2侧都在同一个表中,这种关系称为自引用关系:

本例的关系表是follows,其中的每一行表示一个用户关注了另一个用户。当我们需要存储所连接两个实体之间的额外信息时,只能存储在关联表中,如存储用户关注另一个用户的日期。但像之前那样,关系表完全是由SQLAlchemy管控的内部表,我们无法插手。为了能在关系中处理自定义的数据,我们必须提升关联表的地位,使其变为应用可访问的模型。

app/models.py:关注关系中关联表的实现

class Follow(db.Model):
    __tablename__ = 'follows'
    follower_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)
    followed_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow)

SQLAlchemy不能直接使用这个关联表,因为这么做应用就无法访问其中的自定义字段。相反地,要把这个多对多关系的左右2侧拆分成2个基本的一对多关系,而且要定义成标准的关系。

app/models.py:使用2个1对多关系实现的多对多关系

class User(UserMixin, db.Model):
    __tablename__ = "users"
    ...

    followed = db.relationship('Follow',
                               foreign_keys=[Follow.follower_id],
                               backref=db.backref('followers', lazy='joined'),
                               lazy='dynamic',
                               cascade='all, delete-orphan')
    followers = db.relationship('Follow',
                                foreign_keys=[Follow.followed_id],
                                backref=db.backref('followed', lazy='joined'),
                                lazy='dynamic',
                                cascade='all, delete-orphan')

解读:

  • followed和followers关系都定义为单独的一对多关系。为了消除外键的歧义,定义关系时必须指定可选参数 foreign_keys 外键。此外backref并不是指定两个关系之间的引用关系,而是回引Follow模型。
  • 回引中的lazy参数为'joined',此lazy模式可以实现立即从联结表中加载相关对象。如某个用户关注了100个用户,调用user.followed.all()后返回一个列表,其中包含100个Follow实例,每一个实例的follower和followed回引属性都指向相应的用户。lazy='joined'模式时,就可以在一次数据库查询中完成这些操作。如果设为'select',只有当首次访问follower和followered属性时才会加载相应的用户,而且每个属性都需要一个单独的查询,这就意味着获取全部被关注用户时需要额外增加100次数据库查询。
  • lazy参数都在“一”这一侧设定,返回的结果是“多”这一侧的记录。User一侧设定的lazy参数为'dynamic',因此关系属性不会直接返回记录,而是返回查询对象,所以在执行查询之前还可以添加额外的过滤器。
  • cascade参数的值是一组由逗号分隔的层叠选项,其中all表示除了delete-orpha之外的所有层叠选项,即启用所有默认层叠选项,而且还要删除孤儿记录。这是因为删除对象时,默认的层叠行为是把对象连接的所有相关对象的外键设为空值。但在关联表中,删除记录后正确的行为是把指向该记录的实体也删除,这样才能有效销毁连接。

应用现在需要处理两个一对多关系,以便实现多对多关系。接下来,在User模型中添加关注、取关、是否是xxx的关注者、是否被xxx关注 的辅助方法:

class User(UserMixin, db.Model):
    __tablename__ = "users"
    ... 

    def is_following(self, user):
        if user.id is None:
            return False
        return self.followed.filter_by(followed_id=user.id).first() is not None

    def is_followed_by(self, user):
        if user.id is None:
            return False
        return self.followers.filter_by(follower_id=user.id).first() is not None

    def follow(self, user):
        if not self.is_following(user):
            f = Follow(followed=user, follower=self)
            db.session.add(f)

    def un_follow(self, user):
        f = self.followed.filter_by(followed_id=user.id).first()
        if f:
            db.session.delete(f)

解读:

  • is_followed_by和is_following()方法分别在左右2边的一对多关系中搜索指定用户,如果找到了就返回true。注意发起查询前,这2个方法都确认了指定的用户有没有id,以防创建了用户,但是尚未提交到数据库。
  • follow()方法手动把Follow实例插入关联表,从而把关注者和被关注者联系起来,并让应用有机会设定自定义字段,最后把这个实例对象添加到数据库会话中。
  • 注意,timestamp字段无需手动设定,因为定义字段时指定了默认值,即当前日期和时间。
  • un_follow()方法使用followed关系找到连接用户和被关注用户的Follow实例。若想销毁这两个用户之间的连接,只需删除这个Follow对象即可。

四. 编写新增数据库关系的单元测试

class UserModelTestCase(unittest.TestCase):
    ...
        def test_follows(self):
        u1 = User(email='[email protected]', password='cat')
        u2 = User(email='[email protected]', password='dog')
        db.session.add(u1)
        db.session.add(u2)
        db.session.commit()
        self.assertFalse(u1.is_following(u2))
        self.assertFalse(u1.is_followed_by(u2))
        timestamp_before = datetime.datetime.utcnow()
        u1.follow(u2)
        db.session.add(u1)
        db.session.commit()
        timestamp_after = datetime.datetime.utcnow()
        self.assertTrue(u1.is_following(u2))
        self.assertFalse(u1.is_followed_by(u2))
        self.assertTrue(u2.is_followed_by(u1))
        self.assertTrue(u1.followed.count() == 2)
        self.assertTrue(u2.followers.count() == 2)

        new_followed = u1.followed.all()[-1]
        self.assertTrue(new_followed.followed == u2)
        self.assertTrue(timestamp_before <= new_followed.timestamp <= timestamp_after)
        new_follower = u2.followers.all()[-1]
        self.assertTrue(new_follower.follower == u1)

        u1.un_follow(u2)
        db.session.add(u1)
        db.session.commit()
        self.assertTrue(u1.followed.count() == 1)
        self.assertTrue(u2.followers.count() == 1)
        self.assertTrue(Follow.query.count() == 2)
        u2.follow(u1)
        db.session.add(u1)
        db.session.add(u2)
        db.session.commit()
        db.session.delete(u2)
        db.session.commit()
        self.assertTrue(Follow.query.count() == 1)
发布了132 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Geroge_lmx/article/details/103445976