Γράφοντας 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),
    ]

Το attribute atomic δεν επηρεάζει τις βάσεις δεδομένων οι οποίες δεν υποστηρίζουν τα DDL transactions (πχ MySQL, Oracle).

Ελέγχοντας τη σειρά εκτέλεσης των 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 καταλλήλως.

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

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

Back to Top