Flask RESTful接口编写总结(二)

Flask RESTful接口编写总结(二)

接着上一篇,我们开始实现所有有关数据库的操作。

由于项目涉及到很多部分的功能,而每一部分都可能会涉及到数据库的交互,所以将model层抽离为一个单独的项目维护,在shared目录下我们创建orm文件夹作为我们所有模型的打包。

———— orm
    |—— model/                     所有模型定义
       |—— __init__.py              定义Base
       |—— status.py                用户
       |—— user.py                  角色
       |—— resource.py              接口信息
       ...
    |—— __init__.py                 from model import *
    |—— config.py                   连接配置(字典)
    |—— connect.py                  数据连接
    |—— mapper.py                   字段值与含义的映射
    |—— mixin.py                    模型公用方法
    |—— utils.py                    工具库
———— orm.py                         打包脚本

首先要设计模型公用的方法,在mixin.py中:

# -*- coding: utf-8 -*-
class ModelMixin(object):
    def __new__(cls, **kwargs):
        """
        创建参数校验
        """
        cls.verify(**kwargs)
        return object.__new__(cls)

    def __setattr__(self, key, value):
        """
        触发参数校验
        """
        self.verify(**{key: value})
        return object.__setattr__(self, key, value)

    @staticmethod
    def verify(**kwargs):
        """
        参数校验逻辑
        """
        pass

我们重写所有模型的__new__方法,确保创建新实例都会调用模型上定义的verify方法。然后重写__setattr__方法,确保所有模型实例上的字段更新也会调用verify方法,定义静态方法verify,以便重写。

然后开始定义模型。要实现类对象到数据库表的映射,必须继承sqlachemy的declarative_base的实例。

在model/__init__.py中:

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

几乎所有的模型都有创建时间,更新时间,创建者,编辑者,删除/未删除状态这几个字段,于是我们定义一个抽象模型,绑定所有公有字段,在model/status.py中:

# -*- coding: utf-8 -*-
from datetime import datetime
from sqlalchemy import Column, DateTime
from sqlalchemy.dialects.mysql import INTEGER
from . import Base
from ..mixin import ModelMixin
from ..mapper import WORKING, DATETIME_FORMAT, STATUS_RANGE


class Status(Base, ModelMixin):
    __abstract__ = True

    creator = Column(INTEGER(11, unsigned=True), nullable=False)
    modifier = Column(INTEGER(11, unsigned=True), nullable=False)
    create_time = Column(DateTime, default=datetime.now().strftime(DATETIME_FORMAT))
    last_update_time = Column(DateTime, default=datetime.now().strftime(DATETIME_FORMAT))
    status = Column(INTEGER(1), default=WORKING)

    @staticmethod
    def verify(**kwargs):
        msg = None
        if kwargs.get('status') and kwargs['status'] not in STATUS_RANGE:
            msg = '状态只能设置为{}'.format(STATUS_RANGE)
        if msg:
            raise Exception(msg)

之后可以定义一个用户模型(model/user.py),为了更好的区分,所有模型类都用Model作为后缀:

# -*- coding: utf-8 -*-
from sqlalchemy import Column, String
from sqlalchemy.dialects.mysql import INTEGER
from status import Status
from ..mapper import ACTIVE


# -*- coding: utf-8 -*-
from sqlalchemy import Column, String
from sqlalchemy.dialects.mysql import INTEGER
from status import Status
from ..mapper import ACTIVE, USERNAME_MIN_LEN, USERNAME_MAX_LEN


class UserModel(Status):
    __tablename__ = 'users'

    id = Column(INTEGER(11, unsigned=True), autoincrement=True, primary_key=True)
    username = Column(String(64), nullable=False)
    password = Column(String(256), nullable=False)
    is_active = Column(INTEGER(1), default=ACTIVE)

    @staticmethod
    def verify(**kwargs):
        Status.verify(**kwargs)

        msg = None
        if kwargs.get('username'):
            l = len(kwargs['username'].decode('utf-8'))
            if l < USERNAME_MIN_LEN:
                msg = '用户名长度不能小于{}'.format(USERNAME_MIN_LEN)
            if l > USERNAME_MAX_LEN:
                msg = '用户名长度不能大于{}'.format(USERNAME_MAX_LEN)
        if msg:
            raise Exception(msg)

在connect.py中:

# -*- coding: utf-8 -*-
from sqlalchemy import create_engine
from sqlalchemy.pool import NullPool
from sqlalchemy.orm import session, scoped_session
from config import mysql_config
from utils import update_config

mysql_config = update_config(mysql_config)

mysql_engine = create_engine(
    "mysql+pymysql://{}:{}@{}:{}/{}".format(
        mysql_config['USERNAME'],
        mysql_config['PASSWORD'],
        mysql_config['HOST'],
        mysql_config['PORT'],
        mysql_config['DATABASE']
    ),
    poolclass=NullPool
)

mysql_factory = session.sessionmaker(bind=mysql_engine, autocommit=False, autoflush=False)
mysql_session = scoped_session(mysql_factory)

模型定义好了,想要像包一样使用,还需要打包脚本,在orm.py中:

# -*- coding: utf-8 -*-
import os
from distutils.core import setup

PACKAGE_NAME = 'orm'

scripts = []


def get_packages(dir, packages=[]):
    for file in os.listdir(dir):
        path = os.path.join(dir, file)
        if os.path.isdir(path) and os.path.exists('{}/__init__.py'.format(path)):
            packages.append(path)
            get_packages(path, packages)
        else:
            scripts.append(path)
    packages.append(dir)
    return [p.replace('/', '.') for p in packages]


packages = get_packages(PACKAGE_NAME)

setup(
    name='orm',
    version='1.0.0',
    description='mysql orm',
    license='No License',
    platforms='python 2.7',
    install_requires=[
        'SQLAlchemy==1.2.15'
    ],
    packages=packages
)

os.system('rm -rf build')

输入 python orm.py install 安装orm,然后就可以直接通过from orm import ... 引入所需的东东了。

然后是创建数据库和表,使用alembic,在根目录下创建migrate文件夹作为数据库迁移管理库:

> export PYTHONPATH=~/Desktop/myflask
> alembic init migrations # 生成配置文件

修改配置文件
alembic.ini 38行:
sqlalchemy.url = mysql+pymysql://root:root@localhost:3306/test

migrations/env.py 19行:
from orm import Base
target_metadata = Base.metadata

> alembic revision --autogenerate -m 'initialize' # 生成迁移脚本
> alembic upgrade head # 执行迁移,生成模型对应的表结构

接下来为了使用数据库,在core创建database.py文件:

from orm.connect import mysql_session
from error import abort
from . import status

session = mysql_session()


def session_commit(close=True):
    try:
        session.commit()
        if close:
            session.close()
    except Exception as e:
        session.rollback()
        abort(status.INTERNAL_SERVER_ERROR, message=e.message)


def session_flush():
    try:
        session.flush()
    except Exception as e:
        session.rollback()
        abort(status.INTERNAL_SERVER_ERROR, message=e.message)

新开一个接口:

class AddUserResource(Resource):
    @req_in({
        'username': _in.String(required=True),
        'password': _in.String(required=True),
        'creator': _in.Integer(required=True),
        'modifier': _in.Integer(required=True)
    }, locations=('json',))
    def post(self, params):
        user = UserModel(
            username=params['username'],
            password=params['password'],
            creator=params['creator'],
            modifier=params['modifier']
        )
        session.add(user)
        session_commit()
        return 'ok'

通过接口post用户数据,成功返回“ok”,然后查看数据库,增加了一条记录。

创建用户没有在用户模型上重写__init__函数定义创建实例的输入输出,一旦缺少构建模型的字段参数只能在执行flush数据库时发现异常,这是很不好的体验,但是自己每个模型手写构造函数,然后重复传递参数,我感觉又是很笨的做法,例如:

def __init__(self, username, password, ..., **kwargs):
    Status.__init__(**kwargs)
    self.username = username
    self.password = password
    ...

只是简单的传递参数,过程并没有多大意义,于是我也想通过序列化的方式完成这一过程。首先不可能手写每一个模型的序列化器,那还不如直接写构造函数,一定要通过某种方法自动生成所有模型的序列化器,我找到了marshmallow_sqlalchemy这个插件实现模型序列化:

在app/core/schema.py中:

from marshmallow import post_load
from marshmallow_sqlalchemy import ModelSchema as _ModelSchema, ModelSchemaOpts as _ModelSchemaOpts
from database import session


class ModelSchemaOpts(_ModelSchemaOpts):
    def __init__(self, meta):
        if not hasattr(meta, 'sql_session'):
            meta.sqla_session = session
        super(ModelSchemaOpts, self).__init__(meta)


class ModelSchema(_ModelSchema):
    OPTIONS_CLASS = ModelSchemaOpts

    @post_load
    def create(self, **data):
        errors = self.validate(data)
        if errors:
            raise Exception(errors)
        return self.opts.model(**data)

    def jsonify(self, obj, **kwargs):
        result, errors = self.dump(obj, **kwargs)
        if errors:
            raise Exception(errors)
        return result


def setup_schema(Base):
    for model_class in Base._decl_class_registry.values():
        if hasattr(model_class, '__tablename__'):
            class Meta(ModelSchema.Meta):
                model = model_class

            schema_class = type('%sSchema' % model_class.__name__,
                                (ModelSchema,), {'Meta': Meta})

            schema_instance = schema_class()
            setattr(model_class, '__schema__', schema_class)
            setattr(model_class, '_jsonify', schema_instance.jsonify)
            setattr(model_class, 'create', schema_instance.create)

然后在database.py中执行setup_schema函数:

from schema import setup_schema

setup_schema(Base)

这样所有模型上都添加上了__schema__,_jsonify,create属性

可以在orm/mixin.py上添加:

@classmethod
def create(cls, **kwargs):
    """
    解决动态设置属性时静态检查警告
    |- app/core/schema.py
        |- setattr(model_class, 'create', schema_instance.create)
    """
    pass

def _jsonify(self, obj):
    """
    解决动态设置属性时静态检查警告
    |- app/core/schema.py
        |- setattr(model_class, '_jsonify', schema_instance.jsonify)
    """
    pass

def jsonify(self):
    """
    模型序列化
    """
    return self._jsonify(self)

之后我们创建user就直接使用UserModel.create(...),序列化模型实例就使用user.jsonify(),非常优雅。如果缺少字段参数或者参数类型出错,在创建实例时就会抛出异常。

更新接口:

class AddUserResource(Resource):
    @req_in({
        'username': _in.String(required=True),
        'password': _in.String(required=True),
        'creator': _in.Integer(required=True),
        'modifier': _in.Integer(required=True)
    }, locations=('json',))
    def post(self, params):
        try:
            user = UserModel.create(
                username=params['username'],
                password=params['password'],
                creator=params['creator'],
                modifier=params['modifier']
            )
            session.add(user)
            session_commit(close=False)
            return user.jsonify()
        except Exception as e:
            abort(status.BAD_REQUEST, message=e.message)

至此,我们完成了接口输入输出到数据库存储的过程。对于完整项目还有以下方面值得考虑:

1、sqlachemy查询优化,记录慢查询
2、处理登录登出
3、生成接口权限,配置角色权限项和权限组,鉴权
4、注册中间件
5、使用blinker实现订阅
6、生成swaggerui调试文档

项目地址:https://github.com/dollphis/myyyyflask (commit: c928dfa693fb9a89b49fd747a547ff3c0a4f802b)

未完待续

你可能感兴趣的:(Flask RESTful接口编写总结(二))