Γράφοντας migrations για τη βάση δεδομένων

Αυτό το άρθρο εξηγεί πως να δομήσετε και να γράψετε migrations για τη βάση δεδομένων (database migrations), βασιζόμενοι σε διαφορετικά σενάρια που μπορεί να συναντήσετε. Για μια εισαγωγή στα migrations, δείτε στο άρθρο migrations.

Data migrations και πολλαπλές βάσεις δεδομένων

Όταν χρησιμοποιείτε πολλές βάσεις δεδομένων, συχνά θα βρίσκεστε σε δίλλημα να τρέξετε ένα migration σε κάποια συγκεκριμένη βάση δεδομένων. Για παράδειγμα, ίσως να θέλετε να τρέξετε ένα migration μόνο για μια συγκεκριμένη βάση δεδομένων.

Για να το κάνετε αυτό, ελέγξτε το alias της σύνδεσης της βάσης δεδομένων μέσα σε μια RunPython λειτουργία, κοιτώντας το attribute του schema_editor.connection.alias:

from django.db import migrations

def forwards(apps, schema_editor):
    if schema_editor.connection.alias != 'default':
        return
    # Your migration code goes here

class Migration(migrations.Migration):

    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(forwards),
    ]

Μπορείτε, επίσης, να δώσετε κάποια hints (πληροφορίες) οι οποίες θα περαστούν στη μέθοδο allow_migrate() των database routers ως **hints:

myapp/dbrouters.py
class MyRouter:

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if 'target_db' in hints:
            return db == hints['target_db']
        return True

Έπειτα, για να το αξιοποιήσετε αυτό στα migrations σας, κάντε τα ακόλουθα:

from django.db import migrations

def forwards(apps, schema_editor):
    # Your migration code goes here
    ...

class Migration(migrations.Migration):

    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(forwards, hints={'target_db': 'default'}),
    ]

Αν η RunPython ή η RunSQL λειτουργία σας, επιδρά μόνο σε ένα μοντέλο, μια καλή πρακτική είναι να περάσετε το model_name ως hint για να το κάνετε όσο γίνεται πιο διαφανές στον router. Αυτό είναι ιδιαίτερα σημαντικό για επαναχρησιμοποιήσιμες και third-party εφαρμογές.

Migrations τα οποία προσθέτουν unique πεδία

Εφαρμόζοντας ένα «απλό» migration το οποίο προσθέτει ένα μοναδικό μη-κενό πεδίο (unique non-nullable field) σε έναν πίνακα με ήδη υπάρχουσες γραμμές, θα κάνει raise κάποιο σφάλμα επειδή δεν είναι δυνατόν να αναπαραχθεί κάποια μοναδική τιμή για κάθε γραμμή της στήλης αυτής. Επίσης δεν γίνεται να ορίσουμε μια default τιμή καθώς δεν μπορεί κάθε γραμμή να έχει την ίδια τιμή για αυτή τη στήλη.

Για να λύσουμε αυτό το πρόβλημα, θα πρέπει να ακολουθήσουμε τα παρακάτω βήματα. Σε αυτό το παράδειγμα, θα προσθέσουμε ένα μη-κενό πεδίο του τύπου UUIDField με μια προεπιλεγμένη τιμή (default value). Τροποποιήστε το αντίστοιχο δικό σας πεδίο ανάλογα τις ανάγκες σας.

  • Προσθέστε ένα πεδίο στο μοντέλο σας με τις παραμέτρους default=uuid.uuid4 και unique=True (επιλέξτε μια κατάλληλη προεπιλεγμένη τιμή ανάλογα τον τύπο του πεδίου που πρόκειται να προσθέσετε).

  • Τρέξτε την διαχειριστική εντολή makemigrations. Η εντολή αυτή θα δημιουργήσει ένα αρχείο migration με την λειτουργία του AddField μέσα του.

  • Δημιουργήστε δύο κενά migration αρχεία για την ίδια εφαρμογή, τρέχοντας δύο φορές την εντολή makemigrations myapp --empty. Στο παράδειγμα μας, έχουμε μετονομάσει τα αρχεία των migrations για να τους δώσουμε πιο επεξηγηματικά ονόματα.

  • Αντιγράψτε τη λειτουργία του AddField από το migration αρχείο που δημιουργήθηκε με την εντολή makemigrations (το πρώτο από τα τρία migration αρχεία) στο τελευταίο (το δεύτερο από τα δύο κενά) και αλλάξτε το AddField σε AlterField. Επίσης, προσθέστε τα ανάλογα imports για τα modules uuid και models. Για παράδειγμα:

    0006_remove_uuid_null.py
    # Generated by Django A.B on YYYY-MM-DD HH:MM
    from django.db import migrations, models
    import uuid
    
    class Migration(migrations.Migration):
    
        dependencies = [
            ('myapp', '0005_populate_uuid_values'),
        ]
    
        operations = [
            migrations.AlterField(
                model_name='mymodel',
                name='uuid',
                field=models.UUIDField(default=uuid.uuid4, unique=True),
            ),
        ]
    
  • Επεξεργαστείτε το πρώτο αρχείο του migration, το οποίο θα μοιάζει κάπως έτσι:

    0004_add_uuid_field.py
    class Migration(migrations.Migration):
    
        dependencies = [
            ('myapp', '0003_auto_20150129_1705'),
        ]
    
        operations = [
            migrations.AddField(
                model_name='mymodel',
                name='uuid',
                field=models.UUIDField(default=uuid.uuid4, unique=True),
            ),
        ]
    

    Αλλάξτε το unique=True σε null=True – αυτό θα δημιουργήσει ένα μεσάζων κενό πεδίο που δεν περιέχει τον περιορισμό της μοναδικότητας (unique constraint) μέχρις ότου δημιουργήσουμε μοναδικές τιμές για κάθε γραμμή αυτής της στήλης.

  • Στο πρώτο κενό migration αρχείο (το δεύτερο εκ των τριών), προσθέστε μια λειτουργία RunPython ή RunSQL για να δημιουργήσετε μια μοναδική τιμή (σε αυτό το παράδειγμα δημιουργούμε μια τιμή του τύπου UUID) για κάθε υπάρχουσα γραμμή. Επίσης, προσθέστε και το απαραίτητο import για το module uuid. Για παράδειγμα:

    0005_populate_uuid_values.py
    # Generated by Django A.B on YYYY-MM-DD HH:MM
    from django.db import migrations
    import uuid
    
    def gen_uuid(apps, schema_editor):
        MyModel = apps.get_model('myapp', 'MyModel')
        for row in MyModel.objects.all():
            row.uuid = uuid.uuid4()
            row.save(update_fields=['uuid'])
    
    class Migration(migrations.Migration):
    
        dependencies = [
            ('myapp', '0004_add_uuid_field'),
        ]
    
        operations = [
            # omit reverse_code=... if you don't want the migration to be reversible.
            migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
        ]
    
  • Τώρα μπορείτε να εφαρμόσετε τα migration αρχεία, ως συνήθως, με την διαχειριστική εντολή migrate.

    Σημειώστε ότι θα υπάρξει race condition αν επιτρέψετε να δημιουργηθούν objects όσο το migration τρέχει. Η τιμή της uuid των objects που θα δημιουργηθούν μεταξύ της λειτουργίας του AddField και της RunPython θα γίνει overwritten.

Non-atomic migrations

Στις βάσεις δεδομένων που υποστηρίζουν DDL transactions (όπως η SQLite και η PostgreSQL), τα migrations θα τρέξουν μέσα σε ένα transaction από προεπιλογή. Για τις περιπτώσεις όπου θα πραγματοποιηθούν data migrations σε μεγάλους πίνακες, ίσως να θέλετε να αποτρέψετε το migration να τρέξει μέσα σε ένα transaction θέτοντας το attribute atomic σε False:

from django.db import migrations

class Migration(migrations.Migration):
    atomic = False

Μέσα σε ένα τέτοιο migration, όλες οι λειτουργίες θα τρέξουν εκτός κάποιου transaction. Μπορείτε, επίσης, να τρέξετε μερικά επιμέρους κομμάτια ενός migration μέσα σε ένα transaction χρησιμοποιώντας τη μέθοδο atomic() ή περνώντας την παράμετρο atomic=True στη λειτουργία του RunPython.

Παρακάτω φαίνεται ένα παράδειγμα ενός non-atomic data migration, το οποίο ενημερώνει έναν μεγάλο πίνακα ανά μικρότερα κομμάτια:

import uuid

from django.db import migrations, transaction

def gen_uuid(apps, schema_editor):
    MyModel = apps.get_model('myapp', 'MyModel')
    while MyModel.objects.filter(uuid__isnull=True).exists():
        with transaction.atomic():
            for row in MyModel.objects.filter(uuid__isnull=True)[:1000]:
                row.uuid = uuid.uuid4()
                row.save()

class Migration(migrations.Migration):
    atomic = False

    operations = [
        migrations.RunPython(gen_uuid),
    ]

The atomic attribute doesn’t have an effect on databases that don’t support DDL transactions (e.g. MySQL, Oracle). (MySQL’s atomic DDL statement support refers to individual statements rather than multiple statements wrapped in a transaction that can be rolled back.)

Ελέγχοντας τη σειρά εκτέλεσης των migration αρχείων

Το Django δεν καθορίζει τη σειρά εκτέλεσης των migration αρχείων από το όνομα τους αλλά δημιουργώντας ένα γράφημα χρησιμοποιώντας δύο properties της κλάσης Migration: το dependencies και το run_before.

Αν έχετε χρησιμοποιήσει την διαχειριστική εντολή makemigrations θα έχετε δει, πιθανόν, το property dependencies που δημιουργείται αυτόματα μέσα σε κάθε migration αρχείο.

Το property dependencies ορίζεται ως εξής:

from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0123_the_previous_migration'),
    ]

Συνήθως αυτό θα είναι αρκετό, αλλά μερικές φορές ίσως χρειαστεί να βεβαιωθείτε ότι το migration αρχείο σας τρέξει πριν τρέξουν άλλα migrations. Αυτό είναι χρήσιμο, για παράδειγμα, να κάνετε τα migration αρχεία τρίτων εφαρμογών να τρέξουν μετά την αντικατάσταση της δική σας ρύθμισης AUTH_USER_MODEL.

Για να το επιτύχετε αυτό, τοποθετήστε όλα τα migrations τα οποία εξαρτώνται από τα δικά σας, στο attribute run_before της κλάσης Migration:

class Migration(migrations.Migration):
    ...

    run_before = [
        ('third_party_app', '0001_do_awesome'),
    ]

Προτιμήστε να χρησιμοποιείτε το dependencies αντί του run_before όπου αυτό είναι δυνατόν. Θα πρέπει να χρησιμοποιείτε το run_before μόνο όταν είναι μη επιθυμητό ή μη πρακτικό να προσδιορίσετε το dependencies σε ένα migration αρχείο το οποίο θέλετε να τρέξει μετά από αυτό που γράφετε.

Κάνοντας migration των data μεταξύ third-party εφαρμογών

Μπορείτε να χρησιμοποιήσετε το data migration για να μετακινήσετε δεδομένα (data) από μια εφαρμογή τρίτου σε μια άλλη.

Αν σκοπεύετε να αφαιρέσετε αργότερα την παλιά εφαρμογή σας, θα χρειαστεί να ορίσετε το property dependencies βασιζόμενοι στο αν η παλιά σας εφαρμογή είναι εγκατεστημένη ή όχι. Σε διαφορετική περίπτωση, θα έχετε απόντα dependencies μόλις απεγκαταστήσετε την παλιά εφαρμογή. Ομοίως, θα χρειαστεί να κάνετε catch το exception LookupError μέσα στην κλήση της μεθόδου ``apps.get_model()``η οποία ανακτά τα μοντέλα από την παλιά εφαρμογή. Αυτή η μέθοδος σας επιτρέπει να ανεβάσετε το project σας σε κάποιον server (deployment) χωρίς να χρειάζεται, πρώτα, να εγκαταστήσετε και μετά να απεγκαταστήσετε την παλιά σας εφαρμογή.

Παρακάτω φαίνεται ένα δείγμα ενός migration αρχείου:

myapp/migrations/0124_move_old_app_to_new_app.py
from django.apps import apps as global_apps
from django.db import migrations

def forwards(apps, schema_editor):
    try:
        OldModel = apps.get_model('old_app', 'OldModel')
    except LookupError:
        # The old app isn't installed.
        return

    NewModel = apps.get_model('new_app', 'NewModel')
    NewModel.objects.bulk_create(
        NewModel(new_attribute=old_object.old_attribute)
        for old_object in OldModel.objects.all()
    )

class Migration(migrations.Migration):
    operations = [
        migrations.RunPython(forwards, migrations.RunPython.noop),
    ]
    dependencies = [
        ('myapp', '0123_the_previous_migration'),
        ('new_app', '0001_initial'),
    ]

    if global_apps.is_installed('old_app'):
        dependencies.append(('old_app', '0001_initial'))

Επίσης, σκεφτείτε τι θα θέλατε να γίνει όταν το migration δεν εφαρμοστεί. Θα μπορούσατε να μην κάνετε τίποτα (όπως στο παραπάνω παράδειγμα) ή να αφαιρέσετε κάποια ή όλα τα data από την καινούργια εφαρμογή. Προσαρμόστε το δεύτερο argument της λειτουργίας του RunPython καταλλήλως.

Changing a ManyToManyField to use a through model

If you change a ManyToManyField to use a through model, the default migration will delete the existing table and create a new one, losing the existing relations. To avoid this, you can use SeparateDatabaseAndState to rename the existing table to the new table name whilst telling the migration autodetector that the new model has been created. You can check the existing table name through sqlmigrate or dbshell. You can check the new table name with the through model’s _meta.db_table property. Your new through model should use the same names for the ForeignKeys as Django did. Also if it needs any extra fields, they should be added in operations after SeparateDatabaseAndState.

For example, if we had a Book model with a ManyToManyField linking to Author, we could add a through model AuthorBook with a new field is_primary, like so:

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
    dependencies = [
        ('core', '0001_initial'),
    ]

    operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=[
                # Old table name from checking with sqlmigrate, new table
                # name from AuthorBook._meta.db_table.
                migrations.RunSQL(
                    sql='ALTER TABLE core_book_authors RENAME TO core_authorbook',
                    reverse_sql='ALTER TABLE core_authorbook RENAME TO core_book_authors',
                ),
            ],
            state_operations=[
                migrations.CreateModel(
                    name='AuthorBook',
                    fields=[
                        (
                            'id',
                            models.AutoField(
                                auto_created=True,
                                primary_key=True,
                                serialize=False,
                                verbose_name='ID',
                            ),
                        ),
                        (
                            'author',
                            models.ForeignKey(
                                on_delete=django.db.models.deletion.DO_NOTHING,
                                to='core.Author',
                            ),
                        ),
                        (
                            'book',
                            models.ForeignKey(
                                on_delete=django.db.models.deletion.DO_NOTHING,
                                to='core.Book',
                            ),
                        ),
                    ],
                ),
                migrations.AlterField(
                    model_name='book',
                    name='authors',
                    field=models.ManyToManyField(
                        to='core.Author',
                        through='core.AuthorBook',
                    ),
                ),
            ],
        ),
        migrations.AddField(
            model_name='authorbook',
            name='is_primary',
            field=models.BooleanField(default=False),
        ),
    ]

Αλλαγή ενός unmanaged μοντέλου σε managed

Αν θέλετε να αλλάξετε ένα unmanaged μοντέλο (managed=False) σε managed, θα πρέπει να αφαιρέσετε το managed=False και να δημιουργήσετε ένα migration προτού κάνετε άλλες αλλαγές στο μοντέλο σχετικά με το schema, αφού οι αλλαγές στο schema οι οποίες εμφανίζονται στο migration που περιέχει την αλλαγή του Meta.managed ίσως να μην εφαρμοστούν. Με άλλα λόγια, δημιουργήστε και τρέξτε ένα migration μετά την αφαίρεση του managed=False και κατόπιν μπορείτε ελεύθερα να κάνετε τις schema αλλαγές στο (managed) μοντέλο σας.

Back to Top