Etikett: databaser

  • Åtgärda Alembic-fel i Flask: En steg-för-steg-guide

    Åtgärda Alembic-fel i Flask: En steg-för-steg-guide

    Nu när jag nästan var klar med min nya hemsida, bara lite puts kvar så började plötsligt mina databasmigreringar att strula.

    Please edit configuration/connection/logging settings in 'C:\\Users\\maria\\Documents\\PyCharmProjects\\MittProjekt\\migrations\\alembic.ini' before proceeding.

    Detta är vad jag fann för att rätta till problemet:

    Varför visas felet ”Please edit configuration/connection/logging settings…”?

    Det meddelande som visas (”Please edit configuration/connection/logging settings in ’…/migrations/alembic.ini’ before proceeding.”) är Alembics generella uppmaning efter att man initierat en ny migrationsmiljö. Alembic påminner om att man måste ange vissa inställningar (framförallt databasens URL) i konfigurationsfilen alembic.ini innan man går vidare (alembic.sqlalchemy.org). Om man inte har justerat denna fil (eller på annat sätt försett Alembic med databasens anslutningssträng) kommer Alembic att varna om att standardinställningarna fortfarande är ogiltiga.

    I en Flask-applikation som följer application factory-mönstret (där appen skapas via en funktion create_app), definieras databasens URL oftast i app-konfigurationen (app.config['SQLALCHEMY_DATABASE_URI']). När man kör flask db init skapas en alembic.ini med en platshållar-URL (t.ex. driver://user:pass@localhost/dbname). Om denna inte ersätts eller överskrivs får man ovanstående meddelande. Felet orsakas alltså av att Alembic ännu inte vet vilken databas den ska koppla upp mot. I praktiken är det ingen kraschande bugg, utan en uppmaning att konfigurera Alembic korrekt. Om Flask-Migrate används korrekt kan man ofta ignorera meddelandet och fortsätta med t.ex. flask db migrate – Flask-Migrate kommer då att hämta databas-URL:en från Flask-appen automatiskt (stackoverflow.com). Men för att undvika förvirring bör man se till att Alembic-konfigurationen ställs in rätt.

    Anpassa alembic.ini och env.py för att fungera med create_app

    För att Alembic ska fungera ihop med en Flask-app som skapas dynamiskt via create_app behöver man justera både alembic.ini och migrations/env.py:

    • alembic.ini: Här kan man ange databasens URL direkt under sektionen [alembic] med nyckeln sqlalchemy.url. Till exempel för MySQL:
    sqlalchemy.url = mysql+pymysql://<user>:<password>@<host>/<databasename>

    Detta hårdkodar anslutningssträngen. Alternativt kan man låta denna rad vara en dummy (eller använda en miljövariabelsreferens) och istället sätta URL:en via env.py – vilket är vanligare när man använder Flask-Migrate för att hämta appens konfiguration i stället för att duplicera den i alembic.ini.
    Övriga viktiga rader i alembic.ini är normalt: script_location (som ska peka på migrationskatalogen, t.ex. migrations) och eventuellt inställningar för loggning. Standardvärdena som genererats av flask db init brukar redan vara korrekta för dessa, men bekräfta att script_location = migrations matchar din projektstruktur. För MySQL kan man även försäkra sig om att uppsättningar som encoding (t.ex. utf8mb4) är korrekta om de förekommer.

    • migrations/env.py: Denna fil körs vid varje migrationskommando och här behöver vi koppla in Flask-applikationen så att Alembic får tillgång till appens konfiguration (databas-URL) och modeller. I en application factory-arkitektur måste env.py antingen:

    Använda Flask CLI:s app-kontekst (current_app): Om man kör kommandona via flask db ... och har talat om för Flask hur appen ska skapas (t.ex. genom att sätta miljövariabeln FLASK_APP till "app:create_app"), då kommer Flask att skapa appen och pusha en app context åt Alembic. I env.py kan man då nå appen via flask.current_app. Exempelvis:

    from flask import current_app
    config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI'))
    target_metadata = current_app.extensions['migrate'].db.metadata

    Här hämtar vi databasens URL från appens konfiguration och sätter den i Alembics config, samt tar ut target_metadata (d.v.s. metadata för alla modeller) från SQLAlchemy-objektet som Flask-Migrate registreratstackoverflow.com. Flask-Migrate extension ser till att current_app.extensions['migrate'].db hänvisar till din SQLAlchemy-instans. (Notera: om din databas-URL innehåller procenttecken % kan du behöva ersätta dem med %% för att configparser inte ska misstolka dem (stackoverflow.com, stackoverflow.com.)

    Skapa appen manuellt: Om current_app inte är tillgänglig (t.ex. om man kör Alembic direkt utan Flask CLI), kan man importera och initiera applikationen inom env.py. Då anropar man create_app() själv, och pushar en app context manuellt. På så vis får Alembic samma konfiguration som din applikation. Du måste också importera dina modeller så att de registreras i metadata. Exempel på de relevanta delarna i ett env.py anpassat för application factory:

    from alembic import context
    from logging.config import fileConfig
    import logging

    # Konfiguration från alembic.ini
    config = context.config
    fileConfig(config.config_file_name)
    logger = logging.getLogger('alembic.env')

    # Importera appen och dess databas
    from app import create_app, db
    flask_app = create_app() # skapa applikationen
    flask_app.app_context().push() # pusha app context för att få current_app

    # Importera modelldefinitionerna så att de registreras i metadata
    import app.models

    # Sätt databaskoppling (URL) dynamiskt från appens config
    db_url = flask_app.config['SQLALCHEMY_DATABASE_URI']
    config.set_main_option('sqlalchemy.url', db_url)

    # Hämta SQLAlchemy-metadata som beskriver modellerna
    target_metadata = db.Model.metadata # alternativt db.metadata

    def run_migrations_offline():
    """Kör migrationer utan att koppla upp (offline-läge)."""
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
    url=url,
    target_metadata=target_metadata,
    literal_binds=True
    )
    with context.begin_transaction():
    context.run_migrations()

    def run_migrations_online():
    """Kör migrationer i online-läge, med databaskoppling."""
    connectable = db.engine
    with connectable.connect() as connection:
    context.configure(
    connection=connection,
    target_metadata=target_metadata,
    process_revision_directives=... # ev. utelämnat eller anpassat
    )
    with context.begin_transaction():
    context.run_migrations()

    # Avgör läge
    if context.is_offline_mode():
    run_migrations_offline()
    else:
    run_migrations_online()

    I koden ovan skapas appen via factory-funktionen och appens konfiguration (inklusive databas-URL) läses in i Alembic. Vi hämtar target_metadata från vår SQLAlchemy-instans (här antas att db = SQLAlchemy() i app/init.py, och att modellerna är definierade som subklasser av db.Model i app/models.py). Det är viktigt att importera modulpaketet app.models efter att app-context har skapats – detta säkerställer att alla modelldefinitioner faktiskt laddas och registreras. Om man missar att importera modellerna kan Alembic tro att det inte finns några ändringar, eftersom target_metadata då är tomt (innehåller inga tabeller) trots att de finns definierade i koden.

    Säkerställ att rätt databas-URL och modeller används

    För att verifiera att Alembic/Flask-Migrate verkligen använder korrekt databas och inkluderar modellerna kan man göra följande:

    • Kontrollera databas-URL: Kör en migrationskommando (t.ex. flask db migrate eller alembic revision --autogenerate) med debug/info-loggning på. I terminalens output bör du se att Alembic kopplar upp mot MySQL och inte t.ex. SQLite. Alembic loggar vilken ”Context impl” som används. För MySQL skulle det stå något i stil med ”Context impl MySQLImpl” (för SQLite står det SQLiteImplblog.miguelgrinberg.com). Om du ser MySQL nämnas vet du att den använder rätt driver/databas. Dessutom, om anslutningen misslyckas eller URL:en är felaktig, kommer Alembic att kasta ett fel direkt – då behöver du dubbelkolla SQLALCHEMY_DATABASE_URI i appen eller i alembic.ini.
    • Kontrollera att modellerna laddas: Efter att ha kört flask db migrate (autogenerering), öppna den genererade revisionsfilen under migrations/versions/. Där bör du se skapandet av tabeller och kolumner som matchar dina modellklasser. Om Alembic rapporterar ”No changes in schema detected.” trots att du har oaplicerade modeller, tyder det på att env.py inte lyckades ladda in modelldefinitionerna eller att den jämför mot fel databasblog.miguelgrinberg.com. Säkerställ då att du importerat rätt moduler och att target_metadata faktiskt refererar till all metadata (t.ex. via db.Model.metadata). I vårt exempel importerar vi app.models manuellt i env.py för att undvika detta problem.
    • Testa migrationerna: Kör flask db upgrade för att applicera migrationerna på databasen. Om allt är rätt konfigurerat ska dina tabeller nu skapas/uppdateras i MySQL utan fel. Du kan även köra flask db current för att se aktuell migrationsversion i databasen, vilket bekräftar att Alembic kan kommunicera med databasen.

    Rekommenderat arbetssätt med Flask-Migrate och Alembic

    • Initiera migrationsrepositoryt: För en ny app gör man normalt flask db init en gång (det verkar användaren redan ha gjort, vilket skapade mappen migrations/ i projektroten och en alembic.ini). Se till att FLASK_APP är korrekt satt till din factory (t.ex. export FLASK_APP="app:create_app" i Unix, eller motsvarande i Windows) innan du kör init, så att Flask vet hur appen ska skapas vid migrationskommandon.
    • Skapa migrations (autogenerera): När du har definierat eller ändrat dina modeller, kör flask db migrate -m "Beskrivning av ändring" för att låta Alembic jämföra target_metadata (dina modeller) med databasen och generera en ny migrationsfil. Tack vare konfigurationen ovan kommer Alembic att använda appens MySQL-URL och inkludera alla modeller i jämförelsen. (Observera att första gången i ett projekt med befintlig databas kan Alembic tro att inget behöver göras om databasen redan har dessa tabeller. I så fall kan man behöva använda flask db stamp head för att markera databasen som uppdaterad, eller köra migrationskommandot mot en tom databaskopiablog.miguelgrinberg.comblog.miguelgrinberg.com.)
    • Applicera migrationer: Använd flask db upgrade för att köra migrationerna och uppdatera databasschemat till senaste version. Detta kommando kommer under huven att anropa Alembic och köra env.py med online-läget. Återigen används create_app-funktionen i vår konfiguration för att koppla upp mot rätt databas.
    • Använd Flask-Migrate i utvecklingsflödet: I och med att du har integrerat Alembic med Flask via Flask-Migrate, bör du fortsättningsvis använda flask db ...-kommandona (snarare än att köra alembic direkt) för att hantera migrationerna. Flask-Migrate sköter kopplingen till appen och kontexten åt dig. Som påpekats av utvecklare är Flask-Migrate enklare att använda i Flask-projekt eftersom det automatiserar mycket av konfigurationenstackoverflow.com. Se till att extensions initieras korrekt i create_app (d.v.s. db.init_app(app) och Migrate(app, db) om du inte redan gjort det).
  • Skapa Databasmigreringar med Alembic i Python

    Skapa Databasmigreringar med Alembic i Python

    Jag går en kurs i Python, programmeringsspråket. En av de sista uppgifterna är att skapa en hemsida, egentligen en portfolio med program jag skapat och länkar till Github med flera ställen där jag har något jag vill visa upp. I kursen använde vi SQLite som databas men eftersom jag använd MySql mycket i mina andra projekt så ville jag göra det här med. Plus att det blev en liten utmaning att göra om databaskopplingar.

    För att på ett enkelt sätt kunna göra databasmigreringar när jag uppgraderar mitt program hittade jag Alembic som fungerar bra för det. Sökvägarna behöver rättas utifrån varje projekt, förstås men gången är denna:


    1. Installera Alembic

    Om du inte redan har det:

    pip install alembic

    2. Initiera Alembic

    I roten av ditt projekt (där din app/-mapp och run.py eller liknande ligger):

    alembic init migrations

    Det skapar en migrations/-mapp och en alembic.ini-fil.


    3. Konfigurera alembic.ini

    Öppna alembic.ini och ändra denna rad:

    sqlalchemy.url = driver://user:pass@localhost/dbname

    Byt ut den mot din databas-url, t.ex.:

    sqlalchemy.url = mysql+pymysql://root:password@localhost/portfolio-db

    Eller om du använder en .env-fil – då ändrar vi detta i steg 5 istället via Python.


    4. Peka Alembic till din SQLAlchemy-modell

    Öppna migrations/env.py och leta upp detta block:

    from logging.config import fileConfig
    from sqlalchemy import engine_from_config, pool
    from alembic import context

    Lägg till din models.py-import (efter importerna ovan):

    import sys
    import os
    sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
    from app.models import db # Lägg till detta

    Byt ut denna rad:

    target_metadata = None

    mot:

    target_metadata = db.metadata

    5. (Valfritt) Använd din DATABASE_URL från .env

    Om du vill använda os.getenv("DATABASE_URL"):

    Byt ut i migrations/env.py (under config = context.config):

    from dotenv import load_dotenv
    load_dotenv()
    from sqlalchemy.engine.url import URL
    config.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL"))

    6. Skapa en migrering

    När du ändrar en modell (t.ex. la till description i PortfolioItem), kör:

    alembic revision --autogenerate -m "Add description to portfolio item"

    Det skapar en versionsfil i migrations/versions/.


    7. Kör migreringen

    alembic upgrade head

    Nu har kolumnen description skapats i databasen utan att radera något!


    alembic.ini

    Byt ut din befintliga alembic.ini med denna:

    [alembic]
    script_location = migrations
    sqlalchemy.url = mysql+pymysql://root:password@localhost/portfolio-db

    [loggers]
    keys = root,sqlalchemy,alembic

    [handlers]
    keys = console

    [formatters]
    keys = generic

    [logger_root]
    level = WARN
    handlers = console

    [logger_sqlalchemy]
    level = WARN
    handlers =
    qualname = sqlalchemy.engine

    [logger_alembic]
    level = INFO
    handlers =
    qualname = alembic

    [handler_console]
    class = StreamHandler
    args = (sys.stdout,)
    level = NOTSET
    formatter = generic

    [formatter_generic]
    format = %(levelname)-5.5s [%(name)s] %(message)s

    Byt root:password om du har en annan användare eller databas.


    migrations/env.py

    Byt ut hela innehållet i migrations/env.py med detta:

    from __future__ import with_statement

    import sys
    import os
    from logging.config import fileConfig
    from sqlalchemy import engine_from_config, pool
    from alembic import context

    # Lägg till appens rot till sys.path
    sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

    # Importera extensions och modeller
    from app.extensions import db
    from app.models import *

    # Load environment variables
    from dotenv import load_dotenv
    load_dotenv()

    # Alembic Config
    config = context.config

    # Sätt databas-URL från .env
    config.set_main_option('sqlalchemy.url', os.getenv("DATABASE_URL"))

    # Loggkonfiguration
    fileConfig(config.config_file_name)

    # Metadata från modellerna
    target_metadata = db.metadata

    def run_migrations_offline():
    """Kör migreringar utan en DB-anslutning."""
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
    url=url,
    target_metadata=target_metadata,
    literal_binds=True,
    dialect_opts={"paramstyle": "named"},
    )

    with context.begin_transaction():
    context.run_migrations()

    def run_migrations_online():
    """Kör migreringar med en DB-anslutning."""
    connectable = engine_from_config(
    config.get_section(config.config_ini_section),
    prefix="sqlalchemy.",
    poolclass=pool.NullPool,
    )

    with connectable.connect() as connection:
    context.configure(
    connection=connection,
    target_metadata=target_metadata
    )

    with context.begin_transaction():
    context.run_migrations()

    if context.is_offline_mode():
    run_migrations_offline()
    else:
    run_migrations_online()

    Klar att köra:

    Nu kan du köra:

    alembic revision --autogenerate -m "Add description field"
    alembic upgrade head