Zgodnie z najlepszymi praktykami programistycznymi, zalecane jest korzystanie z dedykowanych baz danych (np MySQL, postgresql). Co jeśli już stworzyliśmy stronę opartą o SQLite i zapełnimy ją treścią? Bazując na własnych testach, zauważyłem że wiele narzędzi nie radzi sobie, z obsługą kluczy między bazami bądź z polskimi znakami.
Czas na własne rozwiązanie :)
Migracja pod kontrolą
1. Na samym początku dodajemy import naszej biblioteki która pozwoli skorzystać nam z .env jak z tablicy konfiguracyjnej oraz moduł pymysql do obsługi baz danych.
blog/project/settings.py
from decouple import config
import pymysql
2. W samym module definiujemy który dokładnie driver wybieramy. W tym przypadku zależy nam aby zintegrować się z MySQL.
blog/project/settings.py
pymysql.install_as_MySQLdb()
3. Następnie, dla zachowania dobrych praktyk, wynosimy nasz secret_key oraz flagę debug, do naszego .env.
blog/project/.settings.py
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
4. Dodajemy teraz nasz nowy driver dla bazy danych, w nowym namespace. Będzie dostępny, tylko gdy określimy dla aplikacji że chcemy z niego korzystać. W innych przypadkach Django skorzysta z "default".
blog/project/settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': path.join(BASE_DIR, 'db.sqlite3'),
},
'mysql': {
'ENGINE': 'django.db.backends.mysql',
'NAME': config('DB'),
'USER': config('DB_USER'),
'PASSWORD': config('DB_PASSWORD'),
'HOST': config('DB_HOST', default='localhost'),
'PORT': config('DB_PORT', default='3306'),
'OPTIONS': {
'charset': 'utf8mb4',
'init_command': "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_0900_ai_ci'",
},
},
}
5. Dla zachowania bezpieczeństwa, definiujemy nasze .env, z wszystkimi sekretami poza głównym plikiem settings.
blog/project/.env
SECRET_KEY=<klucz z django>
DEBUG=True
DB_NAME=migracja
DB_USER=test
DB_PASSWORD=test
DB_HOST=localhost
DB_PORT=3306
6. Dodajemy teraz nasz .env do .gitignore, tak aby nie przesyłać naszych kluczy do publicznego repozytorium GIT.
blog/.gitignore
.env
7. Następnie uruchamiamy nasz plik migracyjny.
Uwagi:
* Pamiętaj aby wypełnić:
- SOURCE_DB - nazwą drivera przypisanego do SQLite
- DESTINATION_DB - nazwą drivera przypisanego do SQLite
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') - poprawić nazwę naszego projektu
* Pamiętaj aby założyć pustą bazę danych na docelowym serwerze MySQL.
blog/migracja.py
import os
import django
from django.apps import apps
from django.db import IntegrityError, OperationalError
from django.db.models import ForeignKey
from django.core.management import call_command
SOURCE_DB = 'default' # Nazwa bazy źródłowej (SOURCE_DB)
DESTINATION_DB = 'mysql' # Nazwa bazy docelowej (MySQL)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') # Nazwa projektu
django.setup()
def run_migrations():
print("[INFO] Wykonywanie migracji na bazie DESTINATION_DB...")
call_command('migrate', database=DESTINATION_DB)
def get_model_dependencies(model):
dependencies = []
for field in model._meta.fields:
if isinstance(field, ForeignKey):
dependencies.append(field.related_model)
return dependencies
def topological_sort(models):
visited = set()
sorted_models = []
def visit(model):
if model in visited:
return
visited.add(model)
for dep in get_model_dependencies(model):
visit(dep)
sorted_models.append(model)
for m in models:
visit(m)
unique_models = []
seen = set()
for m in sorted_models:
if m not in seen:
unique_models.append(m)
seen.add(m)
return unique_models
def migrate_all_models():
print("[INFO] Pobieranie listy modeli...")
all_models = apps.get_models()
models_to_migrate = topological_sort(all_models)
for model in models_to_migrate:
name = model.__name__
label = model._meta.app_label
try:
queryset = model.objects.using(SOURCE_DB).all()
total = queryset.count()
print(f"[INFO] {label}.{name}: {total} rekordów do migracji")
migrated = 0
for obj in queryset:
model.objects.using('default').filter(pk=obj.pk).delete()
try:
obj.save(using='default')
migrated += 1
except IntegrityError as e:
print(f"[ERROR] {label}.{name}: duplikat lub brak FK — {e}")
except Exception as e:
print(f"[ERROR] {label}.{name}: błąd zapisu — {e}")
print(f"[OK] {label}.{name}: zakończono ({migrated}/{total})")
except OperationalError as e:
print(f"[ERROR] {label}.{name}: błąd połączenia — {e}")
except Exception as e:
print(f"[ERROR] {label}.{name}: nieoczekiwany błąd — {e}")
if __name__ == '__main__':
run_migrations()
migrate_all_models()
Poniżej przestawiam Ci, wykonanie naszego skryptu.
programista$ python migracja.py
[INFO] Wykonywanie migracji na bazie DESTINATION_DB...
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, django_summernote, newsletter, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying blog.0001_initial... OK
Applying blog.0002_comment... OK
Applying blog.0003_comment_post... OK
Applying blog.0004_mainpagecategory_alter_comment_content_and_more... OK
Applying blog.0005_alter_mainpagecategory_options_and_more... OK
Applying django_summernote.0001_initial... OK
Applying django_summernote.0002_update-help_text... OK
Applying newsletter.0001_initial... OK
Applying newsletter.0002_newsletter_checked... OK
Applying newsletter.0003_alter_newsletter_options_alter_newsletter_checked... OK
Applying sessions.0001_initial... OK
[INFO] Pobieranie listy modeli...
[INFO] blog.MainPageCategory: 5 rekordów do migracji
[OK] blog.MainPageCategory: zakończono (5/5)
[INFO] blog.Category: 15 rekordów do migracji
[OK] blog.Category: zakończono (15/15)
[INFO] auth.User: 1 rekordów do migracji
[OK] auth.User: zakończono (1/1)
[INFO] blog.Post: 58 rekordów do migracji
[OK] blog.Post: zakończono (58/58)
[INFO] blog.Comment: 0 rekordów do migracji
[OK] blog.Comment: zakończono (0/0)
[INFO] newsletter.Newsletter: 0 rekordów do migracji
[OK] newsletter.Newsletter: zakończono (0/0)
[INFO] django_summernote.Attachment: 59 rekordów do migracji
[OK] django_summernote.Attachment: zakończono (59/59)
[INFO] contenttypes.ContentType: 12 rekordów do migracji
[OK] contenttypes.ContentType: zakończono (12/12)
[INFO] admin.LogEntry: 0 rekordów do migracji
[OK] admin.LogEntry: zakończono (0/0)
[INFO] auth.Permission: 48 rekordów do migracji
[OK] auth.Permission: zakończono (48/48)
[INFO] auth.Group: 0 rekordów do migracji
[OK] auth.Group: zakończono (0/0)
[INFO] sessions.Session: 0 rekordów do migracji
[OK] sessions.Session: zakończono (0/0)
8. Teraz, po wykonanym imporcie możemy zauważyć że:
- nasza baza danych MySQL została utworzona z migracji,
- a następnie uzupełniona z zachowaniem wszelkich znaków specjalnych.
9. Na koniec, w naszym settings.py zmieniamy domyślny driver na mysql-owy wedle poniższego szablonu.
blog/project/settings.py
DATABASES = {
'sqlite': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': path.join(BASE_DIR, 'db.sqlite3'),
},
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': config('DB'),
'USER': config('DB_USER'),
'PASSWORD': config('DB_PASSWORD'),
'HOST': config('DB_HOST', default='localhost'),
'PORT': config('DB_PORT', default='3306'),
'OPTIONS': {
'charset': 'utf8mb4',
'init_command': "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_0900_ai_ci'",
},
},
}
Aktualny blog skorzystał z tej metody :)
Mówię sprawdzam
W jednym pliku definiuję nasze kategorie:
class MainPageCategory(models.Model):
name = models.CharField(_('Name'), max_length=120)
class Category(models.Model):
name = models.CharField(_('Name'), max_length=120)
main_category = models.ForeignKey(MainPageCategory, on_delete=models.CASCADE, verbose_name=_('main_category'), null=True, blank=True)
A w drugim nasz post.
class Post(models.Model):
title = models.CharField(_('Tytuł'), max_length=120)
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Autor'), null=True, blank=True)
category = models.ForeignKey(Category, on_delete=models.CASCADE, verbose_name=_('Category'))
Jak widzimy, import zachował odpowiednią kolejność naszych postów blogowych jak i momentu kiedy był potrzebny import użytkowników.
[INFO] blog.MainPageCategory: 5 rekordów do migracji
[OK] blog.MainPageCategory: zakończono (5/5)
[INFO] blog.Category: 15 rekordów do migracji
[OK] blog.Category: zakończono (15/15)
[INFO] auth.User: 1 rekordów do migracji
[OK] auth.User: zakończono (1/1)
[INFO] blog.Post: 58 rekordów do migracji
[OK] blog.Post: zakończono (58/58)
Komentarze