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)
未完待续