【问题标题】:Check database schema matches SQLAlchemy models on application startup在应用程序启动时检查数据库模式是否匹配 SQLAlchemy 模型
【发布时间】:2019-04-14 02:24:35
【问题描述】:

为了防止人为错误,我想检查当前 SQL 数据库架构是否与 SQLAlchemy 模型代码匹配,并且不需要在应用程序启动时运行迁移。有没有办法在 SQLAlchemy 上迭代所有模型,然后查看数据库模式是否符合模型的预期?

这是为了防止稍后弹出错误(HTTP 500 由于缺少表、字段等)

【问题讨论】:

    标签: python sqlalchemy


    【解决方案1】:

    根据上面的@yoloseem 提示,这是一个完整的答案:

    import logging
    
    from sqlalchemy import inspect
    from sqlalchemy.ext.declarative.clsregistry import _ModuleMarker
    from sqlalchemy.orm import RelationshipProperty
    
    logger = logging.getLogger(__name__)
    
    
    def is_sane_database(Base, session):
        """Check whether the current database matches the models declared in model base.
    
        Currently we check that all tables exist with all columns. What is not checked
    
        * Column types are not verified
    
        * Relationships are not verified at all (TODO)
    
        :param Base: Declarative Base for SQLAlchemy models to check
    
        :param session: SQLAlchemy session bound to an engine
    
        :return: True if all declared models have corresponding tables and columns.
        """
    
        engine = session.get_bind()
        iengine = inspect(engine)
    
        errors = False
    
        tables = iengine.get_table_names()
    
        # Go through all SQLAlchemy models
        for name, klass in Base._decl_class_registry.items():
    
            if isinstance(klass, _ModuleMarker):
                # Not a model
                continue
    
            table = klass.__tablename__
            if table in tables:
                # Check all columns are found
                # Looks like [{'default': "nextval('sanity_check_test_id_seq'::regclass)", 'autoincrement': True, 'nullable': False, 'type': INTEGER(), 'name': 'id'}]
    
                columns = [c["name"] for c in iengine.get_columns(table)]
                mapper = inspect(klass)
    
                for column_prop in mapper.attrs:
                    if isinstance(column_prop, RelationshipProperty):
                        # TODO: Add sanity checks for relations
                        pass
                    else:
                        for column in column_prop.columns:
                            # Assume normal flat column
                            if not column.key in columns:
                                logger.error("Model %s declares column %s which does not exist in database %s", klass, column.key, engine)
                                errors = True
            else:
                logger.error("Model %s declares table %s which does not exist in database %s", klass, table, engine)
                errors = True
    
        return not errors
    

    下面是 py.test 测试代码来练习这个:

    """Tests for checking database sanity checks functions correctly."""
    
    from pyramid_web20.system.model.sanitycheck import is_sane_database
    from sqlalchemy import engine_from_config, Column, Integer, String
    import sqlalchemy
    from sqlalchemy.ext.declarative import declarative_base, declared_attr
    from sqlalchemy.ext.hybrid import hybrid_property
    from sqlalchemy.orm import sessionmaker, relationship
    from sqlalchemy import ForeignKey
    
    
    def setup_module(self):
        # Quiet log output for the tests
        import logging
        from pyramid_web20.system.model.sanitycheck import logger
        #logger.setLevel(logging.FATAL)
    
    
    def gen_test_model():
    
        Base = declarative_base()
    
        class SaneTestModel(Base):
            """A sample SQLAlchemy model to demostrate db conflicts. """
    
            __tablename__ = "sanity_check_test"
    
            #: Running counter used in foreign key references
            id = Column(Integer, primary_key=True)
    
        return Base, SaneTestModel
    
    
    def gen_relation_models():
    
        Base = declarative_base()
    
        class RelationTestModel(Base):
            __tablename__ = "sanity_check_test_2"
            id = Column(Integer, primary_key=True)
    
    
        class RelationTestModel2(Base):
            __tablename__ = "sanity_check_test_3"
            id = Column(Integer, primary_key=True)
    
            test_relationship_id = Column(ForeignKey("sanity_check_test_2.id"))
            test_relationship = relationship(RelationTestModel, primaryjoin=test_relationship_id == RelationTestModel.id)
    
        return Base, RelationTestModel, RelationTestModel2
    
    
    def gen_declarative():
    
        Base = declarative_base()
    
        class DeclarativeTestModel(Base):
            __tablename__ = "sanity_check_test_4"
            id = Column(Integer, primary_key=True)
    
            @declared_attr
            def _password(self):
                return Column('password', String(256), nullable=False)
    
            @hybrid_property
            def password(self):
                return self._password
    
        return Base, DeclarativeTestModel
    
    
    def test_sanity_pass(ini_settings, dbsession):
        """See database sanity check completes when tables and columns are created."""
    
        engine = engine_from_config(ini_settings, 'sqlalchemy.')
        conn = engine.connect()
        trans = conn.begin()
    
        Base, SaneTestModel = gen_test_model()
        Session = sessionmaker(bind=engine)
        session = Session()
        try:
            Base.metadata.drop_all(engine, tables=[SaneTestModel.__table__])
        except sqlalchemy.exc.NoSuchTableError:
            pass
    
        Base.metadata.create_all(engine, tables=[SaneTestModel.__table__])
    
        try:
            assert is_sane_database(Base, session) is True
        finally:
            Base.metadata.drop_all(engine)
    
    
    def test_sanity_table_missing(ini_settings, dbsession):
        """See check fails when there is a missing table"""
    
        engine = engine_from_config(ini_settings, 'sqlalchemy.')
        conn = engine.connect()
        trans = conn.begin()
    
        Base, SaneTestModel = gen_test_model()
        Session = sessionmaker(bind=engine)
        session = Session()
    
        try:
            Base.metadata.drop_all(engine, tables=[SaneTestModel.__table__])
        except sqlalchemy.exc.NoSuchTableError:
            pass
    
        assert is_sane_database(Base, session) is False
    
    
    def test_sanity_column_missing(ini_settings, dbsession):
        """See check fails when there is a missing table"""
    
        engine = engine_from_config(ini_settings, 'sqlalchemy.')
        conn = engine.connect()
        trans = conn.begin()
    
        Session = sessionmaker(bind=engine)
        session = Session()
        Base, SaneTestModel = gen_test_model()
        try:
            Base.metadata.drop_all(engine, tables=[SaneTestModel.__table__])
        except sqlalchemy.exc.NoSuchTableError:
            pass
        Base.metadata.create_all(engine, tables=[SaneTestModel.__table__])
    
        # Delete one of the columns
        engine.execute("ALTER TABLE sanity_check_test DROP COLUMN id")
    
        assert is_sane_database(Base, session) is False
    
    
    def test_sanity_pass_relationship(ini_settings, dbsession):
        """See database sanity check understands about relationships and don't deem them as missing column."""
    
        engine = engine_from_config(ini_settings, 'sqlalchemy.')
        conn = engine.connect()
        trans = conn.begin()
    
        Session = sessionmaker(bind=engine)
        session = Session()
    
        Base, RelationTestModel, RelationTestModel2  = gen_relation_models()
        try:
            Base.metadata.drop_all(engine, tables=[RelationTestModel.__table__, RelationTestModel2.__table__])
        except sqlalchemy.exc.NoSuchTableError:
            pass
    
        Base.metadata.create_all(engine, tables=[RelationTestModel.__table__, RelationTestModel2.__table__])
    
        try:
            assert is_sane_database(Base, session) is True
        finally:
            Base.metadata.drop_all(engine)
    
    
    def test_sanity_pass_declarative(ini_settings, dbsession):
        """See database sanity check understands about relationships and don't deem them as missing column."""
    
        engine = engine_from_config(ini_settings, 'sqlalchemy.')
        conn = engine.connect()
        trans = conn.begin()
    
        Session = sessionmaker(bind=engine)
        session = Session()
    
        Base, DeclarativeTestModel = gen_declarative()
        try:
            Base.metadata.drop_all(engine, tables=[DeclarativeTestModel.__table__])
        except sqlalchemy.exc.NoSuchTableError:
            pass
    
        Base.metadata.create_all(engine, tables=[DeclarativeTestModel.__table__])
    
        try:
            assert is_sane_database(Base, session) is True
        finally:
            Base.metadata.drop_all(engine)
    

    【讨论】:

    • 不错!对于图书馆来说,这是一个很棒的想法。
    • 如果我们让它除了列名之外也检查列类型,那将是完美的@MikkoOhtamaa
    【解决方案2】:

    查看Runtime Inspection API

    您也可以将Engine 传递给inspect()。一旦您拥有sqlalchemy.engine.reflection.Inspector 对象,现在您可以使用get_table_names()get_columns(tbl_name) 和任何其他方法(例如,用于主键、约束、索引等)来检查您的数据库拥有的“真实”模式。

    【讨论】:

    • 谢谢!我对此进行了扩展-见下文。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-17
    • 1970-01-01
    • 2017-11-30
    相关资源
    最近更新 更多