Skip to main content

A Flask serializer built with marshmallow and flask-sqlalchemy

Project description

Flask-Serializer

一个帮助你快速书写Restful的序列化器工具

1. 简介

后端程序员, 最基础也是最常做的事情就是定义数据库模型并进行增删改查, 而在一个Restful接口集合中, 对资源进行增删改查的也离不开参数的校验.

从Json校验到持久化成数据库记录, 这个过程被我们成为反序列化(狭义), 而从数据库表到Json字符串, 这个过程我们成为序列化(狭义).

本软件就是这样一个序列化工具, 它旨在让反序列化和反序列化更加快捷和方便, 让我们更关注业务逻辑(而不是参数校验和增删改查).

2. 安装说明

需求:

flask-serializer 支持Python >= 2.7的版本.

python2.7: 使用Marshmallow2

python 3: 使用Marshmallow3

安装:

pip install flask-serializer

3. 使用

示例代码可以看这里

如果你已经十分熟悉了marshmallow的使用, 你可以直接跳过3.3

3.1 初始化

如同其他的flask插件, flask-serializer的初始化也很简单;

注意: 由于依赖flask-SQLAlchemy, flask-serializer应该在其之后进行初始化

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_serializer import FlaskSerializer

app = Flask(__name__)

app.config["SQLALCHEMY_DATABASE_URI"] = 'postgresql://postgres@localhost:5432/test'

db = SQLAlchemy(app)
session = db.session

fs = FlaskSerializer(app, strict=False)

keyword arguments 将会转换为Marshmallow的class Meta, 详细看这里

然后, 这样定义一个schema:

class BaseSchema(fs.Schema):
    pass

3.2. 准备

我们设计一系列模型:

  1. 模型基类, 提供所有模型的通用字段

    now = datetime.datetime.now
    
    class Status:
        VALID = True
        INVALID = False
    
    class BaseModel(db.Model):
        __abstract__ = True
    
        id = Column(INTEGER, primary_key=True, autoincrement=True, nullable=False, comment=u"主键")
        is_active = Column(BOOLEAN, nullable=False, default=Status.VALID)
        create_date = Column(DATE, nullable=False, default=now)
        update_date = Column(DATE, nullable=False, default=now, onupdate=now)
    
        def delete(self):
            self.is_active = Status.INVALID
            return self.id
    
        def __repr__(self):
            return f"<{self.__class__.__name__}:{self.id}>"
    
  2. 订单模型

    class Order(BaseModel):
        __tablename__ = "order"
        order_no = Column(VARCHAR(32), nullable=False, default=now, index=True)
    
        order_lines = relationship("OrderLine", back_populates="order")
    
  3. 订单明细行, 与订单模型是多对一的关系, 记录了该订单包含的商品数量价格等信息

    class OrderLine(BaseModel):
        __tablename__ = "order_line"
        order_id = Column(ForeignKey("order.id", ondelete="CASCADE"), nullable=False)
        product_id = Column(ForeignKey("product.id", ondelete="RESTRICT"), nullable=False)
    
        price = Column(DECIMAL(scale=2))
        quantities = Column(DECIMAL(scale=2))
    
        order = relationship("Order", back_populates="order_lines")
    
        @property
        def total_price(self):
            return self.price * self.quantities
    
  4. 商品模型, 与订单明细行是一对多的关系, 记录了商品的基本属性

    class Product(BaseModel):
        __tablename__ = "product"
    
        product_name = Column(VARCHAR(255), index=True, nullable=False)
        sku_name = Column(VARCHAR(64), index=True, nullable=False)
        standard_price = Column(DECIMAL(scale=2), default=0.0)
    

3.3. 简单的Marshmallow演示

更加高级的使用技巧, 请看: Marshmallow文档

3.2.1. 反序列化

  1. 假设我们现在要创建一条数据库记录, 创建一个schema来验证数据

    from marshmallow import Schema, fields
    
    class ProductSchema(Schema):
        product_name = fields.String(required=True)
        sku_name = fields.String(required=True)
        standard_price = fields.Float()
    

    我们可以这样做

    raw_data = {
        "product_name": "A-GREAT-PRODUCT",
        "sku_name": "GP19930916",
        "standard_price": 100 ,
    }
    
    ps = ProductSchema()
    
    instance_data = ps.validate(raw_data)  # marshmallow2 will return (data, error) tuple
    
    product = Product(**instance_data)
    
    session.add(product)
    session.flush()
    session.commit()
    
  2. 或者使用marshmallow自带的post_load方法

    from marshmallow import Schema, fields, post_load
    
    class ProductSchema(Schema):
        product_name = fields.String(required=True)
        sku_name = fields.String(required=True)
        standard_price = fields.Float()
    
        @post_load
        def make_instance(data, *args, **kwargs):
            # data是通过验证的数据
            product = Product(**data)
            session.add(product)
            session.commit()
            session.flush()
            return product
    

    然后

    raw_data = {
        "product_name": "A-GREAT-PRODUCT",
        "sku_name": "GP19930916",
        "standard_price": 100 ,
    }
    
    ps = ProductSchema()
    
    product_instance = ps.load(raw_data)
    

3.1.2. 序列化

至于序列化, 也可以使用ProductSchema实例进行处理, 如:

  1. 序列化, 只会取非load_only的字段进行序列化

    product_instance = session.query(Product).get(1)
    data = ps.dump(product_instance)  # dumps will return json string; marshmallow2 will return (data, error) tuple
    
  2. 也可以定义一些dump_only的filed用于序列化

    class ProductSchemaAddDumpOnly(ProductSchema):
        id = fields.Integer(dump_only=True)
        create_date = fields.DateTime(dump_only=True)
        update_date = fields.DateTime(dump_only=True)
        is_active = fields.Boolean(dump_only=True)
    
    ps_with_meta = ProductSchemaAddDumpOnly()
    data = ps_with_meta.dump(product_instance)
    

序列化可以直接使用marshmallow方法, 这里我们主要介绍反序列化方法

3.4 使用DetailMixin进行反序列化

上面我们看到, 第二种方法还是比较Nice的(官网文档中也有事例), 他直接使用了marshmallow post_load方法, 对结果进行后处理, 得到一个Product对象, 实际上DetailMix就是实现了这样方法的一个拓展类.

  1. 使用DetailMixin进行模型创建:

    很简单, 导入DetailMixIN后使得刚才的ProductSchema继承DetailMixIN, 然后为添加__model__到类中, 设置这个Schema需要绑定的对象.

    from marshmallow import Schema, fields
    
    from flask_serializer.mixins.details import DetailMixin 
    
    class BaseSchema(fs.Schema):
        id = fields.Integer()
        create_date = fields.DateTime(dump_only=True)
        update_date = fields.DateTime(dump_only=True)
        is_active = fields.Boolean(dump_only=True)
    
    class ProductSchema(DetailMixin, BaseSchema):
        __model__ = Product
    
        product_name = fields.String(required=True)
        sku_name = fields.String(required=True)
        standard_price = fields.Float()
    
    raw_data = {
        "product_name": "A-GREAT-PRODUCT",
        "sku_name": "GP19930916",
        "standard_price": 100,
    }
    
    ps = ProductSchema()
    product_instance = ps.load(raw_data)
    session.commit()
    
    <Product:1>
    

    注意: DetailMixin 会调用flush()方法, 除非session开启了autocommit, 否则不会提交你的事务(autocommit也是新创建了一个子事务, 不会提交当前主事务), 请开启flask_sqlalchemy的自动提交事务功能或者手动提交

__model__说明: 如果有导入问题, __model__支持设置字符串并在稍后的代码中自动读取SQLAlchemy的metadata并且自动设置对应的Model类

class ProductSchema(DetailMixin, Schema):
    __model__ = "Product"
  1. 使用DetailMixin进行模型更新

    既然有创建就有更新, DetailMixin能够自动读取__model__里面的主键(前提是model主键必须唯一), 当在读取到原始数据中的主键时, load方法会自动更新而不是创建这个模型. 当然, 也不要忘记在schema中定义你的主键字段.

    raw_data = {
        "id": 1,
        "standard_price": 10000000,
    }
    
    ps = ProductSchema(partial=True)  # partial参数可以使得required的字段不进行验证, 适合更新操作
    
    product_instance = ps.load(raw_data)
    session.commit()
    
    <Product:1>
    

    如果只是想读取这个模型, 而不想更新, 只需要传入主键值行就行

    TODO: 以后可以加入ReadOnlyDetailMixIN

还有一些其他的特性, 我们在进阶中再看, 配合上SQLAlchemy的relationship, 还可以实现更多.

3.5 使用ListMixin进行查询

DetailMixin支持的是增改操作(实际上也支持删除, 但未来需要添加专门用来删除的Mixin), 而ListMixin支持查询的操作.

下面是不同的ListMixin的使用

3.5.1 ListModelMixin

ListModelMixin 顾名思义是针对某个模型的查询, 其反序列化的结果自然是模型实例的列表

为了让用户的输入能够转化成我们想要的查询, 这里使用Filter对象作为参数filter传入Field的初始化中

  1. 基本使用

    from flask_serializer.mixins.lists import ListModelMixin
    from sqlalchemy.sql.operators import eq as eq_op
    
    class ProductListSchema(ListModelMixin, BaseSchema):
        __model__ = Product
    
        product_name = fields.String(filter=Filter(eq_op))
    

    此时, 我们接口接收到输入的参数, 我们这样:

    raw_data = {
        "product_name": "A-GREAT-PRODUCT",
    }
    
    pls = ProductListSchema()
    
    product_list = pls.load(raw_data)
    
    Traceback (most recent call last):
    ....
    marshmallow.exceptions.ValidationError: {'_schema': ['分页信息错误, 必须提供limit/offset或者page/size']}
    

    阿偶, 报错了, 实际上, ListModelMixin中会去自动检查Limit/Offset或者Page/Size这样的参数, 如果你不想让数据库爆炸, 可别忘记传入这两个参数!

    raw_data["page"] = 1
    raw_data["size"] = 10
    product_list = pls.load(raw_data)
    
    [<Product:1>]
    
  2. 排序*

    如果想使用排序, 可以重写这一个方法

    class ProductListSchema(ListModelMixin, BaseSchema):
        __model__ = Product
    
        product_name = fields.String(filter=Filter(eq_op))
    
        def order_by(self, data):
            return self.model.update_date.desc()
    

    注意了, self.model可以安全的取到设置的__model__指代的对象, 无论它被设置成字符串还是Model类.

    * 这方方法可能需要重新设计一下, 我们可以将其变成一个属性而不是提供一个可重写的方法, 除非排序非常复杂

3.5.2 Filter类参数说明

  1. operator, 这代表着将要对某一个字段做什么样的操作, 这个参数应该是sqlalchemy.sql.operators下提供的函数, Filter会自动套用这些函数, 将转化成对应的WHERE语句, 上面的例子中, 我们最终得到的SQL就是这样的

    SELECT * FROM product WHERE product_name = 'A-GREAT-PRODUCT' ORDER BY product.update_date DESC
    
  2. field, 如果不设置, 他将默认使用__model__下面的同名Column进行过滤, 所以, 当你的Schema和Model的Filed对不上时, 也可以这样搞

    class ProductListSchema(ListModelMixin, BaseSchema):
        __model__ = Product
    
        name = fields.String(filter=Filter(eq_op, Product.product_name))
    

    这时, 我们的接口文档中还定义的是product_name, Schema将读不到该值, 所以, 接口文档, shecma, model中定义的字段名字可能都不一样, 但是他们指代的同一个东西是, 你还可以这么做:

    class ProductListSchema(ListModelMixin, BaseSchema):
        __model__ = Product
    
        name = fields.String(data_key="product_name", filter=Filter(eq_op, Product.product_name))
    

    data_key是marshmallow自带的参数, 他将告诉Field对象从哪里取值.

    在Marshmallow2中, 这个参数叫load_fromdump_from, 现在合并了, 但实际上好像适用范围变小了.

    同样的, field也可以被设置为字符串, 且可以省略model的名称

    class ProductListSchema(ListModelMixin, BaseSchema):
        __model__ = Product
    
        name = fields.String(data_key="product_name", filter=Filter(eq_op, "product_name"))
    

    对于field参数, 还可以设置为其他模型的Column, 我们放到进阶部分去讲吧

  3. value_process对即将进行查询的值进行处理, 一般情况下用在诸如like的操作上

    value_procee支持传入一个callable对象, 并且只接受一个参数, 返回值该参数的处理.

    from sqlalchemy.sql.operator import like_op
    
    class ProductListSchema(ListModelMixin, BaseSchema):
        __model__ = Product
    
        product_name = fields.String(filter=Filter(eq_op, value_process=lambda x: f"%{x}%"))
    
    raw_data = {
        "product_name": "PRODUCT",
        "limit": 10,
        "offset": 0
    }
    
    pls = ProductListSchema()
    
    product_list = pls.load(raw_data)
    print(product_list)
    
    SELECT * FROM product WHERE product_name LIKE '%PRODUCT%'
    
    [<Product:1>]
    

    事实上, value_process也有默认值, 如果你使用like_op或者ilike_op则会自动在value后面加上%(右模糊匹配)

    其实pre_load装饰器也可以预处理值, 但是我认为不需要写太多了预处理方法

  4. default默认值.

    有时可能会有不传值使用默认值进行过滤的情况, 可以设置default方法.

    这个场景下不能设置marshmallow的Field对象的default参数, 因为这个default是给dump方法用的, 而不是load方法.

    让我们先来删除刚才创建的product

    # delete a product
    for product in product_list:
        product.delete()
    
    session.flush()
    session.commit()
    

    然后我们创建这样一个Schema, 将自动过滤掉软删除的记录

    class ProductListSchema(ListModelMixin, BaseSchema):
        __model__ = Product
    
        is_active = fields.Boolean(filter=Filter(eq_op, default=True))
    
        product_name = fields.String(filter=Filter(eq_op))
    
    
    raw_data = {
        "product_name": "A-GREAT-PRODUCT",
        "limit": 10,
        "offset": 0
    }
    
    pls = ProductListSchema()
    
    print(pls.load(raw_data))
    
    []
    

3.5.3 ListMixin

和ListModelMixin的差别就是这个方法这对一个Model进行全部查询, 而是会对指定的一些字段进行查询, 这样可以避免一些额外的性能开销, 只查询你感兴趣的字段. 并且可以完成跨模型的字段查询.

ListMixin需要一个Query对象来告诉他需要查询的字段

  1. 基本使用:

    from flask_serializer.func_field.filter import Filter
    from flask_serializer.func_filed.query import Query
    from flask_serializer.mixins.lists import ListMixin
    from sqlalchemy.sql.operators import eq as eq_op
    
    class ProductListSchema(ListMixin, BaseSchema):
        __model__ = Product
    
        product_name = fields.String(filter=Filter(eq_op), query=Query())
    

    同样的, 让我们输入参数

    raw_data = {
        "page": 1,
        "size": 10,
        "product_name": "A-GREAT-PRODUCT",
    }
    
    pls = ProductListSchema()
    
    product_list = pls.load(raw_data)
    

    这是时候我们得到的不再是Product的实例列表, 而是sqlalchemy.util._collections.result对象, 这种数据结构有一点像具名元组, 可以进行下标索引和.操作, 但是他只包含你查询的字段, 不包含任何其他多余的字段, 因此:

    product = product_list[0]  # 如果没有的话记得新建一条记录哦!
    
    print(product.product_name)
    print(product[0])
    
    A-GREAT-PRODUCT
    A-GREAT-PRODUCT
    

3.5.4 Query的参数说明

  1. field

    可以是一个SQLAlchemy的Column对象, 也可以是能够被正确指向Column的字符串. 这个参数将会告诉Query查询的字段到底是什么, 如果不填写则直接使用当前field的名称对应__model__字段进行查询.

    其实field完全可以设置另外一个模型的字段, 如果这两个模型之间有外键的关联, SQLAlchemy会自动为我们拼接上Join语句, 并且加上正确的On条件, 如果这两个模型没有直接外键的关联, 也可以重写def modify_before_query(self, query, data)方法来增加自己的Join条件, 我们放到高级部分去讲解.

  2. label

    label参数相当于SQL语句中的AS

    class ProductListSchema(ListMixin, BaseSchema):
        __model__ = Product
    
        product_name = fields.String(filter=Filter(eq_op), query=Query(label="name"))
    
    pls = ProductListSchema()
    
    product = pls.load(raw_data)[0]
    
    print(product.name)
    
    product.product_name # raise a AttributeError
    
    A-GREAT-PRODUCT
    
    Traceback (most recent call last):
    File xxxxxx
        print(product.product_name)
    AttributeError: 'result' object has no attribute 'product_name'
    

4 进阶

3.6.1 结合Nest和relationship完成骚操作

3.6.2 外键检查

3.6.3 联合过滤

已知问题

  1. DetailMixin不能兼容sqlite, sqlite不支持批量更新

TODO

  1. 可以读取Model中的Column, 根据Column自动生成Field.

  2. JsonSchema自动转换成Marshallmallow-Schema.

  3. DeleteMixIN, 支持批量删除的Serializer.

  4. ...

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

flask_serializer-0.0.5.1.tar.gz (23.6 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page