Γράφοντας 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
:
class MyRouter(object):
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 για τα modulesuuid
καιmodels
. Για παράδειγμα:0006_remove_uuid_null.py# -*- coding: utf-8 -*- # Generated by Django A.B on YYYY-MM-DD HH:MM from __future__ import unicode_literals 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.pyclass 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 για το moduleuuid
. Για παράδειγμα:0005_populate_uuid_values.py# -*- coding: utf-8 -*- # Generated by Django A.B on YYYY-MM-DD HH:MM from __future__ import unicode_literals 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 αρχείου:
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) μοντέλο σας.