使用 Alembic 管理 Monorepo 中多个服务的数据库迁移

简介

随着现代软件系统变得越来越复杂,许多组织开始采用微服务架构,将单体应用程序分解为更小、独立且可扩展的组件。每个微服务都专注于特定领域,独立运行,并且可以自主开发、部署和扩展。这种方法可以提高敏捷性、简化开发工作流程,并使团队能够更快地交付功能。

另一方面,跨分布式存储库管理多个微服务可能会成为一场后勤噩梦。当团队不断在存储库之间切换时,依赖管理、版本控制、共享代码和部署管道可能会很快失控。

这时 monorepo(单体存储库)便可以发挥作用。monorepo 将所有微服务整合到一个存储库中,从而实现无缝协作、共享工具和跨团队的一致版本控制。通过将微服务架构与 monorepo 相结合,您可以在模块化和可管理性之间取得平衡。

然而,这种方法带来了新的挑战,尤其是在管理多个微服务的数据库迁移时。每个服务通常拥有自己的数据库模式,保持这些迁移的隔离和无冲突对于确保稳定性和可扩展性至关重要。

在本文中,我们将深入探讨如何有效管理具有多个微服务的 monorepo 中的数据库迁移,并利用 Alembic 等工具简化流程。无论您是刚开始在 monorepo 中使用微服务,还是希望改进现有设置,本指南都将提供您所需的见解和工具。

为什么这是 Monorepos 中的一个挑战?

假设你正在开发一个包含两个服务的 monorepo:

1. ServiceOne – 处理一组任务。

2. ServiceTwo – 处理另一组任务。

每个服务都有自己的数据库架构和自己的迁移集。首先,您可以将所有迁移放在一个地方,如下所示:

monorepo/
│
├── migrations/
│   └── versions/
│       ├── 2024_06_15_create_logs_table.py
│       └── 2024_06_20_add_report_table.py
│
└── services/
    ├── service_one/
    └── service_two/

但这很快就会变得令人困惑:

• 哪个迁移属于哪个服务?

• 如何只对一项服务运行迁移而不影响其他服务?

• 当两种服务同时发展时,如何避免冲突?

我们需要一种方法来隔离每个服务的迁移,同时将所有内容保留在同一个 monorepo 保护伞下。

策略:单独迁移,动态配置

输入 Alembic,一个用于管理基于 SQLAlchemy 的数据库迁移的强大工具,它将通过以下方式帮助我们解决这个问题:

1. 为每个服务创建单独的迁移目录。

2.配置alembic.ini,指定每个服务的迁移路径。

3.使用动态env.py根据服务加载正确的数据库URL和迁移配置。

**清晰的文件夹结构**

以下是我们将使用的文件夹结构:

monorepo/
│
├── alembic.ini                # Global Alembic configuration file
├── migrations/
│   ├── env.py                 # Shared migration environment script
│   └── versions/
│       ├── service_one/
│       │   ├── 2024_06_15_init_schema.py
│       │   └── 2024_06_20_add_field.py
│       │
│       └── service_two/
│           ├── 2024_06_18_create_tables.py
│           └── 2024_06_25_update_schema.py
│
└── services/
    ├── service_one/
    │   ├── config.py          
    │   └── models.py
    │   └── ...
    │
    └── service_two/
        ├── config.py         
        └── models.py   
        └── ...

**此结构实现的功能**

• 每个服务都有自己专用的迁移文件。

• 您可以轻松查看哪些迁移属于哪些服务。

• 服务之间不再混淆或重叠!

**配置 alembic.ini**

alembic.ini 文件需要知道在哪里找到每个服务的迁移。以下是示例配置:

[alembic]
sqlalchemy.url = driver://user:pass@localhost/defaultdb

databases = service_one, service_two

[DEFAULT]
script_location = migrations

[service_one]
version_locations = ./migrations/versions/service_one
script_location = ./migrations

[service_two]
version_locations = ./migrations/versions/service_two
script_location = ./migrations

**分解**

• 后备 URL:[alembic] 部分提供了默认数据库 URL。

• 多个数据库:数据库键列出服务。

• 服务配置:每个 [service_one] 和 [service_two] 部分指定:

  • version_locations:在哪里可以找到该服务的迁移。
  • script_location:Alembic 脚本的基本路径。
  • **Monorepos 的动态 env.py**

    现在让我们让 env.py 变得灵活,以便它可以动态处理多项服务。

    这是 env.py 的关键部分:

    from alembic import context
    import importlib
    
    # Load Alembic config
    config = context.config
    
    # Get the target service from the config section
    service = config.config_ini_section
    
    try:
        # Dynamically import the DB_URL from the service's config
        service_module = importlib.import_module(f"services.{service}.config")
        db_url = service_module.DB_URL.render_as_string(hide_password=False)
        config.set_main_option("sqlalchemy.url", db_url)
    except ImportError:
        raise ValueError(f"Invalid service '{service}' specified.")

    **工作原理**

    1. 服务检测:Alembic 从 alembic.ini 部分读取服务名称。

    2. 动态导入:从该服务的 config.py 导入数据库 URL(DB_URL)。

    3. 设置数据库URL:动态设置正确的URL供Alembic使用。

    这意味着您现在可以为不同的服务运行迁移而无需更改任何代码!

    **运行迁移**

    为 ServiceOne 创建迁移

    `alembic -n service_one revision --autogenerate -m "ServiceOne 的初始模式"`

    **应用迁移**

    升级 ServiceOne 的数据库:

    `alembic -n service_one 升级头`

    降级 ServiceTwo 的数据库:

    `alembic -n service_two 降级基础`

    **为什么这种方法有效**

    1. **隔离性**:每个服务独立管理自己的数据库模式。

    2. **灵活性**:添加新服务?只需在 alembic.ini 中创建一个新部分和一个新的迁移文件夹(可以自动生成)。

    3. **可扩展性**:无论您有 2 个服务还是 20 个服务,都可以无缝运行。

    最后的想法

    在 monorepo 中管理数据库迁移不必混乱。通过分离迁移并使用动态配置,您可以让一切井然有序、易于维护且可扩展。

    你尝试过这种方法吗?你在 monorepo 中管理迁移时遇到了哪些挑战?我很想在评论中听到你的想法和经验!

    祝你编码愉快!🚀