Flaskz介绍1--数据库初始化&常用函数

Flaskz简介

Flaskz[源码] 是Flask和SQLAlchemy ORM的功能扩展,源于项目积累,主要对数据库/API/日志/常用函数等进行了封装和扩展,可以快速灵活的实现各种业务场景和提供API。

通过 pip install flaskz / pip install 'flaskz[ext]' 安装使用。

Flaskz代码目录

                
                    -auth/                  #授权/JWT
                    ---_jws.py              #提供了JSONWebSignatureSerializer和TimedJSONWebSignatureSerializer,用于token生成和校验

                    -ext/                   #扩展功能(需要安装依赖)
                    ---cypher.py/           #提供了RSACipher和AESCipher(基于pycryptodome),用于加密解密
                    ---ssh.py               #ssh操作(基于paramiko)

                    -log/                   #日志
                    ---__init__.py          #提供了flaskz_logger用于系统日志记录

                    -models/                #数据库操作(SQLAlchemy)
                    ---__init__.py          #提供了init_model函数, 用于初始化Model操作
                    ---_base.py             #提供了BaseModelMixin, 扩展&简化了数据库操作
                    ---_model.py            #提供了ModelMixin,在BaseModelMixin的基础上添加了流程&异常处理等功能
                    ---_query_util.py       #提供了query相关功能函数
                    ---_util.py             #提供了ORM常用操作函数

                    -rest/                  #REST API功能
                    ---__init__.py          #提供了多个register函数, 用于自动生成数据模型相关API
                    ---_mgmt.py             #提供了ModelRestManager, 用于设置API控制函数(权限控制/操作日志记录)
                    ---_util.py             #提供了生成API所需常用函数(权限控制/日志记录)

                    -utils/                 #工具函数集合
                    ---_app.py              #flask/celery应用路径&配置
                    ---_cache.py            #应用缓存工具函数(g/app)
                    ---_cls.py              #工具类&对象操作
                    ---_common.py           #通用工具函数(list操作/dict操作/...)
                    ---_request_api.py      #发送API请求工具函数
                    ---_request_args.py     #API请求数据处理工具函数
                    ---_response.py         #API响应工具函数
                    ---_timer.py            #周期&定时执行函数

                    -res_status_codes.py    #API响应code集合
                
            

以_开头的py文件中的函数/变量,其所在package提供了import功能,请通过其package使用

本系列介绍文章主要包括以下内容

  1. 数据库初始化&常用函数
  2. 数据模型扩展类
  3. API封装、访问权限控制和系统日志
  4. 常用函数
  5. 基于Flaskz的管理系统开发模板 Flaskz-admin
  6. 使用手册

开发过程,请遵守开发规范,相关知识点一般在 Flaskz-admin 模板中都可以找到,建议对照着学习。

如果对Flask和SQLAlchemy感兴趣,请参考 FlaskSQLAlchemy

本文主要介绍数据库的初始化和操作的常用方法。

数据库初始化

调用flaskz.models.init_model(flask_app)方法对数据库操作进行初始化,初始化操作会读取配置信息,初始化数据库引擎并进行连接测试。

数据库相关配置

有如下配置属性

  • FLASKZ_DATABASE_URI

    数据库URI,格式为:{db_type}+{db_drive}://{username}:{password}@{hostname}:{port}/{db}, 主要包括数据库地址/账号/密码/端口/驱动等。

    示例:

    • sqlite数据库--sqlite:///D:/PyDev/WorkSpace/flasky/_sqlite/flasky_test.db,Python默认支持sqlite数据库,所以不需要驱动。
    • MySQL数据库--mysql+pymysql://root:123456@10.20.30.60:3306/flasky,需要安装PyMySQL驱动。

    请注意不同数据库可能需要安装对应的驱动,比如PyMySQL或PostgreSQL等

  • FLASKZ_DATABASE_ECHO

    对应SQLAlchemy中create_engine方法的echo参数,如果为True,引擎将记录并打印所有SQL语句, echo功能一般用于开发和测试环境,不适用于线上/产品环境。

  • FLASKZ_DATABASE_POOL_RECYCLE

    单位是,对应SQLAlchemy中create_engine方法的pool_recycle参数,此设置使数据库连接池在经过给定秒数后回收连接。默认为-1,表示不会回收。例如,设置为3600意味着连接将在一小时后循环使用。

    请注意,如果在8小时内未检测到任何活动,MySQL默认会自动断开连接。

    示例:FLASKZ_DATABASE_POOL_RECYCLE = int(timedelta(hours=2).total_seconds()) # 1小时以后刷新数据库连接

  • FLASKZ_DATABASE_POOL_PRE_PING

    对应SQLAlchemy中create_engine方法的pool_pre_ping参数,如果为True,每次和数据库交互之前先测试连接是否可用,如果不可用会重连(HA/数据库重启),虽然有性能损耗,但可以增加系统的健壮性。

  • FLASKZ_DATABASE_ENGINE_KWARGS

    engine自定义参数,可以根据需求和场景自定义参数设置,例如){'pool_timeout': 20, 'pool_size': 20, "poolclass": QueuePool, 'max_overflow': 20}

  • FLASKZ_DATABASE_SESSION_KWARGS

    session自定义参数,可以根据需求和场景自定义参数设置,例如){'expire_on_commit': False}

  • FLASKZ_DATABASE_DEBUG

    是否启用调试模式,如果为True则会对数据库的每次操作进行记录(执行时间/SQL语句等),主要用于性能调优,debug模式下slow_time和access_times才会起作用

  • FLASKZ_DATABASE_DEBUG_SLOW_TIME

    单位是毫秒,如果数据库的单次操作时间大于指定的slow_time,则会以warning日志的方式将其打印,主要用于数据库调优操作,如果值<=0,则不启用。

    示例:FLASKZ_DATABASE_DEBUG_SLOW_TIME = 200 # 如果单次SQL执行时间大于200ms,则将其打印。

  • FLASKZ_DATABASE_DEBUG_ACCESS_TIMES

    一次API请求处理过程中,如果与数据库的交互操作次数大于指定的access_times,则会以日志的方式将次数打印,主要用于优化数据库模型(比如调整relationship的lazy等),如果值<=0,则不启用。

    示例:FLASKZ_DATABASE_DEBUG_ACCESS_TIMES = 20 # 如果一次请求过程中数据库交互次数大于20次,则打印出具体的次数。

以上配置参数,请参考 Flaskz-admin 模板。

ModelBase

初始化操作会创建一个SQLAlchemy的declarative_base实例ModelBase,其他数据库业务模型类必须要继承ModelBase,所有继承自ModelBase的模型类都会跟配置的数据库引擎进行绑定。

示例如下:

                
                    class User(ModelBase, ModelMixin):
                        __tablename__ = 'users'  # User对应的数据库表名

                        id = Column(Integer, primary_key=True, autoincrement=True)  # 自增主键,请注意SQLAlchemy ORM要求必须要有一个主键,用于区分不同的数据对象
                        name = Column(String(32), unique=True, nullable=False)
                        age = Column(Integer)
                        addresses = relationship(Address, cascade='all,delete,delete-orphan', lazy='joined') # Address关系映射
                        #......
                
            

多个数据库

有些业务场景,需要同时连接多个数据库进行操作,例如,数据采集和数据分析在不同的数据库的场景。

下面通过示例代码,介绍如何操作多个数据库

                
                    ABase = declarative_base()                                                          # 1.创建A数据库对应的Base对象

                    def init_app(app):
                        app_config = get_app_config_items(app) or {}
                        database_uri = app_config.get('A_DATABASE_URI')
                        try:
                            engine_kwargs = {}
                            custom_engine_kwargs = app_config.get('A_DATABASE_ENGINE_KWARGS')
                            if type(custom_engine_kwargs) is dict:
                                engine_kwargs.update(custom_engine_kwargs)

                            for engine_key, config_key in {'echo': 'A_DATABASE_ECHO',
                                                           'pool_recycle': 'A_DATABASE_POOL_RECYCLE',
                                                           'pool_pre_ping': 'A_DATABASE_POOL_PRE_PING'}.items():
                                if config_key in app_config:
                                    engine_kwargs[engine_key] = app_config.get(config_key)

                            engine = create_engine(database_uri, **engine_kwargs)                       # 2.创建数据库引擎
                            binds = DBSession.kw.get('binds')
                            binds.update({ABase: engine})
                            DBSession.configure(binds=binds)                                            # 3.将ABase对象和数据库引擎绑定,并更新DBSession配置

                            with engine.connect():  # connect test                                      # 4.连接测试
                                flaskz_logger.info('A Database ' + database_uri + ' is ready\n')

                        except Exception as e:
                            flaskz_logger.exception('Connect to A database ' + database_uri + ' error\n')
                            return


                    class TDevice(ABase, ModelMixin):                                                   # 5.使用ABase创建模型类,所有针对TDevice的操作都会对应到ABase设置的数据库
                        __tablename__ = 't_device'
                        __table_args__ = {'info': {'skip_autogenerate': True}}                          # 6.设置Alembic跳过对A数据库结构的更新

                        id = Column(Integer, primary_key=True)
                        name = Column(String(255))
                        mgmt_ip = Column(String(255))
                
            

SQLAlchemy会根据模型类绑定的数据库,对指定的数据库进行操作,在业务代码层面,不同数据库的模型类,使用起来没有任何差异。

请注意示例代码注释6,如果指定的数据库表不需维护其结构,可以设置Alembic跳过对其的维护。

常用函数

以下为操作数据库经常用到的一些函数介绍。

  • create_instance(model_cls, data, create_relationship=True)
    • 功能:使用dict字典数据,创建指定的模型类对象,主要用于向数据库中添加数据
    • 参数:
      • {class}model_cls --指定的模型类
      • {dict}data --创建对象的属性集合
      • {boolean}create_relationship --是否创建relationship(列表/对象)

        -如果为True,则会使用data中relationship相关的数据创建对应的relationship属性

        -如果为False,则不会创建relationship,默认为True,即只要data中有relationship相关的属性,就会创建relationship属性

    • 返回:创建好的instance对象
    • 示例:
                                      
                                          instance = create_instance(User, {
                                              "name":"taozh",
                                              "age":18,
                                              "addressess":[ # 除了创建User对象,还会创建User关联的addresses关系列表,添加到数据库时,会一起插入(User表一条,Address表两条)
                                                  {"city":"Shanghai"},
                                                  {"city":"Shandong"}
                                              ]});
                                          with db_session() as session: # 使用db_session上下文管理器引用session,将对象添加到数据库中
                                              session.add(instance)
                                          return instance
                                      
                                  

  • db_session(do_commit=True)
    • 功能:数据库session上下文管理器,SQLAlchemy ORM和数据库的所有交互操作,都要通过 session 进行。

      针对session的增删改查操作完成以后,会调用commit方法启动事务将数据插入到数据库中(commit之前数据只是存在于session中),如果操作失败则会进行rollback操作

      同时为了提升效率,一次请求(API)过程中只会生成一个session,并缓存于g应用上下文中,请求结束时销毁

    • 参数:
      • {boolean}do_commit --操作完成以后是否commit,默认为True

        -如果为True,则会执行commit,一般用于增删改操作

        -如果为False,则不会执行commit操作,一般用于查询操作

    • 示例:请参考create_instance示例代码
  • query_all_models(*models)/query_multiple_model(*cls_list)
    • 功能:一次性查询多个数据模型的所有数据(query_all),并以二维列表的形式返回
    • 参数:
      • *models --需要查询的多个模型类(关键字参数)
    • 返回:多个模型类的所有数据组成的二维列表,如果查询失败,会返回(False,reason)的一个元组
    • 示例:
                                      
                                          result = query_all_models(Role, User) # 请求用户列表数据时,也会将系统角色数据一起返回,用于用户的增删改查操作。
                                              if result[0] is False: # 如果查询失败,
                                                  success = False
                                                  res_data = model_to_dict(result[1])
                                              else:
                                                  success = True
                                                  res_data = {
                                                      'roles': model_to_dict(result[0]),
                                                      'users': model_to_dict(result[1])
                                                  }
                                      
                                  

    两个函数功能完全相同,1.5版本添加了query_all_models方法。

  • model_to_dict(ins, option=None)
    • 功能:将模型类的对象或对象列表列表转换为字典或列表,主要用于将模型数据返回到请求客户端,请参考BaseModelMixin的 数据序列化 介绍
    • 参数:
      • ins --要转换的模型类对象或对象列表
      • option --转换选项,请参考 BaseModelMixin.to_dict 方法
    • 返回:转换以后的字典对象或对象列表
  • parse_pss(cls, pss_payload)
    • 功能:将对模型数据的分页,排序,搜索等查询参数进行解析并生成操作数据库对应的方法和参数

      此函数主要是为了简化 ModelMixin.query_pss 的使用, 将查询参数转换成查询语句,并解决SQL注入等问题

    • 版本:1.6.1添加
    • 参数:
      • cls --模型类,例如)SysUser
      • pss_payload --分页,排序,搜索等参数
        • page --分页参数
          1. offset/skip - 查询结果集的偏移起始值,即跳过多少条记录,默认为0
          2. limit/size - 查询结果集的限制条数,为了避免大量数据导致的性能问题,如果参数为空,默认限制为100000
        • search/query -- 查询参数,参数主要包括以下几种类型
          1. 列/属性 - 数据模型类列/属性,使用==进行完全匹配,根据属性值类型有不同的处理逻辑
            • 字符串
              • 如果字符串不包含str_sep(字符串分隔符)/str_sep为空,直接使用, 例如)"name":"admin"
              • 如果字符串包含str_sep,会将值拆分成多个值使用, 例如)"email": "t1@focus-ui.com||t2@flaskz.com" 会被拆分成email==t1@focus-ui.com OR email = t2@flaskz.com
              • 为了更好的适配前端请求,默认会忽略值为""/None的属性,如果查询的值就是""/None,有两种处理方式
                1. 设置全局_ignore_null属性为False
                2. 使用字典方式传值,例如)"name": {"==": ""}
            • 字典/对象 - 字典的键为操作符(==/in/between/...),值为操作符对应的值,例如)"name":{"in":["t1","t2"]} / "age":{">":10,"<":20}}
            • 其他类型 - 直接使用,例如)"age":18
          2. 模糊查询 - payload或数据模型类中指定的like_columns列才会被模糊查询
            • _like/like - 不区分大小写,例如)"like":"hello"
            • _ilike/ilike - 区分大小写(需要数据库支持),例如)"ilike":"Hello"
            • _notlike/notlike - 不区分大小写,例如)"notlike":"hello"
            • _notilike/notilike - 区分大小写(需要数据库支持),例如)"notilike":"Hello"

            注意事项

            • 如果payload和模型类中都没有指定like_columns,则不会模糊查询
            • 多个like/ilike字段之间默认通过OR的方式进行连接(任意一个字段包含即符合), 多个notlike和/notalike之间默认通过AND的方式进行连接(必须所有字段都不包含)
            • 如果参数值不包含%,系统默认使用%text%进行匹配查询,可以在参数值的开头/结束添加%以改变默认匹配
              1. "text" - name like '%text%' (默认)
              2. "%text" - name like '%text'
              3. "text%" - name like 'text%'
          3. ★关系/relationship - 数据模型类relationship关系相关的查询参数

            有两种传值方式

            • 层次字符串 - 以.连接的层次字符串,例如)"address.city":"New York"
            • 字典/对象 - 例如)"address":{"city":"New York"}

            值的类型跟上述search中的查询参数一致

            关于模糊匹配

            • 如果relationship中的like/ilike=True,则会使用search中的like字段对relationship的like_columns进行模糊查询, 并且跟数据模型类的模糊查询默认以OR的方式进行连接
            • 如果relationship中的notlike/notilike=True,则会使用search中的notlike字段对relationship的like_columns进行模糊查询, 并且跟数据模型类的模糊查询默认以AND的方式进行连接

            注意事项

            • 主表和relationship表是通过左外连接outerjoin的方式进行的关联
            • 如果是一对多的relationship(例如:一个用户多个地址),只要有一个relationship中的数据符合条件,所有的relationship(符合条件的+不符合条件的)都会被返回
            • 只支持当前模型类的relationship(一级),不支持relationship的relationship(多级)
          4. 关键字
            1. 在跟列名不冲突的情况下,关键字可以省略开头的_,例如) ignore_null/ors/like
            2. 如果两者都存在,优先使用以_开头的属性,例如) _like > like

            有如下关键字

            • _ignore_null/ignore_null - 是否忽略空值(None/''/null),默认为True(忽略),例如)"_ignore_null": False
            • _str_sep/str_sep - 字符串分隔符,默认为 "||",ex) "_ors": {"email": "t1@focus-ui.com||t2@flaskz.com"}会被拆分成email==t1@focus-ui.com OR email = t1@flaskz.com
            • _like/like - 模糊查询(包含)
            • _ilike/ilike - 模糊查询(包含&区分大小写)
            • _likejoin/likejoin - 模糊查询(包含)的连接方式,可选值为'or'/'and',默认为or(任意一个字段包含即符合)
            • _notlike/notlike - 模糊查询(不包含)
            • _notilike/notilike - 模糊查询(不包含&区分大小写)
            • _notilikejoin/notlikejoin - 模糊查询(不包含)的连接方式,可选值为'or'/'and',默认为and(所有字段都不包含)
            • _like_columns/like_columns - 模糊查询的列,如果不指定,默认使用模型类的like_columns,例如)"like_columns":["name"]
            • _ors/ors - 通过OR进行连接的多个查询条件集合,例如)"_ors": {"name":{"in":["t1","t2"]},"age":{"between":[10,20]}}
            • _ands/ands - 通过AND进行连接的多个查询条件集合,例如)"_ands": {"name":{"in":["t1","t2"]},"age":{">":10,"<":20}}

          以上参数,_ors以外的其他查询参数,都会使用AND进行连接

        • sort --排序参数
          1. field - 排序属性,即以哪个属性值进行排序,支持relationship属性(以.连接的层次字符串),例如)"field":"name" / "field":"address.city"
          2. order - 排序的类型,正序还是倒序,如果order是['desc', 'descend', 'descending']中的值,则按倒序进行排序,否则按正序进行排序
    • 示例:
                                      
                                          User.query_pss(parse_pss({
                                              "search": {             # WHERE
                                                  "name": "admin"     # 精确查询, name='admin'
                                                  "updated_at": {     # operator
                                                      ">=": "2022-01-01 00:00:00",
                                                      "<": "2023-01-01 00:00:00"
                                                  },
                                                  "age": {            # between
                                                      "between": [10,20]
                                                  },
      
                                                  "like": "t",        # 模糊查询, name like '%t%' OR description like '%t%' (TemplateModel.like_columns = ['name', description])
                                                  # "notlike": "t",         # 不包含模糊查询, name notlike '%t%' AND description notlike '%t%'
      
      
                                                  "address.city": "New York", # -relation字段精确查询
                                                  "address": {        # -relation精确查询
                                                      "like": True,       # -使用search中的like字段对address关系的like_columns进行模糊查询
                                                      "country": "USA"    # -使用search中的like字段对address关系的like_columns进行模糊查询
                                                  },
                                                  # 关键字
                                                  # 1. 在不冲突的情况下,可以省略开头的‘_’ ex)ignore_null/ors/like,
                                                  # 2. 如果两者都存在,优先使用以'_'开头的属性 ex)_like > like
                                                  "_ignore_null": False,  # 是否忽略空值(null/''),默认为True(忽略)
                                                  "_str_sep": "||",       # 字符串分隔符,默认为 "||",ex) "_ors": {"email": "t1@focus-ui.com||t2@flaskz.com"}会被拆分成email==t1@focus-ui.com OR  email = t1@flaskz.com
      
                                                  "_like": "t",           # 模糊查询关键字==like,like字段冲突时使用,优先级_like>_like
                                                  "_ilike": "t",          # 不区分大小写模糊查询关键字==ilike(需DB支持),ilike字段冲突时使用,优先级_ilike>ilike
                                                  "_likejoin": "or",      # 模糊查询(包含)的连接方式,可选值为'or'/'and',默认为or(任意一个字段包含即符合)
      
                                                  "_notlike": "t",        # 不包含模糊查询关键字==notlike,notlike字段冲突时使用,优先级_notlike>_notlike
                                                  "_notilike": "t",       # 不区分大小写不包含模糊查询关键字==notilike(需DB支持),notilike字段冲突时使用,优先级_notilike>notilike
                                                  "_notilikejoin": "and", # 模糊查询(不包含)的连接方式,可选值为'or'/'and',默认为and(所有字段都不包含)
      
                                                  "_like_columns": ["name"], # 模糊查询的列,默认使用模型类的like_columns
      
                                                  "_ors": {               # 多个OR连接的条件关键字
                                                      "email": "t1@focus-ui.com||t2@flaskz.com",  # email = t1@focus-ui.com OR email = t2@flaskz.com
                                                      "name": {
                                                          "in": [
                                                              "t1",
                                                              "t2"
                                                          ]
                                                      },
                                                      "age": {
                                                          "between": [
                                                              10,
                                                              20
                                                          ]
                                                      }
                                                  },
                                                  "_ands": {              # 多个AND连接的条件关键字
                                                      "age": { # age >10 AND age<20
                                                          ">": 10,
                                                          "<": 20
                                                      }
                                                  }
                                              },
                                              "sort": {               # 查询结果集的排序方式, ORDER BY users.name ASC
                                                  "field": "name",    # 进行排序的属性
                                                  "order": "asc"      # 排序方式,asc/desc
                                              },
                                              # "sort": [             # 多重排序
                                              #     {
                                              #         "field": "name",
                                              #         "order": "asc"    # asc/desc
                                              #     },
                                              #     {
                                              #         "field": "address.city",  # -relation字段排序
                                              #         "order": "desc"   # asc/desc
                                              #     }
                                              # ],
                                              "page": {               # 分页属性, LIMIT ? OFFSET ? (20, 0)
                                                  "offset": 0,        # 偏移为0
                                                  "size": 20          # 每页20条记录
                                              }
                                              # ,"group": "email"     # Group BY分组
                                          }))
                                      
                                  

      通过parse_pss函数转换以后的结果,示例如下

    • 返回:供ModelMixin.query_pss调用的查询条件集合