Создание исходных данных в flask-migrate или alembic migration

Как я могу вставить исходные данные в мою первую миграцию? Если миграция - не лучшее место для этого, то что лучше?

"""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 ###

person Mark Richman    schedule 12.10.2013    source источник
comment
Незначительное обновление документации показывает, как можно создать таблицу, а затем сразу же произвести ее массовую вставку из созданной таблицы: alembic.readthedocs.org/en/latest/   -  person iJames    schedule 03.01.2015
comment
Что касается создания исходных данных, вы можете проверить github.com/FactoryBoy/factory_boy и github.com/heavenshell/py-sqlalchemy_seed   -  person Ben Creasy    schedule 23.10.2017
comment
См. Также github.com/klen/mixer.   -  person Ben Creasy    schedule 23.10.2017


Ответы (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)},
    ]
)

Также обратите внимание, что перегонный куб имеет операцию 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')})
        )

Обратите внимание, что таблица, которая используется для создания метаданных, используемых в операторе update, определена непосредственно в схеме. Может показаться, что это нарушает DRY (таблица еще не определена в вашем приложении), но на самом деле это довольно необходимый. Если вы попытаетесь использовать определение таблицы или модели, которое является частью вашего приложения, вы нарушите эту миграцию, когда внесете изменения в свою таблицу / модель в своем приложении. Ваши сценарии миграции должны быть твердыми: изменение будущих версий ваших моделей не должно изменять сценарии миграции. Использование моделей приложений будет означать, что определения будут меняться в зависимости от того, какую версию моделей вы проверяли (скорее всего, последнюю). Следовательно, вам нужно, чтобы определение таблицы было автономным в сценарии миграции.

Еще одна вещь, о которой стоит поговорить, - следует ли вам помещать свои исходные данные в сценарий, который запускается как отдельная команда (например, с помощью команды Flask-Script, как показано в другом ответе). Это можно использовать, но с этим следует быть осторожным. Если данные, которые вы загружаете, являются тестовыми, это одно. Но я понял, что «исходные данные» означают данные, необходимые для правильной работы приложения. Например, если вам нужно настроить записи для «admin» и «user» в таблице «role». Эти данные ДОЛЖНЫ быть вставлены как часть миграции. Помните, что сценарий будет работать только с последней версией вашей базы данных, тогда как миграция будет работать с конкретной версией, на которую вы выполняете переход. Если вы хотите, чтобы сценарий загружал информацию о ролях, вам может понадобиться сценарий для каждой версии базы данных с другой схемой для таблицы «роли».

Кроме того, полагаясь на сценарий, вам будет сложнее запускать сценарий между миграциями (скажем, миграция 3-> 4 требует, чтобы исходные данные в начальной миграции находились в базе данных). Теперь вам нужно изменить способ запуска Alembic по умолчанию для запуска этих скриптов. И это по-прежнему не игнорирует проблемы, связанные с тем, что эти скрипты со временем должны будут меняться, и кто знает, какую версию вашего приложения вы извлекли из системы контроля версий.

person Mark Hildreth    schedule 12.10.2013
comment
Есть ли реверс bulk_insert()? Я считаю, что нет, поэтому написать downgrade будет сложнее. Даже если было bulk_delete, что делать, если данные были изменены приложением и выглядят совершенно иначе, чем когда они были вставлены bulk_insert? Было бы безопасно перейти на более раннюю версию, только если таблица была добавлена ​​при той же миграции, так как в этом случае вам все равно придется удалить таблицу, но другие случаи не решаются легко. Тем не менее, я не чувствую необходимости понижать ваш ответ. - person Miguel; 13.10.2013
comment
Если bulk_insert выполняется при создании таблицы (что часто бывает), достаточно удалить таблицу. В противном случае вы можете использовать execute для удаления. Это не проблема с использованием алембика таким образом, это проблема с миграцией базы данных. Это непросто, и никакой инструмент не облегчит их (только облегчит). Кроме того, я снял свой отрицательный голос с вашего ответа после того, как добавил свой комментарий. Без обид :) - person Mark Hildreth; 13.10.2013
comment
@MarkHildreth Я придерживался вашего подхода, поскольку все, что я храню в таблице в этой миграции, - это обязательные константы, а эта таблица предназначена только для чтения. Я согласен с тем, что специальная таблица очень НЕ СУХАЯ. Спасибо!!! - person Mark Richman; 13.10.2013
comment
@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. Вы хоть понимаете, что это значит? - person lol; 17.12.2015
comment
@lol Я бы порекомендовал создать для этого новый вопрос в StackOverflow. - person Mark Hildreth; 17.12.2015
comment
@lol, вам нужно запустить этот скрипт через другой скрипт manage.py, например: python manage.py db upgrade пример здесь: blog.miguelgrinberg.com/post/ - person Shoham; 26.05.2016
comment
чтобы получить существующий объект таблицы по имени таблицы, проверьте следующее: stackoverflow.com/a/57609029/10058386 - person Ziyad Sfaxi; 08.01.2021

Миграции следует ограничивать только изменениями схемы, и не только этим, важно, чтобы при применении миграции вверх или вниз, данные, которые существовали в базе данных ранее, сохранялись в максимально возможной степени. Вставка начальных данных как часть миграции может испортить уже существующие данные.

Как и большинство вещей с Flask, вы можете реализовать это разными способами. На мой взгляд, добавление новой команды в Flask-Script - хороший способ сделать это. Например:

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

Итак, вы запускаете:

python manager.py seed
person Miguel    schedule 12.10.2013
comment
Извините, триггер немного радует, но я категорически не согласен с этим: миграции следует ограничивать только изменениями схемы. Ответ хорош, если вы хотите сделать исходные данные отдельной командой. Но, например, если вы хотите установить такие вещи, как роли (администратор, пользователь и т. Д.), Тогда это вполне нормально сделать при миграции. Фактически, добавление команды вместо того, чтобы помещать ее в миграцию, означает, что теперь вы должны в рамках своей установки выполнить два шага (миграция, загрузка данных) вместо одного. В зависимости от вашей среды, выберите любой вариант. Но, пожалуйста, не говорите, что миграции следует ограничивать. - person Mark Hildreth; 12.10.2013
comment
Хорошо, Марк, как бы вы это сделали в рамках вышеуказанной миграции? - person Mark Richman; 12.10.2013
comment
@MarkHildreth: мои аргументы в пользу хранения данных отдельно от миграций заключаются в том, что изменения схемы имеют довольно хорошо определенную историю, отдельную от приложения, а данные - нет, поскольку приложение имеет к ним доступ и может его изменять. Я предполагаю, что для таблицы только для чтения это не будет проблемой, но я бы не рекомендовал это в качестве общей практики. - person Miguel; 12.10.2013
comment
@Miguel: Думаю, все сводится к тому, о каких данных мы говорим. Однако, как я сказал в своем ответе, мое определение исходных данных - это данные, необходимые для запуска приложения (константы, пользователи-администраторы и т. Д.). Следовательно, необходимые данные ДОЛЖНЫ иметь четко определенную историю, которая соответствует схеме, как я объяснил в своем ответе. - person Mark Hildreth; 13.10.2013
comment
Да согласен; Я хочу добавить в приложение список валют, это единственный способ заполнить таблицу валют. Кажется совершенно естественным связать это с созданием указанной таблицы. - person Rob Grant; 06.02.2016
comment
Несмотря на то, что ваш ответ получил множество положительных отзывов, этот совет легко может увести многих людей по ложному пути. Определенные данные, соответственно, являются частью миграции (тип данных, которые Хилдрет описывает в своем ответе). Ваш аргумент игнорирует существование такого рода данных. С уважением, отредактируйте свой ответ, чтобы прояснить, о каких данных вы говорите, и удалите свое черно-белое утверждение о том, что исходные данные не должны быть частью миграций. (и большое спасибо за вашу работу над flask-migrate в целом) - person melchoir55; 02.03.2017
comment
@MarkHildreth согласен. - person ; 23.05.2017
comment
Миграции не должны ограничиваться только изменениями схемы. Эволюционный дизайн базы данных. - person turdus-merula; 21.10.2017
comment
@MarkHildreth, а как насчет середины миграции вы хотели добавить миграцию данных только для среды разработки. Таким образом, добавление этой миграции данных при разработке и запуске flask db upgrade также добавит эти фиктивные данные в рабочую среду, которые предназначены только для промежуточной обработки. Как мы можем пропустить это, если мы включим это в миграцию схемы? - person Roel; 07.11.2019
comment
@Roel Я не уверен, что понимаю ваш вопрос, но если я это сделаю, и выполнение миграции схемы для данных доставит вам трудности, не делайте это миграцией схемы. Вместо этого сделайте миграцию данных. Вдобавок я добавлю кое-что, о чем хотел бы знать шесть лет назад: неважно, как вы это делаете. Оглядываясь назад, я съеживаюсь от того, насколько я категорически не согласен с ответом Мигеля (шесть лет спустя, я хотел бы извиниться). Любой из способов работает, и, скорее всего, миграции - это не то, что делает наши проекты / продукты интересными / уникальными. - person Mark Hildreth; 07.11.2019

MarkHildreth предоставил прекрасное объяснение того, как перегонный куб может с этим справиться. Однако OP был конкретно о том, как изменить сценарий миграции flask-migration. Я собираюсь опубликовать ответ на этот вопрос ниже, чтобы избавить людей от необходимости вообще заглядывать в перегонный куб.

Предупреждение. Ответ Мигеля верен в отношении обычной информации из базы данных. То есть следует следовать его совету и категорически не использовать этот подход для заполнения базы данных «нормальными» строками. Этот подход специально предназначен для строк базы данных, которые необходимы для работы приложения, типа данных, которые я считаю «исходными» данными.

Сценарий 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, чтобы создать несколько примеров строк.

person melchoir55    schedule 02.03.2017

Вы также можете использовать библиотеку 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)
person Braden Holt    schedule 18.06.2018
comment
Как бы вы использовали faker для этого, если у вас есть столбец внешнего ключа? - person rasen58; 31.05.2020
comment
@ rasen58, если вы посмотрите на мой метод инициализации, он только создает имя, адрес электронной почты и пароль до вставки базы данных. Идентификаторы создаются во время вставки и используются приложением при извлечении записей из базы данных. - person Braden Holt; 04.06.2020