为了尝试快速开发,使用Flask框架构建了web项目,但因需要适配Oracle数据库,从而折腾出一堆问题。
flask项目一般使用 flask-sqlalchemy 基于 SQLAlchemy中间件实现多种类型数据库的适配,Oracle也在其中。
本文为踩坑指南的第二篇。
在Flask项目的配置文件中,可通过设置常量的方式来告知SQLAlchemy连接数据库的配置参数。
配置文件示例:
import os from sqlalchemy.pool import QueuePool class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'some complicate strings' SQLALCHEMY_COMMIT_ON_TEARDOWN = True SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_ENGINE_OPTIONS = { 'poolclass': QueuePool, 'max_overflow': 5, # 超过连接池大小外最多可创建的连接 'pool_size': 20, # 连接池大小 'pool_timeout': 5, # 池满后,线程的最多等待连接的时间,否则报错 'pool_recycle': 1200, # 多久之后对线程池中的线程进行一次连接的回收(重置)—— -1 永不回收 'pool_pre_ping': True } @staticmethod def init_app(app): pass class developmentConfig(Config): """开发环境配置""" SQLALCHEMY_DATABASE_URI = "" # 数据库连接字符串 class ProductionConfig(Config): """生产环境配置""" SQLALCHEMY_DATABASE_URI = "" # 数据库连接字符串 config = { 'development': DevelopmentConfig, 'production': ProductionConfig }
然后,在app中加载配置信息:
from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) conf = config['development'] app.config.from_object(conf) db = SQLAlchemy() db.init_app(app)
之后,即可在功能模块中通过conn = db.session()
建立连接并执行增删改查操作。
一般情况下,对于MySQL、SQLite等数据库,可以通过SQLALCHEMY_ENGINE_OPTIONS
来配置数据库连接池。默认情况下,flask_sqlalchemy通过调用sqlalchemy.create_engine
方法创建连接。
查阅flask_sqlalchemy源码和SQLAlchemy 官方文档,可以发现,flask_sqlalchemy 在create_engine时,有如下注释:
# flask_sqlalchemy.__init__.py 1091 def create_engine(self, sa_url, engine_opts): """ Override this method to have final say over how the SQLAlchemy engine is created. In most cases, you will want to use ``'SQLALCHEMY_ENGINE_OPTIONS'`` config variable or set ``engine_options`` for :func:`SQLAlchemy`. """ return sqlalchemy.create_engine(sa_url, **engine_opts)
注释中提到:create_engine这个方法可以被覆盖,这取决于最终如何创建SQLAlchemy engine。大部分情况下,可以通过配置变量SQLALCHEMY_ENGINE_OPTIONS
来实现。其中,sa_url即连接字符串。sqlalchemy根据连接字符串,解析出数据库类型,并调用不同的方法实现数据库连接。
通过engine创建数据库连接及使用示例:
with engine.connect() as connection: connection.execute(text("insert into table values ('foo')")) connection.commit()
但是假如使用Oracle数据库及cx_Oracle方式(数据库连接字符串以oracle+cx_oracle://
开头),则以上配置方法往往无法使得数据库连接池生效,导致经过一段时间后,session失效,项目请求后台无响应。报错信息如下:
ERROR 2022-01-11 21:04:03 base.py[240]: Exception closing connection <cx_Oracle.Connection to ...db_connect_string> Traceback (most recent call last): File "/.../flask_admin/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 238, in _close_connection self._dialect.do_close(connection) File "/.../flask_admin/lib/python3.8/site-packages/sqlalchemy/engine/default.py", line 673, in do_close dbapi_connection.close() cx_Oracle.DatabaseError: DPI-1080: connection was closed by ORA-3113
报错的代码:
def _close_connection(self, connection): self.logger.debug("Closing connection %r", connection) try: self._dialect.do_close(connection) # 报错代码行,在关闭连接时抛出异常 except Exception: self.logger.error( "Exception closing connection %r", connection, exc_info=True )
推测为Oracle数据库连接已中断,但sqlalchemy在超过pool_recycle
配置的时间时试图关闭连接,从而发生异常。
无论如何调整 SQLALCHEMY_ENGINE_OPTIONS
配置项,都无法解决此问题。
然后,阅读sqlalchemy源码,并从中找到了如下注释:
# sqlalchemy/dialects/oracle/cx_oracle.py 98 """ Using cx_Oracle SessionPool --------------------------- The cx_Oracle library provides its own connectivity services that may be used in place of SQLAlchemy's pooling functionality. This can be achieved by using the :paramref:`_sa.create_engine.creator` parameter to provide a function that returns a new connection, along with setting :paramref:`_sa.create_engine.pool_class` to ``NullPool`` to disable SQLAlchemy's pooling:: import cx_Oracle from sqlalchemy import create_engine from sqlalchemy.pool import NullPool pool = cx_Oracle.SessionPool( user="scott", password="tiger", dsn="oracle1120", min=2, max=5, increment=1, threaded=True ) engine = create_engine("oracle://", creator=pool.acquire, poolclass=NullPool) The above engine may then be used normally where cx_Oracle's pool handles connection pooling:: with engine.connect() as conn: print(conn.scalar("select 1 FROM dual")) """
其中提到了cx_oracle 自有连接池功能,并给出了示例代码。同时,若使用其自有连接池功能,应设置sqlalchemy的poolclass
为NullPool
,即无连接池模式。简单来说,就是不使用sqlalchemy的连接池,转而使用cx_oracle.SessionPool
连接池。
因此,通过覆盖 create_engine
方法,可实现此功能:
class OracleSqlAlchemy(SQLAlchemy): def create_engine(self, sa_url, engine_opts): import cx_Oracle from sqlalchemy import create_engine from sqlalchemy.pool import NullPool from sqlalchemy.engine import url as _url # create url.URL object u = _url.make_url(sa_url) pool = cx_Oracle.SessionPool( user=u.username, password=u.password, dsn=f'{u.host}:{u.port}/{u.database}', min=2, max=5, increment=1, timeout=1200, # 闲置会话关闭前的等待时间 getmode=cx_Oracle.SPOOL_ATTRVAL_TIMEDWAIT, wait_timeout=5, # 池满后创建新会话的等待时间 threaded=True ) engine = create_engine("oracle://", creator=pool.acquire, poolclass=NullPool) return engine
然后,在app初始化时,用如下方式初始化db连接:
db = OracleSqlAlchemy() db.init_app(app)
经过实际测试,flask项目不再发生后台无响应的情况。