使用蓝图中的元数据创建 Flask-SQLAlchemy 实例



TL;DR:如何使用蓝图中的metadata对象来创建Flask-SQLAlchemy实例?我可以看到提供声明性基metadata对象的唯一位置是在初始SQLAlchemy()调用中。但是,当我从extensions.py文件中的蓝图导入它时,蓝图的代码需要db对象,并且由于循环导入,加载失败。


我有几个模型类,我想在 Flask 内外使用它们。我正在使用声明性方法来执行此操作,并且我的应用程序已设置为使用应用工厂模型和蓝图。模型向 SQLAlchemy 注册的方式是在创建db对象时使用metadata参数。在我的应用程序上下文中,在蓝图中声明metadata对象是有意义的,而不是在主应用程序蓝图中声明。(这是引用它的大多数代码的位置,包括用于最初填充数据库的非 Flask 实用程序脚本。但是,从第二个蓝图导入模型类最终会变成循环导入。

$ flask db migrate
Error: While importing "my_app", an ImportError was raised:
Traceback (most recent call last):
File "my_app/venv/lib/python3.7/site-packages/flask/cli.py", line 235, in locate_app
__import__(module_name)
File "my_app/my_app.py", line 1, in <module>
from app import create_app
File "my_app/app/__init__.py", line 7, in <module>
from app.extensions import *
File "my_app/app/extensions.py", line 10, in <module>
from turf.models import metadata
File "my_app/turf/__init__.py", line 1, in <module>
from .routes import bp
File "my_app/turf/routes.py", line 14, in <module>
from app.extensions import db
ImportError: cannot import name 'db' from 'app.extensions' (my_app/app/extensions.py)

如有关蓝图循环导入的一般问题所述,有效的解决方案是从第二个蓝图中的每个函数内部导入db对象,从而在extensions.py文件初始化期间避开导入。但除了烦人之外,这感觉非常笨拙。

理想情况下,我将能够将我创建的metadata对象传递给 SQLAlchemy 的init_app()方法。这将一举解决这个问题。不幸的是,init_app()不接受metadata论点。初始化后是否有其他方法可以将元数据注册到 SQLAlchemy 实例?还是我错过了声明性模型方法的其他一些关键元素?

我应该说,其中的非烧瓶部分工作得很好。我的实用程序脚本能够导入模型并使用它们将对象添加到数据库中。只有烧瓶进口给我带来了麻烦。

下面是层次结构:

.
├── app
│   ├── __init__.py
│   └── extensions.py
└── turf
   ├── __init__.py
   ├── models.py
   └── routes.py

以及由于循环导入而失败的相关代码:

应用/__init__.py:

from app.extensions import *
def create_app():
app = Flask(__name__)
with app.app_context():
import turf
app.register_blueprint(turf.bp)
db.init_app(app)

应用/扩展.py:

from turf.models import metadata
db = SQLAlchemy(metadata=metadata)

草坪/__init__.py:

from .routes import bp

草坪/型号.py:

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import MetaData
metadata = MetaData()
Base = declarative_base(metadata=metadata)
# All the turf models are declared in this file
class Boundary(Base):
# ...etc...

草坪/路线.py:

from .models import *
from app.extensions import db
bp = Blueprint('Turf', __name__, url_prefix='/turf')
@bp.route('/')
def index():
return render_template('turf/index.html')

事实证明,您可以在extensions.py文件中声明元数据对象,然后将其导入蓝图。我确信这会失败,因为metadata对象现在正在创建db对象后填充,但我已经验证了模型确实可用并且按预期工作。并且不再有循环依赖。实际上,我已经将这部分分解成它自己的文件,以允许蓝图代码尽可能少地导入。

应用/基础.py:

from sqlalchemy import MetaData
from sqlalchemy.ext.declarative import declarative_base
metadata = MetaData()
Base = declarative_base(metadata=metadata)

应用/扩展.py:

from flask_sqlalchemy import SQLAlchemy
from .base import metadata
db = SQLAlchemy(metadata=metadata)

草坪/型号.py:

from app.base import Base
# All the turf models are declared in this file
class Boundary(Base):
# ...etc...

这也回答了我对原始方法的另一个问题:如果我有第二个蓝图,其中也有需要从非Flask代码中获得的模型对象,它将如何工作?现在,我已经创建了一个 Base 对象,并可以根据需要使用它在不同的蓝图中实现新类。

不过,这种方法有一个小烦恼。在非 Flask DB 填充脚本中,我最初能够使用from models import *来引用包含模型的同级模块(文件)。这让我直接调用脚本,就像cd turf; python populate_db.py --arg一样。这不再有效,因为models.py文件现在引用了不同的包,app.extensions.因此,我必须使用此解决方法:

草坪/populate_db.py:

try:
from .models import *
except:
print("You have to run this from outside the 'turf' directory like so: $ python -m turf.populate_db [...]")
sys.exit(1)