Почему Flask-Migrate заставляет меня выполнить двухэтапную миграцию?

Я работаю над проектом с Flask, SQLAlchemy, Alembic и их оболочками для Flask (Flask-SQLAlchemy и Flask-Migrate). У меня четыре миграции:

1c5f54d4aa34 -> 4250dfa822a4 (head), Feed: Countries
312c1d408043 -> 1c5f54d4aa34, Feed: Continents
41984a51dbb2 -> 312c1d408043, Basic Structure
<base> -> 41984a51dbb2, Init Alembic

Когда я запускаю новую и чистую базу данных и пытаюсь запустить миграции, я получаю сообщение об ошибке:

vagrant@precise32:/vagrant$ python manage.py db upgrade
...
sqlalchemy.exc.ProgrammingError: (ProgrammingError) relation "continent" does not exist
...

Если я попрошу Flask-Migrate запустить все миграции, кроме последней, все заработает. Если после этого я снова запустил команду обновления, она сработает, то есть полностью обновит мою базу данных без единого изменения кода:

vagrant@precise32:/vagrant$ python manage.py db upgrade 312c1d408043
INFO  [alembic.migration] Context impl PostgresqlImpl.
INFO  [alembic.migration] Will assume transactional DDL.
INFO  [alembic.migration] Running upgrade  -> 41984a51dbb2, Init Alembic
INFO  [alembic.migration] Running upgrade 41984a51dbb2 -> 312c1d408043, Basic Structure

vagrant@precise32:/vagrant$ python manage.py db upgrade
INFO  [alembic.migration] Context impl PostgresqlImpl.
INFO  [alembic.migration] Will assume transactional DDL.
INFO  [alembic.migration] Running upgrade 312c1d408043 -> 1c5f54d4aa34, Feed: Continents
INFO  [alembic.migration] Running upgrade 1c5f54d4aa34 -> 4250dfa822a4, Feed: Countries

TL; DR

Последняя миграция (Лента: Страны) запускает запросы к таблице, загруженной предыдущей (Лента: Континенты). Если у меня есть таблица континентов, созданная и загруженная, скрипты должны работать. Но это не так. Почему я должен останавливать процесс миграции между ними, чтобы перезапустить его с помощью другой команды? Я действительно этого не понимаю. Это какая-то команда, которую Alembic выполняет после серии миграций? Любые идеи?

На всякий случай

Мои модели определены следующим образом:

class Country(db.Model):

    __tablename__ = 'country'
    id = db.Column(db.Integer, primary_key=True)
    alpha2 = db.Column(db.String(2), index=True, unique=True)
    title = db.Column(db.String(140))
    continent_id = db.Column(db.Integer, db.ForeignKey('continent.id'))
    continent = db.relationship('Continent', backref='countries')

    def __repr__(self):
        return '<Country #{}: {}>'.format(self.id, self.title)

class Continent(db.Model):

    __tablename__ = 'continent'
    id = db.Column(db.Integer, primary_key=True)
    alpha2 = db.Column(db.String(2), index=True, unique=True)
    title = db.Column(db.String(140))

    def __repr__(self):
        return '<Continent #{}: {}>'.format(self.id, self.title)

Большое спасибо,

ОБНОВЛЕНИЕ 1. Метод обновления последних двух миграций

Как @Miguel спросил в комментарии, здесь есть методы обновления последних двух миграций:

Лента: континенты

def upgrade():
    csv_path = app.config['BASEDIR'].child('migrations', 'csv', 'en')
    csv_file = csv_path.child('continents.csv')
    with open(csv_file) as file_handler:
        csv = list(reader(file_handler))
        csv.pop(0)
        data = [{'alpha2': c[0].lower(), 'title': c[1]} for c in csv]
        op.bulk_insert(Continent.__table__, data)

Фид: страны (зависит от таблицы, загруженной при последней миграции)

def upgrade():

    # load countries iso3166.csv and build a dictionary
    csv_path = app.config['BASEDIR'].child('migrations', 'csv', 'en')
    csv_file = csv_path.child('iso3166.csv')
    countries = dict()
    with open(csv_file) as file_handler:
        csv = list(reader(file_handler))
        for c in csv:
            countries[c[0]] = c[1]

    # load countries-continents from country_continent.csv
    csv_file = csv_path.child('country_continent.csv')
    with open(csv_file) as file_handler:
        csv = list(reader(file_handler))
        country_continent = [{'country': c[0], 'continent': c[1]} for c in csv]

    # loop
    data = list()
    for item in country_continent:

        # get continent id
        continent_guess = item['continent'].lower()
        continent = Continent.query.filter_by(alpha2=continent_guess).first()

        # include country
        if continent is not None:
            country_name = countries.get(item['country'], False)
            if country_name:
                data.append({'alpha2': item['country'].lower(),
                             'title': country_name,
                             'continent_id': continent.id})

CSV, который я использую, в основном следуют этим шаблонам:

continents.csv

...
AS, "Asia"
EU, "Europe"
NA, "North America"
...

iso3166.csv

...
CL,"Chile"
CM,"Cameroon"
CN,"China"
...

_country_continent.csv_

...
US,NA
UY,SA
UZ,AS
...

Таким образом, Feed: Continents питает таблицу континентов, а Feed: Country - таблицу стран. Но он должен запросить таблицу континентов, чтобы установить правильную связь между страной и континентом.

ОБНОВЛЕНИЕ 2. Кто-то из Reddit уже предложил объяснение и обходной путь

Я задал steps_upgrade/cmua7az">тот же вопрос на Reddit и themathemagician сказал:

Я сталкивался с этим раньше, и проблема в том, что миграции не выполняются индивидуально, а вместо этого алембический пакет объединяет их все (или все из них, которые необходимо запустить), а затем выполняет SQL. Это означает, что к моменту выполнения последней миграции таблицы еще не существуют, поэтому вы не можете выполнять запросы. Делает

from alembic import op

def upgrade():
    #migration stuff
    op.execute('COMMIT')
    #run queries

Это не самое элегантное решение (и это было для Postgres, команда может отличаться для других dbs), но у меня оно сработало. Кроме того, на самом деле это проблема не с Flask-Migrate, а с алембиком, поэтому, если вы хотите получить дополнительную информацию в Google, поищите алембик. Flask-Migrate - это просто оболочка для перегонки, которая легко работает с Flask-Script.


person cuducos    schedule 13.12.2014    source источник
comment
Можете ли вы добавить upgrade() методы последних двух миграций?   -  person Miguel    schedule 14.12.2014
comment
@Miguel, спасибо, только что отредактировал пост и добавил методы. Кстати, в другом месте я сказал вам «спасибо», но, возможно, вы не видел это - так что просто поделимся еще раз, ха-ха ...   -  person cuducos    schedule 14.12.2014


Ответы (1)


Как указано @themathemagician на Reddit, Alembic по умолчанию выполняет все миграции в одной транзакции, поэтому в зависимости от движка базы данных и того, что вы делаете в своих сценариях миграции, некоторые операции, которые зависят от вещей, добавленных в предыдущей миграции, могут завершиться ошибкой.

Я сам этого не пробовал, но Alembic 0.6.5 представил параметр transaction_per_migration, который может решить эту проблему. Это вариант вызова configure() в env.py. Если вы используете файлы конфигурации по умолчанию, поскольку Flask-Migrate их создает, то это то место, где вы исправляете это в migrations/env.py:

def run_migrations_online():
    """Run migrations in 'online' mode.

    # ...
    context.configure(
                connection=connection,
                target_metadata=target_metadata,
                transaction_per_migration=True        # <-- add this
                )
    # ...

Также обратите внимание, что если вы планируете также выполнять автономную миграцию, вам необходимо таким же образом исправить вызов configure() в run_migrations_offline().

Попробуйте и дайте мне знать, решит ли он проблему.

person Miguel    schedule 14.12.2014