【问题标题】:flask-sqlalchemy db.Model._decl_class_registry.values() and db.metadata.tables are inconsistentflask-sqlalchemy db.Model._decl_class_registry.values() 和 db.metadata.tables 不一致
【发布时间】:2018-12-20 03:23:31
【问题描述】:

我想根据单位字段动态生成一个类,如下所示:

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个路由(/&lt;unit&gt;/a,/&lt;unit&gt;/b,/&lt;unit&gt;/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,所以class和table中有三个。但是一旦我们点击 b,.vales() 中就只有 a, b。由于没有table_c类,所以在我们的程序逻辑中会重新生成,但是Tables中确实存在table_c。所以会抛出异常:

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函数,但是原理和我们的方法一致,不起作用。

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

【问题讨论】:

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


    【解决方案1】:

    我昨天一直在与这个异常作斗争,并想出了一个“解决方案”。有点hacky但有效。

    我的问题是烧瓶服务器重新启动后,预先存在的动态创建的表没有加载到db.Model._decl_class_registry 中,但它们列在了db.metadata.tables 中。问题中不包括如何初始化数据库,但我认为问题的根本原因与我的相同,我尝试在启动时使用具有动态表名和metadata.reflect 的持久数据库。重新创建一个名称存在于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。在我看来,当调用db = SQLAlchemy(model_class=Base) 时,关键是local 范围(关于可用模型)。所以我想出了这个:

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

    1. 加载数据库(物理 sqlite 数据库)
    2. 从连接中加载所有表名
    3. 为本地人添加适当的模型
    4. 使用包含所有模型的本地变量重新初始化 SqlAlchemy
    5. 调用反射
    6. 所有以前生成的动态命名表都已正确加载

    app.py - 具有更新的初始化序列 WORKING

    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() hack WORKING

    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。重新声明表/模型将导致上述异常。如果你把你的 re-delcaration 放到 try/except 块中(是的,我试过了),你将无法查询,因为表和模型将无法正确连接。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-03-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-16
      • 2021-10-02
      相关资源
      最近更新 更多