在烧瓶迁移或 alembic 迁移中创建种子数据

Posted

技术标签:

【中文标题】在烧瓶迁移或 alembic 迁移中创建种子数据【英文标题】:Creating seed data in a flask-migrate or alembic migration 【发布时间】:2013-10-20 12:08:08 【问题描述】:

如何在我的第一次迁移中插入一些种子数据?如果迁移不是最好的地方,那么最佳做法是什么?

"""empty message

Revision ID: 384cfaaaa0be
Revises: None
Create Date: 2013-10-11 16:36:34.696069

"""

# revision identifiers, used by Alembic.
revision = '384cfaaaa0be'
down_revision = None

from alembic import op
import sqlalchemy as sa


def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.create_table('list_type',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.String(length=80), nullable=False),
    sa.PrimaryKeyConstraint('id'),
    sa.UniqueConstraint('name')
    )
    op.create_table('job',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('list_type_id', sa.Integer(), nullable=False),
    sa.Column('record_count', sa.Integer(), nullable=False),
    sa.Column('status', sa.Integer(), nullable=False),
    sa.Column('sf_job_id', sa.Integer(), nullable=False),
    sa.Column('created_at', sa.DateTime(), nullable=False),
    sa.Column('compressed_csv', sa.LargeBinary(), nullable=True),
    sa.ForeignKeyConstraint(['list_type_id'], ['list_type.id'], ),
    sa.PrimaryKeyConstraint('id')
    )
    ### end Alembic commands ###

    # ==> INSERT SEED DATA HERE <==


def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('job')
    op.drop_table('list_type')
    ### end Alembic commands ###

【问题讨论】:

对文档的小幅更新显示了如何创建表,然后立即从创建的表中批量插入:alembic.readthedocs.org/en/latest/… 在创建种子数据方面,您可以查看github.com/FactoryBoy/factory_boy和github.com/heavenshell/py-sqlalchemy_seed 另见github.com/klen/mixer 【参考方案1】:

如果您希望使用单独的函数来播种数据,您可以执行以下操作:

from alembic import op
import sqlalchemy as sa

from models import User

def upgrade():
    op.create_table('users',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('name', sa.String(length=80), nullable=False),
        sa.PrimaryKeyConstraint('id'),
        sa.UniqueConstraint('name')
    )

    # data seed
    seed()


def seed():
    op.bulk_insert(User.__table__,
        [
            'name': 'user1',
            'name': 'user2',
            ...
        ]
    )

这样,您无需将create_table 的返回值保存到单独的变量中,然后将其传递给bulk_insert

【讨论】:

【参考方案2】:

您还可以使用 Python 的 faker 库,它可能会更快一些,因为您不需要自己提供任何数据。配置它的一种方法是将一个方法放入您想要为其生成数据的类中,如下所示。

from extensions import bcrypt, db

class User(db.Model):
    # this config is used by sqlalchemy to store model data in the database
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(150))
    email = db.Column(db.String(100), unique=True)
    password = db.Column(db.String(100))

    def __init__(self, name, email, password, fav_movie):
        self.name = name
        self.email = email
        self.password = password

    @classmethod
    def seed(cls, fake):
        user = User(
            name = fake.name(),
            email = fake.email(),
            password = cls.encrypt_password(fake.password()),
        )
        user.save()

    @staticmethod
    def encrypt_password(password):
        return bcrypt.generate_password_hash(password).decode('utf-8')

    def save(self):
        db.session.add(self)
        db.session.commit()

然后实现一个调用种子方法的方法,看起来像这样:

from faker import Faker
from users.models import User

fake = Faker()
    for _ in range(100):
        User.seed(fake)

【讨论】:

当你有一个外键列时,你会如何使用 faker 来做到这一点? @rasen58 如果您查看我的 init 方法,它只会在插入数据库之前创建名称、电子邮件和密码。 ids 是在插入过程中创建的,并在从数据库中检索记录时由应用程序使用。【参考方案3】:

MarkHildreth 很好地解释了 alembic 如何处理这个问题。但是,OP 专门关于如何修改烧瓶迁移迁移脚本。我将在下面发布一个答案,以节省人们不得不研究 Alembic 的时间。

警告 米格尔的回答对于正常的数据库信息是准确的。也就是说,应该听从他的建议,绝对不要使用这种方法来用“正常”行填充数据库。这种方法专门针对应用程序运行所需的数据库行,一种我认为是“种子”数据的数据。

OP 的脚本修改为种子数据:

"""empty message

Revision ID: 384cfaaaa0be
Revises: None
Create Date: 2013-10-11 16:36:34.696069

"""

# revision identifiers, used by Alembic.
revision = '384cfaaaa0be'
down_revision = None

from alembic import op
import sqlalchemy as sa


def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    list_type_table = op.create_table('list_type',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.String(length=80), nullable=False),
    sa.PrimaryKeyConstraint('id'),
    sa.UniqueConstraint('name')
    )
    op.create_table('job',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('list_type_id', sa.Integer(), nullable=False),
    sa.Column('record_count', sa.Integer(), nullable=False),
    sa.Column('status', sa.Integer(), nullable=False),
    sa.Column('sf_job_id', sa.Integer(), nullable=False),
    sa.Column('created_at', sa.DateTime(), nullable=False),
    sa.Column('compressed_csv', sa.LargeBinary(), nullable=True),
    sa.ForeignKeyConstraint(['list_type_id'], ['list_type.id'], ),
    sa.PrimaryKeyConstraint('id')
    )
    ### end Alembic commands ###


    op.bulk_insert(
        list_type_table,
        [
            'name':'best list',
            'name': 'bester list'
        ]
    )


def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('job')
    op.drop_table('list_type')
    ### end Alembic commands ###

flask_migrate 新手的背景

Flask migrate 在migrations/versions 生成迁移脚本。这些脚本在数据库上按顺序运行,以便将其升级到最新版本。 OP 包含这些自动生成的迁移脚本之一的示例。为了添加种子数据,必须手动修改相应的自动生成的迁移文件。我上面发布的代码就是一个例子。

发生了什么变化?

很少。您会注意到,在新文件中,我将从create_table 返回的表用于list_type 存储在一个名为list_type_table 的变量中。然后我们使用op.bulk_insert 对该表进行操作以创建一些示例行。

【讨论】:

【参考方案4】:

Alembic 的运营之一是bulk_insert()。文档给出了以下示例(我已经包含了一些修复):

from datetime import date
from sqlalchemy.sql import table, column
from sqlalchemy import String, Integer, Date
from alembic import op

# Create an ad-hoc table to use for the insert statement.
accounts_table = table('account',
    column('id', Integer),
    column('name', String),
    column('create_date', Date)
)

op.bulk_insert(accounts_table,
    [
        'id':1, 'name':'John Smith',
                'create_date':date(2010, 10, 5),
        'id':2, 'name':'Ed Williams',
                'create_date':date(2007, 5, 27),
        'id':3, 'name':'Wendy Jones',
                'create_date':date(2008, 8, 15),
    ]
)

还要注意,alembic 有一个 execute() 操作,这就像 SQLAlchemy 中的普通 execute() 函数:您可以运行任何您想要的 SQL,如文档示例所示:

from sqlalchemy.sql import table, column
from sqlalchemy import String
from alembic import op

account = table('account',
    column('name', String)
)
op.execute(
    account.update().\
        where(account.c.name==op.inline_literal('account 1')).\
        values('name':op.inline_literal('account 2'))
        )

请注意,用于创建update 语句中使用的元数据的表是直接在架构中定义的。这看起来像是破坏了DRY(不是您的应用程序中已经定义的表),但实际上是非常必要的。如果您尝试使用属于应用程序一部分的表或模型定义,那么当您在应用程序中对表/模型进行更改时,您将中断此迁移。您的迁移脚本应该一成不变:对模型未来版本的更改不应更改迁移脚本。使用应用程序模型意味着定义将根据您检出的模型版本(很可能是最新版本)而改变。因此,您需要在迁移脚本中独立包含表定义。

要讨论的另一件事是您是否应该将种子数据放入作为自己的命令运行的脚本中(例如使用 Flask-Script 命令,如另一个答案所示)。这可以使用,但你应该小心它。如果您加载的数据是测试数据,那是一回事。但我将“种子数据”理解为应用程序正常工作所需的数据。例如,如果您需要在“roles”表中设置“admin”和“user”的记录。这些数据应该作为迁移的一部分插入。请记住,脚本仅适用于数据库的最新版本,而迁移将适用于您要迁移到或从中迁移的特定版本。如果您想要一个脚本来加载角色信息,您可能需要一个脚本用于每个版本的数据库,并为“角色”表提供不同的架构。

此外,通过依赖脚本,您会更难以在迁移之间运行脚本(例如迁移 3->4 要求初始迁移中的种子数据位于数据库中)。您现在需要修改 Alembic 的默认运行方式来运行这些脚本。这仍然没有忽略这些脚本必须随着时间而改变的问题,以及谁知道您从源代码管理中签出的应用程序版本。

【讨论】:

bulk_insert() 有相反的含义吗?我相信没有,这会使编写downgrade 变得更加困难。即使有bulk_delete,如果数据被应用程序更改并且看起来与bulk_insert插入时完全不同,你会怎么做?只有在同一次迁移中添加表时降级才是安全的,因为在这种情况下您无论如何都必须删除表,但其他情况不容易解决。不过,我觉得没有必要否决您的回答。 如果bulk_insert 在创建表时完成(通常是这种情况),删除表就足够了。否则,您可以使用execute 删除。以这种方式使用 alembic 不是问题,这是数据库迁移的问题。它们并不容易,而且没有任何工具能让它们变得简单(只会让它们变得更容易)。另外,在添加评论后,我从您的答案中删除了我的反对票。没有难过的感觉:) @MarkHildreth 我采用了您的方法,因为我在此迁移中存储在表中的所有内容都是必需的常量,并且此表是只读的。我同意临时表非常不干燥。谢谢!!! @MarkHildreth 我正在尝试将其放入 manage.py 脚本中,但我不断收到这个神秘的脚本:NameError: Can't invoke function 'create_table', as the proxy object has not yet been established for the Alembic 'Operations' class. Try placing this code inside a callable. 你知道这意味着什么吗? @lol 我建议为此在 *** 上创建一个新问题。【参考方案5】:

迁移应仅限于架构更改,不仅如此,重要的是,在应用向上或向下迁移时,尽可能保留以前存在于数据库中的数据。在迁移过程中插入种子数据可能会弄乱预先存在的数据。

与 Flask 的大多数事情一样,您可以通过多种方式实现这一点。在我看来,向 Flask-Script 添加一个新命令是一个很好的方法。例如:

@manager.command
def seed():
    "Add seed data to the database."
    db.session.add(...)
    db.session.commit()

然后你运行:

python manager.py seed

【讨论】:

对不起,有点高兴,但我强烈不同意这一点:“迁移应仅限于架构更改”。如果您想让种子数据成为单独的命令,答案很好。但是,例如,如果您想安装“角色”(管理员、用户等)之类的东西,那么在迁移中这样做是完全可以的。实际上,添加命令而不是将其放入迁移中意味着您现在必须在安装过程中执行两个步骤(迁移、数据加载)而不是一个步骤。根据您的环境,选择任何一种方式。但请不要说迁移应该是“有限的”。 @MarkHildreth:我将数据与迁移分开的原因是架构更改具有与应用程序分开的非常明确的历史记录,但数据没有,因为应用程序可以访问它并且可以更改它.我想对于只读表来说这不是问题,但我不建议将其作为一般做法。 @Miguel:我想这真的归结为我们正在谈论的数据。但是,正如我在回答中所说,我对“种子数据”的定义是运行应用程序所需的数据(常量、管理员用户等)。因此,正如我在回答中所解释的那样,必要的数据应该具有与架构同步的明确定义的历史记录。 虽然您的回答得到了很多人的支持,但这个建议很容易导致很多人走错路。某些数据是迁移的适当部分(Hildreth 在他的回答中描述的那种数据)。您的论点忽略了这种数据的存在。恕我直言,请编辑您的答案以明确您在谈论哪种数据,并删除您关于种子数据不应成为迁移的一部分的黑白断言。 (我对您在烧瓶迁移方面所做的工作深表感谢) 迁移应该仅限于架构更改。 Evolutionary Database Design.

以上是关于在烧瓶迁移或 alembic 迁移中创建种子数据的主要内容,如果未能解决你的问题,请参考以下文章

使用alembic为SQLAlchemy迁移数据

4.alembic数据迁移工具

没有数据库的 Alembic 迁移

四十七:数据库之alembic数据库迁移工具的基本使用

sqlalchemy + alembic数据迁移

在 MikroORM 中创建迁移时,如何解决“请在 EntityName.id 中提供 'type' 或 'entity' 属性”错误?