Database
 sql >> Datenbank >  >> RDS >> Database

So erstellen Sie einen Index in Django ohne Ausfallzeiten

Die Verwaltung von Datenbankmigrationen ist eine große Herausforderung in jedem Softwareprojekt. Glücklicherweise verfügt Django ab Version 1.7 über ein integriertes Migrationsframework. Das Framework ist sehr leistungsfähig und nützlich bei der Verwaltung von Änderungen in Datenbanken. Aber die durch das Framework bereitgestellte Flexibilität erforderte einige Kompromisse. Um die Einschränkungen von Django-Migrationen zu verstehen, werden Sie sich mit einem bekannten Problem befassen:Erstellen eines Indexes in Django ohne Ausfallzeit.

In diesem Tutorial lernen Sie:

  • Wie und wann Django neue Migrationen generiert
  • So überprüfen Sie die Befehle, die Django generiert, um Migrationen auszuführen
  • So passen Sie Migrationen sicher an Ihre Anforderungen an

Dieses Tutorial für Fortgeschrittene richtet sich an Leser, die bereits mit Django-Migrationen vertraut sind. Eine Einführung in dieses Thema finden Sie unter Django Migrations:A Primer.

Kostenloser Bonus: Klicken Sie hier, um kostenlosen Zugriff auf zusätzliche Django-Tutorials und -Ressourcen zu erhalten, mit denen Sie Ihre Python-Webentwicklungsfähigkeiten vertiefen können.


Das Problem beim Erstellen eines Index in Django-Migrationen

Eine häufige Änderung, die normalerweise erforderlich wird, wenn die von Ihrer Anwendung gespeicherten Daten wachsen, ist das Hinzufügen eines Indexes. Indizes werden verwendet, um Abfragen zu beschleunigen und dafür zu sorgen, dass sich Ihre App schnell und reaktionsschnell anfühlt.

In den meisten Datenbanken erfordert das Hinzufügen eines Indexes eine exklusive Sperre für die Tabelle. Eine exklusive Sperre verhindert Datenänderungsoperationen (DML) wie UPDATE , INSERT , und DELETE , während der Index erstellt wird.

Sperren werden implizit von der Datenbank erhalten, wenn bestimmte Operationen ausgeführt werden. Wenn sich beispielsweise ein Benutzer bei Ihrer App anmeldet, aktualisiert Django den last_login Feld im auth_user Tisch. Um die Aktualisierung durchzuführen, muss die Datenbank zuerst eine Sperre für die Zeile erhalten. Wenn die Zeile derzeit von einer anderen Verbindung gesperrt wird, erhalten Sie möglicherweise eine Datenbankausnahme.

Das Sperren einer Tabelle kann ein Problem darstellen, wenn es notwendig ist, das System während Migrationen verfügbar zu halten. Je größer die Tabelle, desto länger kann es dauern, den Index zu erstellen. Je länger es dauert, den Index zu erstellen, desto länger ist das System nicht verfügbar oder reagiert nicht mehr auf Benutzer.

Einige Datenbankanbieter bieten eine Möglichkeit, einen Index zu erstellen, ohne die Tabelle zu sperren. Um beispielsweise einen Index in PostgreSQL zu erstellen, ohne eine Tabelle zu sperren, können Sie den CONCURRENTLY verwenden Stichwort:

CREATE INDEX CONCURRENTLY ix ON table (column);

In Oracle gibt es ein ONLINE Option, um DML-Operationen für die Tabelle zuzulassen, während der Index erstellt wird:

CREATE INDEX ix ON table (column) ONLINE;

Beim Generieren von Migrationen verwendet Django diese speziellen Schlüsselwörter nicht. Wenn Sie die Migration unverändert ausführen, erwirbt die Datenbank eine exklusive Sperre für die Tabelle und verhindert DML-Operationen, während der Index erstellt wird.

Das gleichzeitige Erstellen eines Index hat einige Einschränkungen. Es ist wichtig, die für Ihr Datenbank-Backend spezifischen Probleme im Voraus zu verstehen. Ein Vorbehalt in PostgreSQL ist beispielsweise, dass das gleichzeitige Erstellen eines Index länger dauert, da ein zusätzlicher Tabellenscan erforderlich ist.

In dieser Anleitung verwenden Sie Django-Migrationen, um einen Index für eine große Tabelle zu erstellen, ohne Ausfallzeiten zu verursachen.

Hinweis: Um diesem Tutorial zu folgen, wird empfohlen, dass Sie ein PostgreSQL-Back-End, Django 2.x und Python 3 verwenden.

Es ist auch möglich, anderen Datenbank-Backends zu folgen. An Stellen, an denen PostgreSQL-spezifische SQL-Funktionen verwendet werden, ändern Sie die SQL so, dass sie zu Ihrem Datenbank-Backend passt.



Einrichtung

Sie werden einen erfundenen Sale verwenden Modell in einer App namens app . In einer realen Lebenssituation werden Modelle wie Sale verwendet sind die Haupttabellen in der Datenbank, und sie sind normalerweise sehr groß und speichern viele Daten:

# models.py

from django.db import models

class Sale(models.Model):
    sold_at = models.DateTimeField(
        auto_now_add=True,
    )
    charged_amount = models.PositiveIntegerField()

Um die Tabelle zu erstellen, generieren Sie die anfängliche Migration und wenden Sie sie an:

$ python manage.py makemigrations
Migrations for 'app':
  app/migrations/0001_initial.py
    - Create model Sale

$ python manage migrate
Operations to perform:
  Apply all migrations: app
Running migrations:
  Applying app.0001_initial... OK

Nach einer Weile wird die Verkaufstabelle sehr groß und die Benutzer beginnen sich über Langsamkeit zu beschweren. Beim Überwachen der Datenbank ist Ihnen aufgefallen, dass viele Abfragen den sold_at verwenden Säule. Um die Dinge zu beschleunigen, beschließen Sie, dass Sie einen Index für die Spalte benötigen.

Um einen Index zu sold_at hinzuzufügen , nehmen Sie die folgende Änderung am Modell vor:

# models.py

from django.db import models

class Sale(models.Model):
    sold_at = models.DateTimeField(
        auto_now_add=True,
        db_index=True,
    )
    charged_amount = models.PositiveIntegerField()

Wenn Sie diese Migration unverändert ausführen, erstellt Django den Index für die Tabelle und wird gesperrt, bis der Index abgeschlossen ist. Es kann eine Weile dauern, einen Index für eine sehr große Tabelle zu erstellen, und Sie möchten Ausfallzeiten vermeiden.

In einer lokalen Entwicklungsumgebung mit einem kleinen Dataset und sehr wenigen Verbindungen fühlt sich diese Migration möglicherweise sofort an. Bei großen Datensätzen mit vielen gleichzeitigen Verbindungen kann es jedoch eine Weile dauern, eine Sperre zu erhalten und den Index zu erstellen.

In den nächsten Schritten ändern Sie die von Django erstellten Migrationen, um den Index zu erstellen, ohne Ausfallzeiten zu verursachen.



Gefälschte Migration

Der erste Ansatz besteht darin, den Index manuell zu erstellen. Sie werden die Migration generieren, aber Django sie nicht tatsächlich anwenden lassen. Stattdessen führen Sie die SQL manuell in der Datenbank aus und lassen dann Django davon ausgehen, dass die Migration abgeschlossen ist.

Generieren Sie zuerst die Migration:

$ python manage.py makemigrations --name add_index_fake
Migrations for 'app':
  app/migrations/0002_add_index_fake.py
    - Alter field sold_at on sale

Verwenden Sie sqlmigrate Befehl zum Anzeigen der SQL, die Django zum Ausführen dieser Migration verwendet:

$ python manage.py sqlmigrate app 0002
BEGIN;
--
-- Alter field sold_at on sale
--
CREATE INDEX "app_sale_sold_at_b9438ae4" ON "app_sale" ("sold_at");
COMMIT;

Sie möchten den Index erstellen, ohne die Tabelle zu sperren, also müssen Sie den Befehl ändern. Fügen Sie CONCURRENTLY hinzu Schlüsselwort und in der Datenbank ausführen:

app=# CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
ON "app_sale" ("sold_at");

CREATE INDEX

Beachten Sie, dass Sie den Befehl ohne BEGIN ausgeführt haben und COMMIT Teile. Wenn Sie diese Schlüsselwörter weglassen, werden die Befehle ohne Datenbanktransaktion ausgeführt. Wir werden Datenbanktransaktionen später in diesem Artikel besprechen.

Wenn Sie nach Ausführung des Befehls versuchen, Migrationen anzuwenden, erhalten Sie die folgende Fehlermeldung:

$ python manage.py migrate

Operations to perform:
  Apply all migrations: app
Running migrations:
  Applying app.0002_add_index_fake...Traceback (most recent call last):
  File "venv/lib/python3.7/site-packages/django/db/backends/utils.py", line 85, in _execute
    return self.cursor.execute(sql, params)

psycopg2.ProgrammingError: relation "app_sale_sold_at_b9438ae4" already exists

Django beschwert sich, dass der Index bereits vorhanden ist, sodass die Migration nicht fortgesetzt werden kann. Sie haben den Index gerade direkt in der Datenbank erstellt, also müssen Sie Django jetzt glauben machen, dass die Migration bereits angewendet wurde.

Eine Migration vortäuschen

Django bietet eine integrierte Möglichkeit, Migrationen als ausgeführt zu markieren, ohne sie tatsächlich auszuführen. Um diese Option zu verwenden, setzen Sie --fake Flag beim Anwenden der Migration:

$ python manage.py migrate --fake
Operations to perform:
  Apply all migrations: app
Running migrations:
  Applying app.0002_add_index_fake... FAKED

Django hat dieses Mal keinen Fehler ausgelöst. Tatsächlich hat Django keine wirkliche Migration angewendet. Es hat es nur als ausgeführt (oder FAKED) markiert ).

Hier sind einige Probleme, die beim Vortäuschen von Migrationen zu berücksichtigen sind:

  • Der manuelle Befehl muss dem von Django generierten SQL entsprechen: Sie müssen sicherstellen, dass der von Ihnen ausgeführte Befehl dem von Django generierten SQL entspricht. Verwenden Sie sqlmigrate um den SQL-Befehl zu erzeugen. Wenn die Befehle nicht übereinstimmen, kann es zu Inkonsistenzen zwischen der Datenbank und dem Modellstatus kommen.

  • Andere nicht angewendete Migrationen werden ebenfalls vorgetäuscht: Wenn Sie mehrere nicht angewendete Migrationen haben, werden sie alle gefälscht. Bevor Sie Migrationen anwenden, ist es wichtig sicherzustellen, dass nur die Migrationen, die Sie vortäuschen möchten, nicht angewendet werden. Andernfalls könnten Sie mit Inkonsistenzen enden. Eine weitere Option besteht darin, die genaue Migration anzugeben, die Sie vortäuschen möchten.

  • Direkter Zugriff auf die Datenbank ist erforderlich: Sie müssen den SQL-Befehl in der Datenbank ausführen. Dies ist nicht immer eine Option. Außerdem ist das direkte Ausführen von Befehlen in einer Produktionsdatenbank gefährlich und sollte nach Möglichkeit vermieden werden.

  • Automatisierte Bereitstellungsprozesse müssen möglicherweise angepasst werden: Wenn Sie den Bereitstellungsprozess automatisiert haben (mithilfe von CI, CD oder anderen Automatisierungstools), müssen Sie den Prozess möglicherweise ändern, um Migrationen vorzutäuschen. Dies ist nicht immer erwünscht.

Aufräumen

Bevor Sie mit dem nächsten Abschnitt fortfahren, müssen Sie die Datenbank direkt nach der ersten Migration wieder in ihren Zustand versetzen. Migrieren Sie dazu zurück zur ursprünglichen Migration:

$ python manage.py migrate 0001
Operations to perform:
  Target specific migration: 0001_initial, from app
Running migrations:
  Rendering model states... DONE
  Unapplying app.0002_add_index_fake... OK

Django hat die Änderungen, die in der zweiten Migration vorgenommen wurden, rückgängig gemacht, daher ist es jetzt sicher, auch die Datei zu löschen:

$ rm app/migrations/0002_add_index_fake.py

Überprüfen Sie die Migrationen, um sicherzustellen, dass Sie alles richtig gemacht haben:

$ python manage.py showmigrations app
app
 [X] 0001_initial

Die anfängliche Migration wurde angewendet und es gibt keine nicht angewendeten Migrationen.



Raw-SQL in Migrationen ausführen

Im vorherigen Abschnitt haben Sie SQL direkt in der Datenbank ausgeführt und die Migration vorgetäuscht. Dies erledigt die Arbeit, aber es gibt eine bessere Lösung.

Django bietet eine Möglichkeit, rohes SQL in Migrationen mit RunSQL auszuführen . Lassen Sie uns versuchen, es zu verwenden, anstatt den Befehl direkt in der Datenbank auszuführen.

Generieren Sie zunächst eine neue leere Migration:

$ python manage.py makemigrations app --empty --name add_index_runsql
Migrations for 'app':
  app/migrations/0002_add_index_runsql.py

Bearbeiten Sie als Nächstes die Migrationsdatei und fügen Sie RunSQL hinzu Betrieb:

# migrations/0002_add_index_runsql.py

from django.db import migrations, models

class Migration(migrations.Migration):
    atomic = False

    dependencies = [
        ('app', '0001_initial'),
    ]

    operations = [
        migrations.RunSQL(
            'CREATE INDEX "app_sale_sold_at_b9438ae4" '
            'ON "app_sale" ("sold_at");',
        ),
    ]

Wenn Sie die Migration ausführen, erhalten Sie die folgende Ausgabe:

$ python manage.py migrate
Operations to perform:
  Apply all migrations: app
Running migrations:
  Applying app.0002_add_index_runsql... OK

Das sieht gut aus, aber es gibt ein Problem. Versuchen wir erneut, Migrationen zu generieren:

$ python manage.py makemigrations --name leftover_migration
Migrations for 'app':
  app/migrations/0003_leftover_migration.py
    - Alter field sold_at on sale

Django hat dieselbe Migration erneut generiert. Warum hat es das getan?

Aufräumen

Bevor wir diese Frage beantworten können, müssen Sie die Änderungen, die Sie an der Datenbank vorgenommen haben, bereinigen und rückgängig machen. Beginnen Sie mit dem Löschen der letzten Migration. Es wurde nicht angewendet, daher kann es sicher gelöscht werden:

$ rm app/migrations/0003_leftover_migration.py

Listen Sie als Nächstes die Migrationen für die app auf Anwendung:

$ python manage.py showmigrations app
app
 [X] 0001_initial
 [X] 0002_add_index_runsql

Die dritte Migration ist weg, aber die zweite wird angewendet. Sie möchten direkt nach der anfänglichen Migration zum Status zurückkehren. Versuchen Sie, wie im vorherigen Abschnitt zur ursprünglichen Migration zurückzukehren:

$ python manage.py migrate app 0001
Operations to perform:
  Target specific migration: 0001_initial, from app
Running migrations:
  Rendering model states... DONE
  Unapplying app.0002_add_index_runsql...Traceback (most recent call last):

NotImplementedError: You cannot reverse this operation

Django kann die Migration nicht rückgängig machen.



Umgekehrter Migrationsvorgang

Um eine Migration rückgängig zu machen, führt Django für jede Operation eine entgegengesetzte Aktion aus. In diesem Fall besteht die Umkehrung des Hinzufügens eines Index darin, ihn zu löschen. Wie Sie bereits gesehen haben, können Sie eine rückgängig zu machende Migration rückgängig machen. Genauso wie Sie checkout verwenden können In Git können Sie eine Migration rückgängig machen, indem Sie migrate ausführen zu einer früheren Migration.

Viele integrierte Migrationsvorgänge definieren bereits eine umgekehrte Aktion. Die umgekehrte Aktion zum Hinzufügen eines Felds besteht beispielsweise darin, die entsprechende Spalte zu löschen. Die umgekehrte Aktion zum Erstellen eines Modells besteht darin, die entsprechende Tabelle zu löschen.

Einige Migrationsvorgänge sind nicht umkehrbar. Beispielsweise gibt es keine umgekehrte Aktion zum Entfernen eines Felds oder zum Löschen eines Modells, da die Daten nach Anwendung der Migration nicht mehr vorhanden sind.

Im vorherigen Abschnitt haben Sie RunSQL verwendet Betrieb. Beim Versuch, die Migration rückgängig zu machen, ist ein Fehler aufgetreten. Gemäß dem Fehler kann einer der Vorgänge in der Migration nicht rückgängig gemacht werden. Django ist standardmäßig nicht in der Lage, Raw-SQL umzukehren. Da Django keine Kenntnis darüber hat, was durch die Operation ausgeführt wurde, kann es nicht automatisch eine entgegengesetzte Aktion generieren.

So machen Sie eine Migration reversibel

Damit eine Migration umkehrbar ist, müssen alle darin enthaltenen Vorgänge umkehrbar sein. Es ist nicht möglich, einen Teil einer Migration rückgängig zu machen, sodass ein einzelner nicht umkehrbarer Vorgang die gesamte Migration nicht umkehrbar macht.

Um ein RunSQL zu erstellen Vorgang umkehrbar ist, müssen Sie SQL bereitstellen, das ausgeführt wird, wenn der Vorgang rückgängig gemacht wird. Das umgekehrte SQL wird in reverse_sql bereitgestellt Argument.

Die entgegengesetzte Aktion zum Hinzufügen eines Index ist das Löschen. Um Ihre Migration umkehrbar zu machen, geben Sie reverse_sql an um den Index zu löschen:

# migrations/0002_add_index_runsql.py

from django.db import migrations, models

class Migration(migrations.Migration):
    atomic = False

    dependencies = [
        ('app', '0001_initial'),
    ]

    operations = [
        migrations.RunSQL(
            'CREATE INDEX "app_sale_sold_at_b9438ae4" '
            'ON "app_sale" ("sold_at");',

            reverse_sql='DROP INDEX "app_sale_sold_at_b9438ae4";',
        ),
    ]

Versuchen Sie nun, die Migration rückgängig zu machen:

$ python manage.py showmigrations app
app
 [X] 0001_initial
 [X] 0002_add_index_runsql

$ python manage.py migrate app 0001
Operations to perform:
  Target specific migration: 0001_initial, from app
Running migrations:
  Rendering model states... DONE
 Unapplying app.0002_add_index_runsql... OK

$ python manage.py showmigrations app
app
 [X] 0001_initial
 [ ] 0002_add_index_runsql

Die zweite Migration wurde rückgängig gemacht, und der Index wurde von Django gelöscht. Jetzt ist es sicher, die Migrationsdatei zu löschen:

$ rm app/migrations/0002_add_index_runsql.py

Es ist immer eine gute Idee, reverse_sql bereitzustellen . In Situationen, in denen das Umkehren einer rohen SQL-Operation keine Aktion erfordert, können Sie die Operation mit dem speziellen Sentinel migrations.RunSQL.noop als reversibel markieren :

migrations.RunSQL(
    sql='...',  # Your forward SQL here
    reverse_sql=migrations.RunSQL.noop,
),


Modellstatus und Datenbankstatus verstehen

In Ihrem vorherigen Versuch, den Index manuell mit RunSQL zu erstellen , generierte Django immer wieder die gleiche Migration, obwohl der Index in der Datenbank erstellt wurde. Um zu verstehen, warum Django das getan hat, müssen Sie zuerst verstehen, wie Django entscheidet, wann neue Migrationen generiert werden.


Wenn Django eine neue Migration generiert

Beim Generieren und Anwenden von Migrationen synchronisiert Django zwischen dem Status der Datenbank und dem Status der Modelle. Wenn Sie beispielsweise einem Modell ein Feld hinzufügen, fügt Django der Tabelle eine Spalte hinzu. Wenn Sie ein Feld aus dem Modell entfernen, entfernt Django die Spalte aus der Tabelle.

Um zwischen den Modellen und der Datenbank zu synchronisieren, verwaltet Django einen Status, der die Modelle darstellt. Um die Datenbank mit den Modellen zu synchronisieren, generiert Django Migrationsvorgänge. Migrationsoperationen werden in eine herstellerspezifische SQL übersetzt, die in der Datenbank ausgeführt werden kann. Wenn alle Migrationsoperationen ausgeführt werden, wird erwartet, dass die Datenbank und die Modelle konsistent sind.

Um den Zustand der Datenbank abzurufen, aggregiert Django die Vorgänge aller vergangenen Migrationen. Wenn der aggregierte Status der Migrationen nicht mit dem Status der Modelle übereinstimmt, generiert Django eine neue Migration.

Im vorherigen Beispiel haben Sie den Index mit unformatiertem SQL erstellt. Django wusste nicht, dass Sie den Index erstellt haben, weil Sie keinen vertrauten Migrationsvorgang verwendet haben.

Als Django alle Migrationen aggregierte und mit dem Zustand der Modelle verglich, stellte es fest, dass ein Index fehlte. Aus diesem Grund dachte Django, selbst nachdem Sie den Index manuell erstellt hatten, dass er fehlt, und generierte eine neue Migration dafür.



So trennen Sie Datenbank und Status bei Migrationen

Da Django den Index nicht so erstellen kann, wie Sie es möchten, möchten Sie Ihr eigenes SQL bereitstellen, aber Django trotzdem wissen lassen, dass Sie es erstellt haben.

Mit anderen Worten, Sie müssen etwas in der Datenbank ausführen und Django die Migrationsoperation bereitstellen, um seinen internen Zustand zu synchronisieren. Dazu stellt uns Django eine spezielle Migrationsoperation namens SeparateDatabaseAndState zur Verfügung . Diese Operation ist wenig bekannt und sollte Spezialfällen wie diesem vorbehalten bleiben.

Es ist viel einfacher, Migrationen zu bearbeiten, als sie von Grund auf neu zu schreiben. Beginnen Sie also mit dem Erstellen einer Migration auf die übliche Weise:

$ python manage.py makemigrations --name add_index_separate_database_and_state

Migrations for 'app':
  app/migrations/0002_add_index_separate_database_and_state.py
    - Alter field sold_at on sale

Dies ist der Inhalt der von Django generierten Migration, derselbe wie zuvor:

# migrations/0002_add_index_separate_database_and_state.py

from django.db import migrations, models

class Migration(migrations.Migration):

    dependencies = [
        ('app', '0001_initial'),
    ]

    operations = [
        migrations.AlterField(
            model_name='sale',
            name='sold_at',
            field=models.DateTimeField(
                auto_now_add=True,
                db_index=True,
            ),
        ),
    ]

Django hat ein AlterField generiert Operation auf dem Feld sold_at . Die Operation erstellt einen Index und aktualisiert den Status. Wir möchten diese Operation beibehalten, aber einen anderen Befehl zur Ausführung in der Datenbank bereitstellen.

Verwenden Sie erneut das von Django generierte SQL, um den Befehl zu erhalten:

$ python manage.py sqlmigrate app 0002
BEGIN;
--
-- Alter field sold_at on sale
--
CREATE INDEX "app_sale_sold_at_b9438ae4" ON "app_sale" ("sold_at");
COMMIT;

Fügen Sie CONCURRENTLY hinzu Schlüsselwort an der entsprechenden Stelle:

CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
ON "app_sale" ("sold_at");

Bearbeiten Sie als Nächstes die Migrationsdatei und verwenden Sie SeparateDatabaseAndState um Ihren modifizierten SQL-Befehl zur Ausführung bereitzustellen:

# migrations/0002_add_index_separate_database_and_state.py

from django.db import migrations, models

class Migration(migrations.Migration):

    dependencies = [
        ('app', '0001_initial'),
    ]

    operations = [

        migrations.SeparateDatabaseAndState(

            state_operations=[
                migrations.AlterField(
                    model_name='sale',
                    name='sold_at',
                    field=models.DateTimeField(
                        auto_now_add=True,
                        db_index=True,
                    ),
                ),
            ],

            database_operations=[
                migrations.RunSQL(sql="""
                    CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
                    ON "app_sale" ("sold_at");
                """, reverse_sql="""
                    DROP INDEX "app_sale_sold_at_b9438ae4";
                """),
            ],
        ),

    ],

Der Migrationsvorgang SeparateDatabaseAndState akzeptiert 2 Operationslisten:

  1. state_operations sind Operationen, die auf den Zustand des internen Modells anzuwenden sind. Sie wirken sich nicht auf die Datenbank aus.
  2. Datenbankoperationen sind Operationen, die auf die Datenbank angewendet werden sollen.

Sie haben die von Django generierte Originaloperation in state_operations beibehalten . Bei Verwendung von SeparateDatabaseAndState , das ist es, was Sie normalerweise tun möchten. Beachten Sie, dass db_index=True Argument wird dem Feld bereitgestellt. Dieser Migrationsvorgang teilt Django mit, dass es einen Index für das Feld gibt.

Sie haben das von Django generierte SQL verwendet und CONCURRENTLY hinzugefügt Stichwort. Sie haben die spezielle Aktion RunSQL verwendet Roh-SQL bei der Migration auszuführen.

Wenn Sie versuchen, die Migration auszuführen, erhalten Sie die folgende Ausgabe:

$ python manage.py migrate app
Operations to perform:
  Apply all migrations: app
Running migrations:
  Applying app.0002_add_index_separate_database_and_state...Traceback (most recent call last):
  File "/venv/lib/python3.7/site-packages/django/db/backends/utils.py", line 83, in _execute
    return self.cursor.execute(sql)
psycopg2.InternalError: CREATE INDEX CONCURRENTLY cannot run inside a transaction block



Nichtatomare Migrationen

In SQL CREATE , DROP , ALTER , und TRUNCATE Operationen werden als Datendefinitionssprache bezeichnet (DDL). In Datenbanken, die transaktionale DDL unterstützen, wie z. B. PostgreSQL, führt Django Migrationen standardmäßig innerhalb einer Datenbanktransaktion aus. Gemäß dem obigen Fehler kann PostgreSQL jedoch keinen Index gleichzeitig innerhalb eines Transaktionsblocks erstellen.

Um einen Index gleichzeitig mit einer Migration erstellen zu können, müssen Sie Django anweisen, die Migration nicht in einer Datenbanktransaktion auszuführen. Dazu markieren Sie die Migration als nicht-atomar, indem Sie atomic festlegen zu False :

# migrations/0002_add_index_separate_database_and_state.py

from django.db import migrations, models

class Migration(migrations.Migration):
    atomic = False

    dependencies = [
        ('app', '0001_initial'),
    ]

    operations = [

        migrations.SeparateDatabaseAndState(

            state_operations=[
                migrations.AlterField(
                    model_name='sale',
                    name='sold_at',
                    field=models.DateTimeField(
                        auto_now_add=True,
                        db_index=True,
                    ),
                ),
            ],

            database_operations=[
                migrations.RunSQL(sql="""
                    CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
                    ON "app_sale" ("sold_at");
                """,
                reverse_sql="""
                    DROP INDEX "app_sale_sold_at_b9438ae4";
                """),
            ],
        ),

    ],

Nachdem Sie die Migration als nicht atomar markiert haben, können Sie die Migration ausführen:

$ python manage.py migrate app
Operations to perform:
  Apply all migrations: app
Running migrations:
  Applying app.0002_add_index_separate_database_and_state... OK

Sie haben gerade die Migration durchgeführt, ohne Ausfallzeiten zu verursachen.

Hier sind einige Probleme, die Sie berücksichtigen sollten, wenn Sie SeparateDatabaseAndState verwenden :

  • Datenbankoperationen müssen Zustandsoperationen entsprechen: Inkonsistenzen zwischen der Datenbank und dem Modellstatus können viele Probleme verursachen. Ein guter Ausgangspunkt ist es, die von Django generierten Operationen in state_operations zu speichern und bearbeiten Sie die Ausgabe von sqlmigrate zur Verwendung in database_operations .

  • Nicht atomare Migrationen können im Fehlerfall nicht rückgängig gemacht werden: Wenn während der Migration ein Fehler auftritt, können Sie kein Rollback durchführen. Sie müssten die Migration entweder rückgängig machen oder manuell abschließen. Es ist eine gute Idee, die innerhalb einer nicht-atomaren Migration ausgeführten Operationen auf ein Minimum zu beschränken. Wenn Sie zusätzliche Vorgänge in der Migration haben, verschieben Sie sie in eine neue Migration.

  • Migration kann herstellerspezifisch sein: Das von Django generierte SQL ist spezifisch für das im Projekt verwendete Datenbank-Backend. Es funktioniert möglicherweise mit anderen Datenbank-Backends, aber das ist nicht garantiert. Wenn Sie mehrere Datenbank-Backends unterstützen müssen, müssen Sie einige Anpassungen an diesem Ansatz vornehmen.



Schlussfolgerung

Sie haben dieses Tutorial mit einer großen Tabelle und einem Problem begonnen. Sie wollten Ihre App für Ihre Benutzer schneller machen, und zwar ohne Ausfallzeiten.

Am Ende des Tutorials haben Sie es geschafft, eine Django-Migration zu generieren und sicher zu ändern, um dieses Ziel zu erreichen. Dabei sind Sie verschiedene Probleme angegangen und haben es geschafft, sie mit integrierten Tools zu überwinden, die vom Migrationsframework bereitgestellt werden.

In diesem Tutorial haben Sie Folgendes gelernt:

  • Wie Django-Migrationen intern mit Modell- und Datenbankstatus funktionieren und wann neue Migrationen generiert werden
  • So führen Sie benutzerdefiniertes SQL in Migrationen mit RunSQL aus Aktion
  • Was reversible Migrationen sind und wie man ein RunSQL erstellt Aktion reversibel
  • Was atomare Migrationen sind und wie Sie das Standardverhalten Ihren Anforderungen entsprechend ändern können
  • Wie man komplexe Migrationen in Django sicher durchführt

Die Trennung zwischen Modell- und Datenbankzustand ist ein wichtiges Konzept. Sobald Sie es verstanden haben und wissen, wie Sie es verwenden, können Sie viele Einschränkungen der integrierten Migrationsvorgänge überwinden. Einige Anwendungsfälle, die mir in den Sinn kommen, umfassen das Hinzufügen eines Index, der bereits in der Datenbank erstellt wurde, und das Bereitstellen herstellerspezifischer Argumente für DDL-Befehle.