Flask学习之十一——关注者

1. 关注者功能在数据库中的实现

一对多关系是最常用的关系类型,它把一个记录和一组相关的记录联系在一起。实现这种关系时,要在“多”这一侧加入一个外键,指向“一”这一侧联接的记录。比如在User中指定外键,指向Role(多个User为一个Role)

大部分的其他关系类型都可以从一对多类型中衍生。多对一关系从“多”这一侧看,就是一对多关系。一对一关系类型是简化版的一对多关系,限制“多”这一侧最多只能有一个记录。唯一不能从一对多关系中简单演化出来的类型是多对多关系。

多对多关系

下面以一个典型的多对多关系为例,即一个记录学生和他们所选课程的数据库。很显然,你不能在学生表中加入一个指向课程的外键,因为一个学生可以选择多个课程,一个外键不够用。同样,你也不能在课程表中加入一个指向学生的外键,因为一个课程有多个学生选择。两侧都需要一组外键。

这种问题的解决方法是添加第三张表,这个表称为关联表。这样,多对多关系可以分解成原表和关联表之间的两个一对多关系。

这个例子中的关联表是 registrations,表中的每一行都表示一个学生注册的一个课程。

查询多对多关系要分成两步。若想知道某位学生选择了哪些课程,你要先从学生和注册之间的一对多关系开始,获取这位学生在 registrations 表中的所有记录,然后再按照多到一的方向遍历课程和注册之间的一对多关系,找到这位学生在 registrations 表中各记录所对应的课程。

若想找到选择了某门课程的所有学生,你要先从课程表中开始,获取其在 registrations 表中的记录,再获取这些记录联接的学生。

像上面这样的简单多对多关系,SQLAlchemy 就可以完成大部分操作

registrations = db.Table(‘registrations‘,
    db.Column(‘student_id‘, db.Integer, db.ForeignKey(‘student.id‘)),
    db.Column(‘class_id‘, db.Integer, db.ForeignKey(‘classes.id‘))
    )

class Student(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    classes = db.relationship(‘Class‘,
                            secondary=registrations,
                            backref=db.backref(‘students‘, lazy=‘dynamic‘),
                            lazy=‘dynamic‘)

class Class(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)

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

假设学生是 s,课程是 c 。
则学生注册课程:

>>> s.classes.append(c)
>>> db.session.add(s)

列出 s 注册的课程, 以及 c 的学生:

>>> s.classes.all()
>>> c.students.all()

学生取消课程:

>>> s.classes.remove(c)

自引用关系

在学生和课程的例子中,关联表联接的是两个明确的实体。
但是,表示用户关注其他用户时,只有用户一个实体,没有第二个实体。

如果关系中的两侧都在同一个表中,这种关系称为自引用关系。在关注中,关系的左侧是用户实体,可以称为follower。关系的右侧也是用户实体,但这些是followed。

关联表为follows,其中每一行都表示一个用户关注另一个用户。
图中左边表示的一对多关系把用户和 follows 表中的一组记录联系起来,用户是follower
图中右边表示的一对多关系把用户和 follows 表中的一组记录联系起来,用户是followed

高级多对多关系

用前一节介绍的自引用多对多关系可在数据库中表示用户之间的关注,但却有个限制。
使用多对多关系时,往往需要存储所联两个实体之间的额外信息。对用户之间的关注来说,可以存储用户关注另一个用户的日期,这样就能按照时间顺序列出所有关注者。这种信息只能存储在关联表中,但是在之前实现的学生和课程之间的关系中,关联表完全是由SQLAlchemy 掌控的内部表。为了能在关系中处理自定义的数据,我们必须提升关联表的地位,使其变成程序可访问的模型。

app/models/user.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)

app/models/user.py: 使用两个一对多关系实现的多对多关系
class User(UserMixin, db.Model):
    # ...
    followed = db.relationship(‘Follow‘,
                                foreign_keys=[Follow.follower_id],
                                backref=db.backref(‘follower‘, 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‘)

这里的逻辑关系是,如果用户A关注了100个用户,调用user.followed.all() 后会返回一个列表其中包含100个Follow实例,每个实例的follower和followed回引属性都指向相应的用户。(follower指向用户A,followed指向用户A关注的用户)

为了消除外键间的歧义,定义关系时必须使用可选参数foreign_keys指定的外键

如果把lazy 设为默认值 select,那么首次访问 follower 和 followed 属性时才会加载对应的用户,而且每个属性都需要一个单独的查询,这就意味着获取全部被关注用户时需要增加 100 次额外的数据库查询。
而设定为 lazy=‘joined‘ 模式,就可在一次数据库查询中完成这些操作。

设为 all, delete-orphan 的意思是启用所有默认cascade选项,而且还要删除孤儿记录。

cascade 配置在父对象上执行的操作对相关对象的影响。
cascade 的默认值能满足大多数情况的需求,但对这个多对多关系来说却不合用。删除对象时,默认的cascade是把对象联接的所有相关对象的外键设为空值。但在关联表中,删除记录后正确的行为应该是把指向该记录的实体也删除,因为这样能有效销毁联接。这就是cascade选项delete-orphan 的作用。

app/models.py:在模型中添加follow, unfollow 功能

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

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

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

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

2. 在资料页中显示关注者

app/templates/user.html: 添加关注信息的用户信息页模板

{% if current_user.can(Permission.FOLLOW) and user != current_user %}
    {% if not current_user.is_following(user) %}
    <a href="{{ url_for(‘.follow‘, username=user.username) }}"
        class="btn btn-primary">Follow</a>
    {% else %}
    <a href="{{ url_for(‘.unfollow‘, username=user.username) }}"
        class="btn btn-default">Unfollow</a>
    {% endif %}
{% endif %}
<a href="{{ url_for(‘.followers‘, username=user.username) }}">
    Followers: <span class="badge">{{ user.followers.count() }}</span>
</a>
<a href="{{ url_for(‘.followed_by‘, username=user.username) }}">
    Following: <span class="badge">{{ user.followed.count() }}</span>
</a>
{% if current_user.is_authenticated() and user != current_user and
    user.is_following(current_user) %}
| <span class="label label-default">Follows you</span>
{% endif %}

app/main/views.py: follow路由

@main.route(‘/follow/<username>‘)
@login_required
@permission_required(Permission.FOLLOW)
def follow(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        flash(‘Invalid user‘)
        return redirect(url_for(‘.index‘))
    if current_user.is_following(user):
        flash(‘You are already following this user‘)
        return redirect(url_for(‘.user, username=username‘))
    current_user.follow(user)
    flash(‘You are now following %s.‘ % username)
    return redirect(url_for(‘.user‘, username))

app/main/views.py: followers路由

@main.route(‘/followers/<username>‘)
def followers(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        flash(‘Invalid user‘)
        return redirect(url_for(‘.index‘))
    page = request.args.get(‘page‘, 1, type=int)
    pagination = user.followers.paginate(
        page, per_page=current_app.config[‘FLASKY_FOLLOWERS_PER_PAGE‘],
        error_out=False)
    followss = [{‘user‘: item.follower, ‘timestamp‘: item.timestamp}
                for item in pagination.items]
    return render_template(‘followers.html‘, user=user, title=‘Followers of‘,
                            endpoint=‘.followers‘, pagination=pagination, follows=follows)

3. 使用数据库联结查询所关注用户的文章

想显示所关注用户发布的所有文章,第一步显然先要获取这些用户,然后获取各用户的文章,再按一定顺序排列,写入单独列表。可是这种方式的伸缩性不好,随着数据库不断变大,生成这个列表的工作量也不断增长,而且分页等操作也无法高效率完成。获取博客文章的高效方式是只用一次查询。
完成这个操作的数据库操作称为联结。联结操作用到两个或更多的数据表,在其中查找满足指定条件的记录组合,再把记录组合插入一个临时表中,这个临时表就是联结查询的结果。

如下示例

若想获得 susuan 所关注用户发布的文章,就要合并 posts 表和 follows 表。首先过滤follows 表,只留下关注者为 susuan 的记录,即上面表中的最后两行。然后过滤 posts 表,留下 author_id 和过滤后的 follows 表中 followed_id 相等的记录,把两次过滤结果合并,组成临时联结表,这样就能高效查询 susuan 所关注用户的文章列表。

app/models.py: 获取所关注用户的文章

class User(db.Model):
    # ...
    @property
    def followed_posts(self):
        return Post.query.join(Follow, Follow.followed_id == Post.author_id)             .filter(Follow.follower_id == self.id)

 

 4. 在首页显示所关注用户的文章

app/main/views.py: 显示所有文章或只显示所关注用户的文章

@app.route(‘/‘, methods=[‘GET‘, ‘POST‘])
def index():
    # ...
    show_followed = False
    if current_user.is_authenticated():
        show_followed = bool(request.cookies.get(‘show_followed‘, ‘‘))
    if show_followed:
        query = current_user.followed_posts
    else:
        query = Post.query
    pagination = query.order_by(Post.timestamp.desc()).paginate(
        page, per_page=current_app.config[‘FLASKY_FOLLOWERS_PER_PAGE‘],
        error_out=False)
    posts = pagination.items
    return render_template(‘index.html‘, form=form, posts=posts,
                            show_followed=show_followed, pagination=pagination)

app/main/views.py: 设定 show_followed cookie

@main.route(‘/all‘)
@login_required
def show_all():
    resp = make_response(redirect(url_for(‘.index‘)))
    resp.set_cookie(‘show_followed‘, ‘‘, max_age=30*24*60*60)
    return resp

@main.route(‘/followed‘)
@login_required
def show_followed():
    resp = make_response(redirect(url_for(‘.index‘)))
    resp.set_cookie(‘show_followed‘, ‘1‘, max_age=30*24*60*60)
    return resp

添加功能: 用户查看好友文章时能看到自己的文章。最简单的解决办法是,注册时把用户设为自己的关注者

app/models.py:构建用户时把用户设为自己的关注者

class User(UserMixin, db.Model):
    # ...
    def __init__(self, **kwargs):
        # ...
        self.follow(self)

现在的数据库中可能已经创建了一些用户,而且都没有关注自己。如果数据库还比较小,容易重新生成,那么可以删掉再重新创建。如果情况相反,那么正确的方法是添加一个函数,更新现有用户。

class User(UserMixin, db.Model):
    # ...
    @staticmethod
    def add_self_follows():
        for user in User.query.all():
            if not user.is_following(user):
                user.follow(user)
                db.session.add(user)
                db.session.commit()

可以在shell中运行该函数来更新数据库

(venv) $ python manage.py shell
>>> User.add_self_follows()

2015-05-27

时间: 2024-10-03 02:11:06

Flask学习之十一——关注者的相关文章

Flask学习之十一 邮件支持

英文博客地址:blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xi-email-support 中文翻译地址:http://www.pythondoc.com/flask-mega-tutorial/email.html 开源中国社区:http://www.oschina.net/translate/the-flask-mega-tutorial-part-xi-email-support 对于我们这个应用,我们可能想要有这样的

初探swift语言的学习笔记十一(performSelector)

作者:fengsh998 原文地址:http://blog.csdn.net/fengsh998/article/details/35842441 转载请注明出处 如果觉得文章对你有所帮助,请通过留言或关注微信公众帐号fengsh998来支持我,谢谢! 在OC中使用好好的performSelector,但不知为什么在swift有意的被拿掉了.更有甚者连IMP, objc_msgSend也不能用了.虽然想不通为什么,但应该有他的道理.就不纠结了. 大家可能在OC中使用得更多的就是延时处理,及后台处

Docker学习(十一)Docker系列结束-新的开始K8S

Docker学习(十一)Docker系列结束-新的开始K8S 标签(空格分隔): docke k8s Docker系列结束 上一篇讲到使用docker官方提供的容器编排工具docker-compose,但是docker-compose强调的是单机机进行容器编排,使用起来比较受限,对于一些大公司,应用不是部署在一台机器上的,在这种情况下,Docker-compose就无能为力了,在这样的情况下,需要使用集群容器编排工具,比如,google的Kubernetes(k8s),官方提供的Docker S

《Hibernate学习笔记十一》:树状结构设计

<Hibernate学习笔记十一>:树状结构设计 这是马士兵老师讲解Hibernate的一个作业题,树状结构设计,这是一个比较典型的例子,因此有必要写篇博文记录下. 树状结构的设计,它是在同一个类中使用了多对一(ManyToOne)和一对多(OneToMany). 在完成这个题目我们应该按照如下的步骤进行: 1.先思考数据库的模型应该是什么样的?? 数据库中的模型应该如下:即存在id p_id 2.思考面向对象的模型,及如何来进行映射??? 根据数据库中表的特点,对象应该有id name;由于

Flask 学习(四)静态文件

Flask 学习(四)静态文件 动态 web 应用也需要静态文件,一般是 CSS 和 JavaScript 文件.理想情况下你的服务器已经配置好提供静态文件的服务. 在开发过程中, Flask 也能做好这个工作. 静态文件引用 我们先来看下普通的 html 引用静态文件,如 css(js也同样,就不多加示例了),以下为一简单实例,直接打开html: flask 处理 —— static 若直接将该html 当成 flask 模板,相对路径自然就失效了,静态文件将不会被成功读取. 那在flask中

第十七篇:实例分析(4)--初探WDDM驱动学习笔记(十一)

感觉有必要把 KMDDOD_INITIALIZATION_DATA 中的这些函数指针的意思解释一下, 以便进一步的深入代码. DxgkDdiAddDevice 前面已经说过, 这个函数的主要内容是,将BASIC_DISPLAY_DRIVER实例指针存在context中, 以便后期使用, 支持多实例. DxgkDdiStartDevice 取得设备信息, 往注册表中加入内容, 从POST设备中获取FRAME BUFFER以及相关信息(DxgkCbAcquirePostDisplayOwnershi

#HTTP协议学习# (十一)理解HTTP幂等性

在httpcomponent 文档中看到如下段落: 1.4.1. HTTP transport safety It is important to understand that the HTTP protocol is not well suited to all types of applications. HTTP is a simple request/response oriented protocol which was initially designed to support s

【转】MYSQL入门学习之十一:触发器的基本操作

转载地址:http://www.2cto.com/database/201212/176781.html 触发器是MySQL响应以下任意语句而自动执行的一条MySQL语句(或位于BEGIN和END语句之间的一组语句):  www.2cto.com DELETE: INSERT: UPDATE: 使用触发器,需要MySQL5或之后的版本支持. 一.触发器基本操作 1.创建触发器 创建触发器时,需要给出4条信息: 唯一的触发器名:(虽然MySQL5允许不同的表上的触发器名称相同,但一般最好不要这么做

[ZHUAN]Flask学习记录之Flask-SQLAlchemy

From: http://www.cnblogs.com/agmcs/p/4445583.html Flask-SQLAlchemy库让flask更方便的使用SQLALchemy,是一个强大的关系形数据库框架,既可以使用orm方式操作数据库,也可以使用原始的SQL命令. Flask-Migrate 是一个数据迁移框架,需要通过Flask-script库来操作. 一.配置Flask-SQLAlchemy 程序使用的数据库地址需要配置在SQLALCHEMY_DATABASE_URI中,SQLALch