[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"public:announcements":3,"footer:categories:zh":4,"public:navigations:zh":132,"post:detail:post-32-Alembic深度:zh":177},[],[5,23,39,55,71,88,103,117],{"id":6,"name":7,"slug":12,"description":13,"icon":18,"color":19,"cover_image":20,"created_at":21,"post_count":22},1,{"en":8,"ja":9,"zh":10,"zh_Hant":11},"Technology","テクノロジー","技术","技術","technology",{"en":14,"ja":15,"zh":16,"zh_Hant":17},"Cutting-edge technology exploration and in-depth analysis covering programming languages, frameworks, and architecture design","プログラミング言語、フレームワーク、アーキテクチャ設計を含む先端技術の探究","前沿技术探索与深度分析，涵盖编程语言、框架、架构设计等核心领域","前沿技術探索與深度分析，涵蓋編程語言、框架、架構設計等核心領域","heroicons:cog-6-tooth","#6366f1",null,"2026-09-30T18:09:53.633610Z",8,{"id":24,"name":25,"slug":29,"description":30,"icon":35,"color":36,"cover_image":20,"created_at":37,"post_count":38},2,{"en":26,"ja":27,"zh":28,"zh_Hant":28},"Tutorial","チュートリアル","教程","tutorial",{"en":31,"ja":32,"zh":33,"zh_Hant":34},"Hands-on tutorials from scratch with clear steps and complete code for developers at all levels","ゼロから始める実践的なチュートリアル。明確な手順と完全なコードで、全てのレベルの開発者に最適","从零开始的实战教程，步骤清晰，代码完整，适合各阶段开发者学习","從零開始的實戰教程，步驟清晰，代碼完整，適合各階段開發者學習","heroicons:academic-cap","#10b981","2026-09-30T18:09:53.641581Z",6,{"id":40,"name":41,"slug":45,"description":46,"icon":51,"color":52,"cover_image":20,"created_at":53,"post_count":54},3,{"en":42,"ja":43,"zh":44,"zh_Hant":44},"Frontend","フロントエンド","前端","frontend",{"en":47,"ja":48,"zh":49,"zh_Hant":50},"Full coverage of web frontend development including Vue, React, new CSS features, and performance optimization","Vue、React、CSSの新機能やパフォーマンス最適化を含むWebフロントエンド開発の全領域","Web前端开发全栈覆盖，包括Vue、React、CSS新特性及性能优化等话题","Web前端開發全棧覆蓋，包括Vue、React、CSS新特性及性能優化等話題","heroicons:computer-desktop","#ec4899","2026-09-30T18:09:53.644838Z",7,{"id":56,"name":57,"slug":62,"description":63,"icon":68,"color":69,"cover_image":20,"created_at":70,"post_count":40},4,{"en":58,"ja":59,"zh":60,"zh_Hant":61},"Backend","バックエンド","后端","後端","backend",{"en":64,"ja":65,"zh":66,"zh_Hant":67},"Server-side development best practices including API design, database optimization, and microservices architecture","API設計、データベース最適化、マイクロサービスアーキテクチャを含むサーバーサイド開発の実践","服务端开发实战经验，API设计、数据库优化、微服务架构等深度内容","服務端開發實戰經驗，API設計、數據庫優化、微服務架構等深度內容","heroicons:server-stack","#f59e0b","2026-09-30T18:09:53.647836Z",{"id":72,"name":73,"slug":78,"description":79,"icon":84,"color":85,"cover_image":20,"created_at":86,"post_count":87},5,{"en":74,"ja":75,"zh":76,"zh_Hant":77},"Fullstack","フルスタック","全栈","全棧","fullstack",{"en":80,"ja":81,"zh":82,"zh_Hant":83},"End-to-end development guides covering the full project lifecycle and technology stack selection","フロントエンドからバックエンドまでの開発プロセス全体をカバーするガイド","从前端到后端的全流程开发指南，覆盖完整项目生命周期与技术选型","從前端到後端的全流程開發指南，覆蓋完整項目生命週期與技術選型","heroicons:swatch","#8b5cf6","2026-09-30T18:09:53.650513Z",0,{"id":38,"name":89,"slug":94,"description":95,"icon":100,"color":101,"cover_image":20,"created_at":102,"post_count":56},{"en":90,"ja":91,"zh":92,"zh_Hant":93},"Essays","エッセイ","随笔","隨筆","essays",{"en":96,"ja":97,"zh":98,"zh_Hant":99},"Technical reflections and development insights recording the programmer's growth journey","プログラマーの成長の軌跡を記録する技術的考察と開発の洞察","技术思考与开发心得分享，记录程序员成长路上的点滴感悟","技術思考與開發心得分享，記錄程序員成長路上的點滴感悟","heroicons:pencil-square","#f43f5e","2026-09-30T18:09:53.653105Z",{"id":54,"name":104,"slug":108,"description":109,"icon":114,"color":115,"cover_image":20,"created_at":116,"post_count":56},{"en":105,"ja":106,"zh":107,"zh_Hant":107},"Tools","ツール","工具","tools",{"en":110,"ja":111,"zh":112,"zh_Hant":113},"Development toolchain and productivity tool recommendations to supercharge your development experience","開発効率を飛躍的に向上させるツールチェーンと生産性ツールの推奨","开发工具链与效率提升利器推荐，让你的开发体验如虎添翼","開發工具鏈與效率提升利器推薦，讓你的開發體驗如虎添翼","heroicons:wrench-screwdriver","#06b6d4","2026-09-30T18:09:53.655605Z",{"id":22,"name":118,"slug":123,"description":124,"icon":129,"color":130,"cover_image":20,"created_at":131,"post_count":87},{"en":119,"ja":120,"zh":121,"zh_Hant":122},"Translation","翻訳","翻译","翻譯","translation",{"en":125,"ja":126,"zh":127,"zh_Hant":128},"Curated translations of high-quality foreign technical articles to keep up with international trends","国際的な技術トレンドを追うための厳選された海外技術記事の翻訳","优质外文技术文章精选翻译，紧跟国际技术前沿动态","優質外文技術文章精選翻譯，緊跟國際技術前沿動態","heroicons:language","#84cc16","2026-09-30T18:09:53.658041Z",[133,144,150,157,164,171],{"id":6,"title":134,"url":139,"icon":20,"parent_id":20,"location":140,"order":6,"is_active":141,"target_blank":142,"created_at":143,"updated_at":20},{"zh":135,"en":136,"ja":137,"zh_TW":138},"首页","Home","ホーム","首頁","\u002F","header",true,false,"2026-09-30T18:09:54.251095Z",{"id":24,"title":145,"url":149,"icon":20,"parent_id":20,"location":140,"order":24,"is_active":141,"target_blank":142,"created_at":143,"updated_at":20},{"zh":146,"en":147,"ja":148,"zh_TW":146},"文章","Posts","記事","\u002Fposts",{"id":40,"title":151,"url":156,"icon":20,"parent_id":20,"location":140,"order":40,"is_active":141,"target_blank":142,"created_at":143,"updated_at":20},{"zh":152,"en":153,"ja":154,"zh_TW":155},"分类","Categories","カテゴリー","分類","\u002Fcategories",{"id":56,"title":158,"url":163,"icon":20,"parent_id":20,"location":140,"order":56,"is_active":141,"target_blank":142,"created_at":143,"updated_at":20},{"zh":159,"en":160,"ja":161,"zh_TW":162},"标签","Tags","タグ","標籤","\u002Ftags",{"id":72,"title":165,"url":170,"icon":20,"parent_id":20,"location":140,"order":72,"is_active":141,"target_blank":142,"created_at":143,"updated_at":20},{"zh":166,"en":167,"ja":168,"zh_TW":169},"关于","About","概要","關於","\u002Fpage\u002Fabout",{"id":38,"title":172,"url":176,"icon":20,"parent_id":20,"location":140,"order":38,"is_active":141,"target_blank":142,"created_at":143,"updated_at":20},{"zh":173,"en":174,"ja":175,"zh_TW":173},"留言板","Guestbook","ゲストブック","\u002Fpage\u002Fguestbook",{"id":178,"title":179,"subtitle":20,"slug":180,"source":181,"source_url":20,"audio":20,"video":20,"video_url":20,"content":182,"excerpt":183,"cover_image":20,"author":184,"category":187,"tags":188,"status":211,"visibility":212,"password":20,"views":213,"likes_count":87,"is_pinned":142,"allow_comments":141,"comments_count":72,"is_password_protected":142,"meta_title":179,"meta_description":183,"meta_keywords":214,"created_at":215,"published_at":215,"updated_at":216,"reading_time":6},32,"Alembic 深度指南：数据库迁移 + 自动生成 + 分支合并","post-32-Alembic深度","原创","# Alembic 深度指南：数据库迁移 + 自动生成 + 分支合并\n\n数据库 Schema 变更管理是团队协作最容易出事故的一环：手动 DDL 改表忘记录入、生产与本地不一致、多分支合并后 migration 执行死锁。Alembic 作为 SQLAlchemy 官方迁移工具，能自动检测模型变更、版本化、可回滚、支持复杂合并。\n\n## 自动生成与命名约定\n\n命名约定（NAMING_CONVENTION）是多人协作的地基。不加命名约定的话，同一个约束在两台机器 autogenerate 名字随机（如约束名含随机hash），PR 一合并立刻 migration 冲突。env.py 配置 compare_type、compare_server_default 让自动检测更准，避免漏掉 bool 默认值变更、VARCHAR 长度变化这种常见的自动生成漏检。\n\n```python\n```python\n# ====== alembic.ini 核心调优 ======\n[alembic]\nscript_location = migrations\nsqlalchemy.url = postgresql+psycopg:\u002F\u002Fapp:${DB_PASSWORD}@${DB_HOST}:5432\u002Fapp\nprepend_sys_path = .\ntimezone = Asia\u002FShanghai\n\n[loggers]\nkeys = root,sqlalchemy,alembic\n[handlers]\nkeys = console\n[formatters]\nkeys = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n\n# ====== migrations\u002Fenv.py 支持自动生成 + 命名约定 ======\nfrom __future__ import with_statement\nfrom logging.config import fileConfig\nfrom alembic import context\nfrom sqlalchemy import engine_from_config, pool, MetaData\nimport os, sys\n\nsys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '..')))\nfrom app.models import Base\nfrom app.db.naming import NAMING_CONVENTION\n\nconfig = context.config\nif config.config_file_name is not None:\n    fileConfig(config.config_file_name)\n\ntarget_metadata: MetaData = Base.metadata\ntarget_metadata.naming_convention = NAMING_CONVENTION  # 关键：约束自动命名\n\ndef run_migrations_offline() -> None:\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url, target_metadata=target_metadata, literal_binds=True,\n        dialect_opts={\"paramstyle\": \"named\"},\n        render_as_batch=True,  # SQLite 等支持批处理ALTER\n        compare_type=True, compare_server_default=True,\n        include_object=include_object_fn\n    )\n    with context.begin_transaction():\n        context.run_migrations()\n\ndef include_object_fn(object, name, type_, reflected, compare_to):\n    if type_ == \"table\" and name in {\"spatial_ref_sys\", \"alembic_version\"}:\n        return False\n    return True\n\ndef run_migrations_online() -> None:\n    connectable = engine_from_config(\n        config.get_section(config.config_ini_section, {}),\n        prefix=\"sqlalchemy.\", poolclass=pool.NullPool, future=True\n    )\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection, target_metadata=target_metadata,\n            render_as_batch=True,\n            compare_type=True,\n            compare_server_default=True,\n            include_object=include_object_fn,\n            transaction_per_migration=True,  # 每个迁移独立事务\n            lock_timeout=30                   # DDL 锁超时 30s\n        )\n        with context.begin_transaction():\n            context.run_migrations()\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n\n# ====== app\u002Fdb\u002Fnaming.py — 约束命名公约防分支冲突 ======\nNAMING_CONVENTION = {\n    \"ix\": \"ix_%(column_0_label)s\",\n    \"uq\": \"uq_%(table_name)s_%(column_0_N_name)s\",\n    \"ck\": \"ck_%(table_name)s_%(constraint_name)s\",\n    \"fk\": \"fk_%(table_name)s_%(column_0_N_name)s_%(referred_table_name)s\",\n    \"pk\": \"pk_%(table_name)s\",\n}\n\n# ====== 典型迁移 revision 示例：data-only 迁移 ======\n'''${message}\n\nRevision ID: 0004_add_user_status\nRevises: 0003_user_baseline\nCreate Date: 2024-06-15 11:22:33\n\n'''\nfrom alembic import op\nimport sqlalchemy as sa\nfrom sqlalchemy import update\nfrom sqlalchemy.orm import Session\n\n# revision identifiers, used by Alembic.\nrevision = '0004_add_user_status'\ndown_revision = '0003_user_baseline'\nbranch_labels = None\ndepends_on = None\n\ndef upgrade() -> None:\n    op.add_column('users', sa.Column('status', sa.String(16), nullable=False,\n                  server_default='ACTIVE'))\n    op.create_index('ix_users_status_created', 'users', ['status', 'created_at'])\n    bind = op.get_bind(); sess = Session(bind)\n    from app.models import User\n    sess.execute(update(User).where(User.email.endswith('@test.com')).\n                 values(status='INACTIVE'))\n    sess.commit()\n\ndef downgrade() -> None:\n    op.drop_index('ix_users_status_created', 'users')\n    op.drop_column('users', 'status')\n\n# ====== 多分支 HEAD 合并：建一个 merge migration ======\n# 场景: Alice 分支 head=0005_add_posts, Bob 分支 head=0006_add_tags\n# 解决:\n#   alembic merge -m \"merge posts and tags branches\" 0005_add_posts 0006_add_tags\n# 生成:\nrevision = '0007_merge_posts_tags'\ndown_revision = ('0005_add_posts', '0006_add_tags')\nbranch_labels = None\ndepends_on = None\n\ndef upgrade(): pass   # merge 迁移通常空实现，只解决 DAG 头分叉\ndef downgrade(): pass\n```\n```\n\n## 分支合并与生产发布流程\n\nAlice 和 Bob 同时拉两个 feature 分支，各写各的 migration，最后 merge 回 main 就会出现\"两个 head\"（分叉）。标准解法：`alembic merge \u003Chash1> \u003Chash2> -m \"merge A and B\"` 生成一个空的 merge revision，把两条链合成一条，down_revision 写成元组。生产发布：(1) 先备份数据库；(2) 事务+DDL锁超时；(3) 超大表（>100万行）用 CREATE INDEX CONCURRENTLY，禁止单一事务包裹长迁移。\n\n| 场景 | 操作命令 | 注意事项 |\n|------|---------|---------|\n| 检测模型变化生成迁移 | alembic revision --autogenerate -m \"msg\" | 生成后必须人工review生成的up\u002Fdown代码 |\n| 升到最新版本 | alembic upgrade head | CI必须校验 upgrade→downgrade→upgrade 幂等 |\n| 撤回到上一个版本 | alembic downgrade -1 | 数据删除型down要谨慎，最好 data-only 先备份 |\n| 两个 head 分叉合并 | alembic merge head1 head2 -m \"merge\" | merge 迁移通常空实现 |\n| 生成SQL脚本离线部署 | alembic upgrade head --sql | DBA审批流程适用 |\n| 大表索引不锁表建 | op.create_index(..., postgresql_concurrently=True) | 不能在事务里，transaction_per_migration=True |\n\n## 最佳实践\n\n每个迁移拆两类：Schema-only（结构变更，可快速回滚）和 Data-only（数据清洗回填，慎用）。禁止同一 migration 既改表又回写数据。每次 PR 发 CI 检查三件事：upgrade 跑一遍、downgrade 回前一版再 upgrade 一遍（幂等）、和主分支 head 数量一致。","SQLAlchemy 官方迁移工具 Alembic 完全指南：从 env.py 配置、autogenerate 自动检测模型变更（字段、索引、约束）、批处理脚本、downgrade 回滚策略，到多人并发开发时 migration 分支冲突、head 分叉合并的标准操作流程，附生产部署检查表。",{"id":6,"username":185,"nickname":185,"avatar":20,"cover_image":20,"bio":20,"website":20,"github":20,"avatar_source":20,"resolved_avatar_url":20,"title":20,"created_at":186},"Choyeon","2026-09-30T18:09:53.330367Z",{"id":24,"name":28,"slug":29,"description":33,"icon":35,"color":36,"cover_image":20,"created_at":37,"post_count":87},[189,194,199,205],{"id":6,"name":190,"slug":191,"color":192,"icon":20,"is_active":141,"created_at":193,"post_count":87},"Python","python","#3776ab","2026-09-30T18:09:53.663835Z",{"id":24,"name":195,"slug":196,"color":197,"icon":20,"is_active":141,"created_at":198,"post_count":87},"FastAPI","fastapi","#009688","2026-09-30T18:09:53.669567Z",{"id":200,"name":201,"slug":202,"color":203,"icon":20,"is_active":141,"created_at":204,"post_count":87},10,"PostgreSQL","postgresql","#336791","2026-09-30T18:09:53.691803Z",{"id":206,"name":207,"slug":208,"color":209,"icon":20,"is_active":141,"created_at":210,"post_count":87},12,"Git","git","#f05032","2026-09-30T18:09:53.697858Z","published","public",122,"Python,PostgreSQL,Git,FastAPI","2026-09-29T22:09:53.733462Z","2026-10-01T10:32:41.701466Z"]