当前位置: 首页 > 工具软件 > alembic > 使用案例 >

SqlAlchemy + Alembic Migrate

胡夕
2023-12-01

SqlAlchemy+sqlite3+Alembic 数据库迁移踩坑…
NotImplementedError: No support for ALTER of constraints in SQLite dialect.
Constraint must have a name

迁移需求:

1. 因为需求不断变化,所以数据库的字段甚至是table变化会比较大,自动迁移非常重要。
2. 数据库是分布式的,每个user都有自己的db
3. 丢弃迁移生成的文件(对我来说不重要),只需要保证数据库是最新的版本

对于有编程需求的来说,官网文档太折磨人了。。。
我只想设置几个变量来替换默认的参数,但看了几遍文档,似乎并没找到。
唯一简单点的参数就只有script_location和sqlalchemy.url了,但这也仅仅针对于ini文件。
想改环境变量中的script_location,url,我也只能改已经生成的env.py文件了,最好的方式应该是设置好这些环境变量,在生成脚本的时候直接替换默认的环境变量,但由于时间紧迫,只能先用笨办法了,后面有机会再深究…

详细步骤(Function):

1. 先实例化Config,把script_location和sqlalchemy.url传进去 --> setup_config
2. 执行init生成ini文件和env.py以及其他相关文件(在init之前会先清空目录,这里是因为我有特殊需求,一旦version信息被清除,数据库就无法退回了,谨慎操作!!!) --> init
3. 修改env.py 文件
	a. sqlalchemy.url
	b. metadata
	c. 修改alembic.context.configure的参数 render_as_batch=True
	d. *.ini 文件也是要改sqlalchemy.url,不知道为什么config设置了,但是.ini文件中还是默认的url
4. clear alembic version table. 由于迁移相关的文件都是在user目录下面的,所以可能会被删。这些文件里面有个version,每次迁移都会生成一个新的,而且会在数据库建一个alembic_version的table一旦两边的version版本对不上或者本地的version被删除,再迁移的时候就会报错,由于我不需要这些东西,只需要保证数据库是最新的,所以这里我直接暴力删除数据库中上一次alembic_version的数据。注意:这样做风险比较大,有restore需求的不能这样做。-->clear_alembic_version_table
5. revision --> revision 
6. upgrade --> upgrade 

踩坑一: NotImplementedError: No support for ALTER of constraints in SQLite dialect.
batch_mode
这个是sqlite3的坑,普通模式下,删除列之后再迁移就会报这个错,加外键也会。
解决办法:使用批处理模式 batch_mode
在环境变量的configure中设置参数render_as_batch=True,由于没找到怎么设置默认的环境变量并使之生效,所以这里简单粗暴的去改了env.py,[详细步骤]中的3.c。

踩坑二: Constraint must have a name
外网解决办法
当我把坑一的问题解决之后,我以为已经大功告成了,结果又出现另一个报错…
百度了一圈,倒是有解决办法,但都是一模一样,抄的外网论坛的方法,而且是flask-migrate。
但还是有点启发的,目的都一样就是给Base.metadata 设置 naming_convention具体代码在下面models的部分
在models中设置naming_convention之后,env.py import Base之后就生效了

migrate完整代码:

import re
import os
import sys
import shutil
from sqlalchemy import create_engine

from alembic import command
from alembic.config import Config

models_path = os.path.abspath(os.path.dirname(__file__))
sys.path.append(models_path)


class Migration:

    def __init__(self, db_path, script_location):
        self.db_path = db_path
        self.script_location = script_location #path to migratoin script
        self.alem_ini = f"{self.script_location.rstrip('alembic')}/alembic.ini"
        self.alembic_cfg = Config(self.alem_ini)

    def setup_config(self):
        self.alembic_cfg.set_main_option("script_location", self.script_location)
        self.alembic_cfg.set_main_option(
            "sqlalchemy.url", f"sqlite:///{self.db_path}?check_same_thread=False")

    def init(self):
        if os.path.exists(self.script_location):
            shutil.rmtree(self.script_location)
        command.init(self.alembic_cfg, self.script_location)

    def modify_env_file(self):
        alem_env = f"{self.script_location}/env.py"
        alem_url = f"sqlalchemy.url = sqlite:///{self.db_path}?check_same_thread=False\n"
        alem_meta = f"import sys\nsys.path.append('{models_path}')\nfrom models import Base\ntarget_metadata=Base.metadata\n"
        env_para = f"render_as_batch=True, compare_type=True,"
        env_fix_bug = ""
        with open(self.alem_ini)as ini_file:
            ini_lines = ini_file.readlines()
        mdf_ini_lines = [alem_url if i.startswith("sqlalchemy.url") else i for i in ini_lines]
        with open(self.alem_ini, "w") as ini_file:
            ini_file.write("".join(mdf_ini_lines))

        with open(alem_env) as env_file:
            env_lines = env_file.readlines()
        mdf_env_lines = [alem_meta if i.startswith("target_metadata") else i for i in env_lines]
        new_env_lines = []
        for line in mdf_env_lines:
            new_env_lines.append(line)
            if line.strip() == "context.configure(":
                space = re.search(r"\s+", line).group()
                new_env_lines.append(f"{space}    {env_para}\n")
        with open(alem_env, "w") as env_file:
            env_file.write("".join(new_env_lines))

    def revision(self):
        #command.revision(self.alembic_cfg, self.revision_message, autogenerate=True)
        command.revision(self.alembic_cfg, autogenerate=True)

    def upgrade(self):
        command.upgrade(self.alembic_cfg, 'head')

    def clear_alembic_version_table(self):
        engine = create_engine(f"sqlite:///{self.db_path}?check_same_thread=False")
        with engine.begin() as connection:
            result = connection.execute("SELECT COUNT(*) FROM sqlite_master where type ='table' and name = 'alembic_version'")
            if result.first()[0]:
                connection.execute("delete from alembic_version")


def main(db_path, script_location):
    migrate = Migration(db_path, script_location)
    migrate.setup_config()
    migrate.init()
    migrate.modify_env_file()
    migrate.clear_alembic_version_table()
    migrate.revision()
    migrate.upgrade()

models

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()
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"}

Base.metadata.naming_convention=naming_convention
 类似资料: