web-dev-qa-db-fra.com

Création de données de graines dans une migration de flacon ou de migration d'alambic

Comment puis-je insérer des données de départ dans ma première migration? Si la migration n'est pas le meilleur endroit pour cela, alors quelle est la meilleure pratique?

"""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 ###
43
Mark Richman

Alembic a, comme l'une de ses opérations, bulk_insert() . La documentation donne l'exemple suivant (avec quelques correctifs que j'ai inclus):

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)},
    ]
)

Notez également que l'alambic a une opération execute() , qui est exactement comme la fonction normale execute() dans SQLAlchemy: vous pouvez exécuter n'importe quel SQL de votre choix, comme le l'exemple de documentation montre:

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')})
        )

Notez que la table utilisée pour créer les métadonnées utilisées dans l'instruction update est définie directement dans le schéma. Cela peut sembler se casser SEC (n'est pas le tableau déjà défini dans votre application), mais est en fait tout à fait nécessaire. Si vous essayez d'utiliser la définition de table ou de modèle qui fait partie de votre application, vous interrompez cette migration lorsque vous apportez des modifications à votre table/modèle dans votre application. Vos scripts de migration doivent être figés: un changement vers une future version de vos modèles ne doit pas changer les scripts de migration. L'utilisation des modèles d'application signifie que les définitions changeront en fonction de la version des modèles que vous avez extraits (probablement la dernière). Par conséquent, vous avez besoin que la définition de table soit autonome dans le script de migration.

Une autre chose à discuter est de savoir si vous devez placer vos données de départ dans un script qui s'exécute comme sa propre commande (comme l'utilisation d'une commande Flask-Script, comme indiqué dans l'autre réponse). Cela peut être utilisé, mais vous devez y faire attention. Si les données que vous chargez sont des données de test, c'est une chose. Mais j'ai compris que les "données de départ" désignaient les données nécessaires au bon fonctionnement de l'application. Par exemple, si vous devez configurer des enregistrements pour "admin" et "utilisateur" dans le tableau "rôles". Ces données DEVRAIENT être insérées dans le cadre des migrations. N'oubliez pas qu'un script ne fonctionnera qu'avec la dernière version de votre base de données, tandis qu'une migration fonctionnera avec la version spécifique à partir de laquelle vous migrez. Si vous vouliez qu'un script charge les informations sur les rôles, vous pourriez avoir besoin d'un script pour chaque version de la base de données avec un schéma différent pour la table "rôles".

De plus, en vous appuyant sur un script, vous rendriez plus difficile l'exécution du script entre les migrations (par exemple, la migration 3-> 4 nécessite que les données de départ de la migration initiale soient dans la base de données). Vous devez maintenant modifier le mode d'exécution par défaut d'Alembic pour exécuter ces scripts. Et cela n'ignore toujours pas les problèmes liés au fait que ces scripts devraient changer au fil du temps, et qui sait quelle version de votre application vous avez extraite du contrôle de code source.

65
Mark Hildreth

Les migrations doivent être limitées aux modifications de schéma uniquement, et pas seulement cela, il est important que lorsqu'une migration vers le haut ou vers le bas est appliquée, les données qui existaient auparavant dans la base de données sont préservées autant que possible. L'insertion de données de départ dans le cadre d'une migration peut gâcher des données préexistantes.

Comme la plupart des choses avec Flask, vous pouvez l'implémenter de plusieurs façons. L'ajout d'une nouvelle commande à Flask-Script est un bon moyen de le faire, à mon avis. Par exemple:

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

Alors vous lancez:

python manager.py seed
24
Miguel

MarkHildreth a fourni une excellente explication de la façon dont l'alambic peut gérer cela. Cependant, l'OP portait spécifiquement sur la façon de modifier un script de migration de migration de flacons. Je vais poster une réponse à cela ci-dessous pour éviter aux gens d'avoir à se pencher sur l'alambic.

Avertissement La réponse de Miguel est exacte par rapport aux informations de base de données normales. C'est-à-dire qu'il faut suivre ses conseils et ne pas utiliser cette approche pour remplir une base de données avec des lignes "normales". Cette approche est spécifiquement pour les lignes de base de données qui sont nécessaires au fonctionnement de l'application, une sorte de données que je considère comme des données "de départ".

Le script OP a été modifié pour amorcer les données:

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

Contexte pour ceux qui découvrent flask_migrate

Flask migrate génère des scripts de migration à migrations/versions. Ces scripts sont exécutés dans l'ordre sur une base de données afin de l'amener à la dernière version. L'OP comprend un exemple de l'un de ces scripts de migration générés automatiquement. Pour ajouter des données de départ, il faut modifier manuellement le fichier de migration généré automatiquement. Le code que j'ai publié ci-dessus en est un exemple.

Qu'est-ce qui a changé?

Très peu. Vous remarquerez que dans le nouveau fichier, je stocke la table renvoyée par create_table pour list_type dans une variable appelée list_type_table. Nous opérons ensuite sur cette table en utilisant op.bulk_insert pour créer quelques exemples de lignes.

5
melchoir55

Vous pouvez également utiliser la bibliothèque faker de Python qui peut être un peu plus rapide car vous n'avez pas besoin de fournir vous-même des données. Une façon de le configurer serait de placer une méthode dans une classe pour laquelle vous souhaitez générer des données, comme indiqué ci-dessous.

from extensions import bcrypt, db

class User(db.Model):
    # this config is used by sqlalchemy to store model data in the database
    __table= '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()

Et puis implémentez une méthode qui appelle la méthode seed qui pourrait ressembler à ceci:

from faker import Faker
from users.models import User

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