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 ###
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.
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
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.
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)