在flask-migrate或alembic迁移中创建种子数据

Mar*_*man 43 python flask flask-sqlalchemy alembic flask-migrate

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

"""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 ###
Run Code Online (Sandbox Code Playgroud)

Mar*_*eth 66

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)},
    ]
)
Run Code Online (Sandbox Code Playgroud)

还要注意,alembic有一个execute()操作,就像execute()SQLAlchemy中的普通函数一样:你可以运行你想要的任何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')})
        )
Run Code Online (Sandbox Code Playgroud)

请注意,用于创建update语句中使用的元数据的表直接在架构中定义.这似乎打破了DRY(不是您的应用程序中已定义的表),但实际上非常必要.如果您尝试使用属于应用程序的表或模型定义,则在应用程序中对表/模型进行更改时,将会中断此迁移.您的迁移脚本应该一成不变:对模型的未来版本的更改不应更改迁移脚本.使用应用程序模型意味着定义将根据您检出的模型版本(最可能是最新版本)而改变.因此,您需要在迁移脚本中自定义表定义.

另一件要说的是你是否应该将种子数据放入一个作为自己的命令运行的脚本(例如使用Flask-Script命令,如另一个答案中所示).这可以使用,但你应该小心.如果您加载的数据是测试数据,那么这是一回事.但我理解"种子数据"是指应用程序正常工作所需的数据.例如,如果您需要在"角色"表中为"admin"和"user"设置记录.该数据应该作为迁移的一部分插入.请记住,脚本只能与最新版本的数据库一起使用,而迁移将与您要迁移到的特定版本一起使用.如果您希望脚本加载角色信息,

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

  • 如果在创建表时(通常是这种情况)完成`bulk_insert`,则删除表就足够了.否则,您可以使用`execute`删除.这不是以这种方式使用alembic的问题,这是数据库迁移的问题.它们并不容易,也没有任何工具可以让它们变得简单(只是让它们更容易).另外,在我添加评论后,我从你的回答中删除了我的downvote.别往心里放 :) (3认同)
  • 有没有与'bulk_insert()`相反?我相信没有,这会使写"降级"更难.即使有一个bulk_delete,如果数据被应用程序更改并且看起来与`bulk_insert`插入时完全不同,你会怎么做?如果在同一个迁移中添加了表,那么降级是安全的,因为在这种情况下你必须删除表,但其他情况并不容易解决.不过,我觉得不需要对你的答案进行投票. (2认同)

Mig*_*uel 25

迁移应仅限于模式更改,不仅如此,重要的是,当应用向上或向下迁移时,数据库中存在的数据将尽可能保留.作为迁移的一部分插入种子数据可能会破坏预先存在的数据.

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

@manager.command
def seed():
    "Add seed data to the database."
    db.session.add(...)
    db.session.commit()
Run Code Online (Sandbox Code Playgroud)

那么你运行:

python manager.py seed
Run Code Online (Sandbox Code Playgroud)

  • 对不起,有点引发高兴,但我强烈反对这一点:"迁移应仅限于架构更改".如果要将种子数据作为单独的命令,答案很好.但是,例如,如果您想安装"角色"(管理员,用户等)之类的东西,那么完全可以进行迁移.实际上,添加命令而不是将其放入迁移意味着您现在必须在安装过程中执行两个步骤(迁移,数据加载)而不是一个步骤.根据您的环境,选择其中一种方式.但请不要说迁移应该"有限". (22认同)
  • @Miguel:我想这真的归结为我们所谈论的数据.但是,正如我在回答中所说,我对"种子数据"的定义是运行应用程序所需的数据(常量,管理员用户等).因此,正如我在答案中所解释的那样,必要的数据应该具有与模式同步的明确定义的历史记录. (14认同)
  • @MarkHildreth:我将数据与迁移分开的原因是模式更改具有与应用程序分离的非常明确的历史记录,但数据没有,因为应用程序可以访问它并可以更改它.我想对于一个只读表这不会是一个问题,但这是我不推荐的一般做法. (9认同)
  • 迁移不应仅限于架构更改.[进化数据库设计](https://martinfowler.com/articles/evodb.html). (4认同)
  • 尽管您的回答得到了很多人的支持,但这个建议很容易导致很多人走上错误的道路。某些数据是迁移的适当组成部分(Hildreth 在他的回答中描述的数据类型)。您的论点忽略了这种数据的存在。尊重地,请编辑您的答案以明确您所谈论的数据类型,并删除您的黑白断言,即种子数据不应成为迁移的一部分。(我深表感谢您对 Flask-migrate 的总体工作) (2认同)

Bra*_*olt 6

您还可以使用 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()
Run Code Online (Sandbox Code Playgroud)

然后实现一个调用种子方法的方法,该方法可能如下所示:

from faker import Faker
from users.models import User

fake = Faker()
    for _ in range(100):
        User.seed(fake)
Run Code Online (Sandbox Code Playgroud)


mel*_*r55 5

MarkHildreth提供了一个很好的解释,说明了Alembic如何处理此问题。但是,OP专门涉及如何修改flask-migration迁移脚本。我将在下面发布该问题的答案,以使人们不必再花些时间去研究除菌剂。

警告 Miguel的答案相对于常规数据库信息而言是准确的。就是说,应该遵循他的建议,绝对不要使用这种方法来用“正常”行填充数据库。这种方法专门用于应用程序运行所需的数据库行,我认为这是一种“种子”数据。

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 ###
Run Code Online (Sandbox Code Playgroud)

烧瓶新手的上下文

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

发生了什么变化?

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