Flask 结构简单,源码清晰,我觉得很符合 Python 简洁易上手的特点。现在这是我最喜欢的一个 Python Web Framework,目前正在尝试基于 Flask 从小到大,边实现功能边扩展架构,看它对实际业务的支撑情况如何。在尝试过程中,将获得的一些经验在本页面记录和分享。

1.  Model 部分(SQLAlchemy 为主)

1.1  sqla 如何给 Model 设定默认值

办法1:给这个 Model 所属的每一个实例,在进行初始化的时候给对应属性赋予初始值。

class SomeModel(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    valid = db.Column(db.Boolean, default=False)    # 默认值总是 False

方法2:在数据库创建数据库表的时候,在对应数据列标记默认值。这样即使不通过 Python 访问这个数据库,数据列也受这一默认值控制。

class SomeModel(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    valid = db.Column(db.Boolean, server_default='0')    # 默认值总是 0,无论是何种数据类型,在这里都要写成字符串格式,类似在写 SQL 语句。

方法3:利用 sqla 的数据库事件监听。例如以下代码将 SomeModel 的主键自增起始位置设为 1234 。

event.listen(
    SomeModel.__table__,
    "after_create",
    DDL("ALTER TABLE %(table)s AUTO_INCREMENT = 1234;").execute_if(dialect=('postgresql', 'mysql'))
)

1.2  sqla 的 sqlalchemy.exc.AmbiguousForeignKeysError 错误

主要由于两个 Model 间有多个 Foreign Key 关联,因此 sqla 无法自动猜出具体 relationship 是使用哪个 Foreign Key,因此需要在 relationship 的 foreign_keys 参数中显示指定。具体参考 sqlalchemy foreign key relationship attributes

1.3  两数据表相互有外键指向对方,插入新数据死锁问题(sqlalchemy.exc.CircularDependencyError: Circular dependency detected)

当两个表都有外键指向对方时(例如图片表记录图片的拥有用户,用户表又需要一个头像而指向了这个图片表),插入数据就会出现外键死锁——都要求对方的数据先行准备好,才能在自己的表中插入新行。这就需要先在其中一个表中插入数据,然后在另一个表的数据中插入完成插入后,再通过 update 语句来更新第一个表的外键取值。

这个机制在 sqla 中已经有官方的办法处理,可参考 Rows that point to themselves / Mutually Dependent Rows 以及 Mutable Primary Keys / Update Cascades 章节中的描述。

另外可参考 sqlalchemy.exc.CircularDependencyError: Circular dependency detected 的讨论。

2.  组件使用经验

2.1  Flask-Admin 使用经验

浮点数(Float)精度不够怎么办?

Flask-Admin 对数据表 Model 中的 Float 类型,在编辑 Form 中默认只保留小数点之后2位,这经常是不够用的,比如用来保存经纬度的时候损失的精度就会比较严重。解决这个问题有以下几个方面需要考虑:

  1. Float 类型在有些数据库本身精度就比较差,比如 MySQL 下大约只有小数点后4位(二进制小数其实我这个描述不准确,具体请查询相关文档)。因此首先要在 model 中选择足够高的数据存储格式。比如如果想让 MySQL 使用 Double 类型的数据列,可以在 SQLAlchemy 的 Model 里面这样写:
    from flask import Flask
    from flask.ext.sqlalchemy import SQLAlchemy

    app = Flask(__name__)
    db = SQLAlchemy(app)
    db.app = app
    db.init_app(app)

    class City(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        longitude = db.Column(db.REAL)
        latitude = db.Column(db.REAL)
  2. 完成了上一步,会发现在 Flask-Admin 表单里提交的高精度浮点数可以被正确保存到数据库中,但点击进入编辑状态时,原先高精度的数值还是会被截断成小数点之后2位。因此我们还得调整 Flask-Admin 的表单生成参数,如下一步。
  3. 查看 Flask-Admin 的源代码,在 flask-admin.contrib.sqla.form.AdminModelConverter 中有这么一个函数
        @converts('Numeric', 'Float')
        def handle_decimal_types(self, column, field_args, **extra):
            places = getattr(column.type, 'scale', 2)
            if places is not None:
                field_args['places'] = places
            return fields.DecimalField(**field_args)
    从中我们可以看明白它会根据 column.type 的 scale 属性的取值来确定如何在表单中截断。查看 SQLAlchemy 文档,我们会看到 type.Float, type.REAL 构造函数都没有 scale 参数,只有 type.Numeric 有。如果在 Model 中直接使用 Numeric 类型(并指定 scale 参数),能解决这个问题,但其在数据库和 Python 对象中都是对应 decimal 类型,性能会有数倍差距。因此我们封装一下 type.REAL ,给它一个默认的 scale 取值。示例代码如下:
    class MyReal(db.REAL):
        scale = 10

    class City(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        longitude = db.Column(MyReal)
        latitude = db.Column(MyReal)
    这样就会让经纬度字段在表单编辑时都支持小数点后10位,通常差不多够用了。

如何给特定字段增加额外的验证检查逻辑

可以利用 ModelView 的 form_args 参数来添加,详细参考问答 Flask-Admin ModelView custom validation?

2.2  Flask-RESTful 使用经验

强制以 utf-8 而不是 Unicode 字符输出

Flask-RESTful 在以 json 格式输出数据时,会把 Unicode 原样输出,而不会自动做编码转换。就可能导致以其他编程语言实现的客户端在数据解析时出现困难。

可参考 Unicode in Flask-Restful API and JSON issue 讨论中的答案解决这个问题。

用 api 签名保护 RESTful api

最常见的保护 RESTful api 的方法是启用 HTTP basic authentication,参考 Designing a RESTful API using Flask-RESTful 以及 RESTful Authentication with Flask

数据签名可能可以利用 itsdangerous 这个库。

不过我比较喜欢用 Flask-HmacAuth 来实现 api 的非对称签名保护机制。(这个库的历史不是特别长,可能文档存在一些小 bug ,不过代码也很简短,遇到问题很容易解决。)

3.  其他组件备忘

  • flask-security:提供最基本的登陆、认证界面、注册验证码、密码找回等功能的组件。权限控制可能还得另外弄。
  • Web Service Made Easy (WSME):也是一个创建 Web Service 的支撑库,也提供多协议支持、加密保护等完整的特性。不过我老觉得易用性似乎不如 Flask-RESTful 。。
  • Dash:Flask 的交互式可视化呈现工具,可以用来做数据驾驶舱。
    • Pyxley:似乎是一个类似的工具,评价似乎比 Dash 略弱。

4.  数据接口协议设计思路参考

GlossyBlue theme adapted by David Gilbert
Powered by PmWiki