flask-sqlalchemy db.Model._decl_class_registry.values()和db.metadata.tables不一致

时间:2018-07-12 02:13:59

标签: python-3.x flask sqlalchemy flask-sqlalchemy

我想像这样基于unit字段动态生成一个类:

def gm_a(unit):
    tname = '%s_a' % unit

    for c in db.Model._decl_class_registry.values():
        if hasattr(c, '__table__') and c.__table__.fullname == tname:
        return c

    class A(DynamicBase):
        __tablename__ = '%s_a' % unit
        id = db.Column(db.Integer, primary_key=True)
        ......# other fields
    return A

您可以查看是否要在ORM操作中使用table_a或desk_a表,可以这样做:

@app.route('/<unit>/a')
def a(unit):
    obj_table  = gm_a('table').query.filter_by(xxx).all()
    obj_desk = gm_a('desk').query.filter_by(xxx).all()

通过这种方式,我们可以对具有相同结构的不同表名的表进行操作。如果我们有3个gm_ *函数(gm_a,gm_b,gm_c)和3条路由(/<unit>/a/<unit>/b/<unit>/c),则每个模板都是这样的:

<ul>
  <li><a href="a">A</a></li>
  <li><a href="b">B</a></li>
  <li><a href="c">C</a></li>
</ul>

如果我们随机单击这些链接,我们期望在db.Model._decl_class_registry.values()中生成3个类,并在db.metadata.tables中生成3个表。

Strange phenomenon

---
[<class '__main__.gm_a.<locals>.A'>, <class '__main__.gm_b.<locals>.B'>, <class '__main__.gm_c.<locals>.C'>]
immutabledict({'table_a': Table('table_a', MetaData(bind=None), Column('id', Integer(), table=<table_a>, primary_key=True, nullable=False), schema=None), 'table_b': Table('table_b', MetaData(bind=None), Column('id', Integer(), table=<table_b>, primary_key=True, nullable=False), schema=None), 'table_c': Table('table_c', MetaData(bind=None), Column('id', Integer(), table=<table_c>, primary_key=True, nullable=False), schema=None)})
127.0.0.1 - - [12/Jul/2018 09:58:03] "GET /table/b HTTP/1.1" 200 -
---
[<class '__main__.gm_a.<locals>.A'>, <class '__main__.gm_b.<locals>.B'>, <class '__main__.gm_c.<locals>.C'>]
immutabledict({'table_a': Table('table_a', MetaData(bind=None), Column('id', Integer(), table=<table_a>, primary_key=True, nullable=False), schema=None), 'table_b': Table('table_b', MetaData(bind=None), Column('id', Integer(), table=<table_b>, primary_key=True, nullable=False), schema=None), 'table_c': Table('table_c', MetaData(bind=None), Column('id', Integer(), table=<table_c>, primary_key=True, nullable=False), schema=None)})
127.0.0.1 - - [12/Jul/2018 09:58:04] "GET /table/c HTTP/1.1" 200 -
---
[<class '__main__.gm_a.<locals>.A'>, <class '__main__.gm_b.<locals>.B'>]
immutabledict({'table_a': Table('table_a', MetaData(bind=None), Column('id', Integer(), table=<table_a>, primary_key=True, nullable=False), schema=None), 'table_b': Table('table_b', MetaData(bind=None), Column('id', Integer(), table=<table_b>, primary_key=True, nullable=False), schema=None), 'table_c': Table('table_c', MetaData(bind=None), Column('id', Integer(), table=<table_c>, primary_key=True, nullable=False), schema=None)})
127.0.0.1 - - [12/Jul/2018 09:58:04] "GET /table/b HTTP/1.1" 200 -
---
[<class '__main__.gm_a.<locals>.A'>, <class '__main__.gm_b.<locals>.B'>]
immutabledict({'table_a': Table('table_a', MetaData(bind=None), Column('id', Integer(), table=<table_a>, primary_key=True, nullable=False), schema=None), 'table_b': Table('table_b', MetaData(bind=None), Column('id', Integer(), table=<table_b>, primary_key=True, nullable=False), schema=None), 'table_c': Table('table_c', MetaData(bind=None), Column('id', Integer(), table=<table_c>, primary_key=True, nullable=False), schema=None)})
127.0.0.1 - - [12/Jul/2018 09:58:04] "GET /table/a HTTP/1.1" 200 -

您可以在图片或代码中看到,我们已经单击了a,b,c,因此在类和表中有3个。但是,一旦单击b,.vales()中只有一个b。由于没有table_c类,它将在我们的程序逻辑中重新生成,但是table_c确实存在于Tables中。因此它将引发异常:

sqlalchemy.exc.InvalidRequestError: Table 'table_c' is already defined for this MetaData instance.  Specify 'extend_existing=True' to redefine options and columns on an existing Table object.

我很困惑,为什么db.Model._decl_class_registry.values()中的数字将随机变化,而_decl_class_registry.values()和db.metadata.tables()的数目将不同。我还在sqlalchemy-utils中使用了get_class_by_table函数,但是该原理与我们的方法一致并且不起作用。

有人知道为什么吗?谢谢。

1 个答案:

答案 0 :(得分:0)

昨天我一直在为这个例外而苦苦挣扎,并提出了“解决方案”。有点hacky,但可以。

我的问题是在flask服务器重新启动后,先前存在的动态创建的表未加载到db.Model._decl_class_registry中,但已在db.metadata.tables中列出。问题中没有包含如何初始化数据库的方法,但是我认为问题的根本原因与我的相同,我已尝试在启动时使用具有动态表名和metadata.reflect的持久性db。重新创建名称为db.metadata.tables但不存在db.Model._decl_class_registry的表将导致上面的异常。 无效状态表示在初始化期间已加载持久数据库,并且flask-sqalchemy找不到表的模型。

这只是一个假设,我有点晚了,这个问题已经有2年的历史了,可能您甚至没有该段代码了。无论如何,如果有人想使用动态表名和持久数据库:我希望这个答案将有助于避免花费大量的时间进行谷歌搜索和调试。

那么,有问题的代码就会出现在

sqlalchemy.exc.InvalidRequestError: Table '{something}' is already defined for this MetaData instance.  Specify 'extend_existing=True' to redefine options and columns on an existing Table object.

服务器重新启动后:

app.py-原始方法<< 错误

from flask import Flask, request, render_template

from config.config_test import ConfigCache, db

import logging
import os

app = Flask(__name__, static_url_path='/static')
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] =  'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] =  False
app.config['SECRET_KEY'] = 'absolutely secret'

# !!!!!
# https://stackoverflow.com/questions/28789063/associate-external-class-model-with-flask-sqlalchemy
db.init_app(app)
with app.app_context():
    # !!!!
    # https://stackoverflow.com/a/40984584/8375783
    db.metadata.reflect(bind=db.engine)

configs.py-原始方法<< 错误

from flask_sqlalchemy import SQLAlchemy, declarative_base

Base = declarative_base()
db = SQLAlchemy(model_class=Base)


class ServerConfig(db.Model):
    __tablename__ = 'server_config'
    __abstract__ = True
    name = db.Column(db.String, unique=True, primary_key=True)
    location = db.Column(db.String)
    config_type = db.Column(db.String)
    descriptor = ['name', 'location', 'config_type']


class ConfigCache:

    def _is_server_config_cached(self, config_name):
        return db.metadata.tables.get(config_name, False)

    def _create_server_config_table(self, name):
        new_table = type(name, (ServerConfig,), {'__tablename__': name})
        db.create_all()
        return new_table

    def add_server_config(self, data):
        table = self._is_server_config_cached(data['config_name']) 
        if table is False:
            table = self._create_server_config_table(data['config_name'])
        for d in data["data_batch"]:
            entry = table(name=d['name'], location=d['location'], config_type=d['config_type'])
            db.session.add(entry)
        db.session.commit()

花了几个小时解决这个问题后,我注意到所有非抽象的,非动态的命名表都可以正常工作。在db.metadata.reflect(bind=db.engine)之后,所有模型均已正确加载到db.Model._decl_class_registrydb.metadata.tables中。在我看来,关键是调用local时的db = SQLAlchemy(model_class=Base)范围(关于可用的模型)。所以我想到了:

让我们调整启动顺序以提供所有模型:

  1. 加载db(物理sqlite db)
  2. 从连接中,加载所有表名
  3. 为本地人添加适当的模型
  4. 使用包含所有模型的局部变量重新初始化SqlAlchemy
  5. 致电反映
  6. 所有先前生成的动态命名表均已正确加载

app.py-具有更新的初始化顺序<< 工作

from flask import Flask, request, render_template

from config.config_test import db, dirty_factory

import logging
import os

app = Flask(__name__, static_url_path='/static')
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] =  'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] =  False
app.config['SECRET_KEY'] = 'absolutely secret'

# !!!!!
# https://stackoverflow.com/questions/28789063/associate-external-class-model-with-flask-sqlalchemy
db.init_app(app)
with app.app_context():
    # !!!!
    # https://stackoverflow.com/a/40984584/8375783
    dirty_factory(config_db.engine.engine.table_names())
    db.metadata.reflect(bind=config_db.engine)
    db.create_all(app=app)

configs.py-使用locals()破解<< 工作

from flask_sqlalchemy import SQLAlchemy, declarative_base

Base = declarative_base()
db = SQLAlchemy(model_class=Base)


class ServerConfig(db.Model):
    __tablename__ = 'server_config'
    __abstract__ = True
    name = db.Column(db.String, unique=True, primary_key=True)
    location = db.Column(db.String)
    config_type = db.Column(db.String)
    descriptor = ['name', 'location', 'config_type']

def dirty_factory(config_names):
    for config_name in config_names:
        if 'server-config' in config_name: #pattern in data['config_name']
            locals().update({config_name: type(config_name, (ServerConfig,), {'__tablename__': config_name, })})
    db = SQLAlchemy(model_class=Base)

class ConfigCache:

    def _is_server_config_cached(self, config_name):
        return db.metadata.tables.get(config_name, False)

    def _create_server_config_table(self, name):
        new_table = type(name, (ServerConfig,), {'__tablename__': name})
        db.create_all()
        return new_table

    def add_server_config(self, data):
        table = self._is_server_config_cached(data['config_name']) 
        if table is False:
            table = self._create_server_config_table(data['config_name'])
        for d in data["data_batch"]:
            entry = table(name=d['name'], location=d['location'], config_type=d['config_type'])
            db.session.add(entry)
        db.session.commit()

TLDR;

如果使用动态表名和持久数据库,请确保在初始化SQLAlchemy之前注意加载模型。如果在调用db = SQLAlchemy(model_class=Base)时无法加载模型,表将被加载到db.metadata.tables中,但相关模型将不会被加载到db.Model._decl_class_registry中。重新声明表/模型将导致上面的异常。如果将重新分配放入try / except块中(是的,我已经尝试过),则无法查询,因为表和模型无法正确连接。