flask_migrate的一些坑和解决办法

       最近再搞数据库自动升级的功能,本以为有现成的插件Flask_Migrate可以用,但是没想到还是开始了填坑之路。

第一坑,外键约束的增减。

       Sqlalchemy的model在我们做外键关联的时候,是不给外键名称的。这样做没什么问题,因为每一种数据库都有自己的默认的外键生成规则,orm完全可以不考虑这一部分。但是和Flask_Migrate一起使用的时候就会出现问题。当我们新增一个外键字段时,做migrate和upgrade不会有任何问题,外键约束的name依赖数据库自己的机制生成,但是一旦该外键有业务上的调整或者其他状况需要移除时,就会出现问题。我们做downgrade时,Flask_migrate会报错:

            Can't emit DROP CONSTRAINT for constraint ForeignKeyConstraint(..., None, .......); it has no name

意思是,删除这个约束,但是这个约束叫None,他没有名字,删不掉!!!!

       解决方法:

       那就给它一个名字吧。在我们初始化SQLAlchemy对象的时候,可以设置其metadata属性,在其中设置命名规则,比如这样,

def set_constraint_name():
    # 这段是网上抄的,可以参照参数自己设置自己的规则
    naming_convention = {
        "ix": 'ix_%(column_0_label)s',
        "uq": "uq_%(table_name)s_%(column_0_name)s",
        "ck": "ck_%(table_name)s_%(column_0_name)s",
        "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
        "pk": "pk_%(table_name)s"
    }
    return naming_convention
db = SQLAlchemy(metadata=MetaData(naming_convention=set_constraint_name()))

 

第二坑,自定义的字段类型

       在项目里,框架自带的字段类型可能并不能满足我们千奇百怪的业务功能要求,所以会需要自己设计一些特殊的字段类型。但是在数据迁移时,会很蛋疼。依然是migrate,upgrade两步走,你会发现upgrade直接失败了。因为migrate生成的脚本可能直接就是个一片红的玩意。

from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = '78d3dba1a218'
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('mytable',
    sa.Column('version_id', sa.INTEGER(), nullable=False),  
    ...
    sa.Column('MyField', myTypes.TimeType(), nullable=True), 
    ...

因为它生成的文件里并不会引入我们自定义的类型,所以,当然会报错。这里走过一段不长但是很套路的弯路,在alembic的源码的EnvironmentContext类内的configure方法里有个sqlalchemy_module_prefix参数。看这个名字仿佛已经接近了胜利但是其实它完全没用!完全没用!完全没用!官方文档是这样写的:

When types are rendered, they are generated with a module prefix, so that they are available based on a relatively small number of imports. The rules for what the prefix is is based on the kind of datatype as well as configurational settings. For example, when Alembic renders SQLAlchemy types, it will by default prefix the type name with the prefix sa.:

Column("my_column", sa.Integer())

The use of the sa. prefix is controllable by altering the value of EnvironmentContext.configure.sqlalchemy_module_prefix:

def run_migrations_online():
    # ...

    context.configure(
                connection=connection,
                target_metadata=target_metadata,
                sqlalchemy_module_prefix="sqla.",
                # ...
                )

    # ...

In either case, the sa. prefix, or whatever prefix is desired, should also be included in the imports section of script.py.mako; it also defaults to import sqlalchemy as sa.

For user-defined types, that is, any custom type that is not within the sqlalchemy. module namespace, by default Alembic will use the value of __module__ for the custom type:

Column("my_column", myapp.models.utils.types.MyCustomType())

The imports for the above type again must be made present within the migration, either manually, or by adding it to script.py.mako.

一句话概括,官方说了,这个字段不是自定义module,要自定义自己去改script.py.mako模板文件。

    问题终结,我们可以在db init 后生成的migrations文件夹下面找到script.py.mako文件,把我们自定义的类型路径放进去。之后生成的版本文件都会自动引入。

 

第三坑,当使用sqlite时的alter

    这次坑的是sqlite,主要是sqlite自己太坑。它不支持drop列,不支持完整的数据库alter语句功能等等。当使用sqlite做migrate和upgrade以及downgrade时,模型有字段的减少或者约束的减少,会报错:

        No support for ALTER of constraints in SQLite dialect

        解决方法:

        初始化Migrate对象时可以设置其提交类型为批处理

        migrate = Migrate(db=db, render_as_batch=True)

        以批处理的方式做表结构修改sqlite是支持的,但是批处理本身有安全性的问题。批处理提升了效率,但是丧失了原子性,其中某些sql语句出错我们并不能直接感知到。所以尽量对migrate生成的脚本进行人工检查吧,不怕一万就怕万一。

 

第四坑,字段长度等属性的修改

       FlaskMigrate默认对字段属性的变动是不做检查的。所以当发现256长的A字段的不足以支撑功能,我们需要把它变成1000,修改代码后做migrate会发现提示No changes in schema detected.

       解决方法:

       上面提到的alembic的源码的EnvironmentContext类内的configure方法里有个sqlalchemy_module_prefix参数旁边还有一些其他的小参数,这次没有坑爹,比较给力,还是贴一下源码吧:

def configure(
    self,
    connection=None,
    url=None,
    dialect_name=None,
    transactional_ddl=None,
    transaction_per_migration=False,
    output_buffer=None,
    starting_rev=None,
    tag=None,
    template_args=None,
    render_as_batch=False,
    target_metadata=None,
    include_symbol=None,
    include_object=None,
    include_schemas=False,
    process_revision_directives=None,
    compare_type=False,
    compare_server_default=False,
    render_item=None,
    literal_binds=False,
    upgrade_token="upgrades",
    downgrade_token="downgrades",
    alembic_module_prefix="op.",
    sqlalchemy_module_prefix="sa.",
    user_module_prefix=None,
    on_version_apply=None,
    **kw
):
     ...后续不贴了太长了

       这里有一个熟悉的面孔,没错!三号坑里的render_as_batch参数也在这里。同时,还有一个叫做compare_type的参数,默认是False,该参数用于标注是否比较字段属性变化。所以,同三号坑,在初始化Migrate对象的时候加入该参数,如下:

      Migrate(db=db, render_as_batch=True, compare_type=True)

问题解决。

 

第五坑,default默认约束的生成

        sqlalchemy的默认值设置有两种方式,第一种是直接的在Column中设置default,这种方式并不在数据库生成默认值约束,是依靠sqlalchemy自身的功能在生成数据的时候生成默认值。另一种是server_default,这种方式是直接在数据库内生成默认值约束。使用较为麻烦,因为server_default参数只接受str或者unicode这种类型,比如Integer字段也需要设置为字符串,时间戳则需要自己转化,较为麻烦。所以,想要在数据迁移时生成数据库的约束,需要使用server_default而不是default。此外,在四号坑内的源码里,也能看见一个熟脸,就是compare_server_default,默认值是False。

        解决方法:

        1.保证model内所有有默认值的字段的default以server_default形式设置。

        2.初始化Migrate对象时做配置,如下:

       migrate = Migrate(db=db, render_as_batch=True, compare_type=True, compare_server_default=True)

 

 

其他坑尚未开掘,再有发现另行补充...

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值