momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Transferarea unei baze de date din MySQL în PostgreSQL

Django | MySQL | PostgreSQL
2018 jan

[1] Un proiect PGN-games cu R, Django şi PostgreSQL

Site-ul local //docere.hom a fost construit mai demult cu Django 1.8 (instalat global, pe aceeaşi versiune de Python (2.7) folosită intern de către Ubuntu) şi cu MySQL. Avem acum din [1] şi site-ul //py3.hom, construit într-un "mediu virtual" cu Django 2.0 (necesitând Python 3.5) şi PostgreSQL. Apache2 va putea deservi fie primul site, fie pe al doilea - după cum în fişierul /etc/apache2/mods-available/wsgi.load setăm LoadModule wsgi_module pe biblioteca "mod_wsgi.so" specifică pentru Python 2.7, respectiv pentru Python 3.5 (v. [1], §4).

Vrem să unificăm lucrurile, abandonând Django 1.8 şi MySQL. Un aspect important al acestei tranziţii este transferarea bazei de date MySQL existente, în PostgreSQL; pentru aceasta, vom folosi comenzile de administrare 'dumpdata' şi 'loaddata' (pe cele două versiuni de Django).

Activăm mediul virtual ~/envPy3 (constituit în [1]) şi creem un proiect Django 2.0 'docer/', adăugându-i o aplicaţie pe care o numim 'blog':

vb@Home:~/envPy3$ source bin/activate
(envPy3) vb@Home:~/envPy3$ django-admin startproject docer
(envPy3) vb@Home:~/envPy3$ cd docer
(envPy3) vb@Home:~/envPy3/docer$ python manage.py startapp blog

În fişierul de configurare a proiectului, "docer/docer/settings.py", adăugăm "'blog.apps.BlogConfig'" în lista INSTALLED_APPS şi setăm:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'docer', 'USER': 'vbdocer', 
        'PASSWORD': 'mhikysOD7PFGwou0rGK57P8xGfDCxQ5R38U6Hvx88O3dOVUnEe5PBW4wkV1HPlr',
        'HOST': 'localhost', 'PORT': '',
    }
}

Creem user-ul PostgreSQL 'vbdocer' (cu parola specificată mai sus) şi baza de date 'docer' (a vedea eventual [1] §5 şi §6):

(envPy3) vb@Home:~/envPy3$ sudo -u postgres psql
[sudo] password for vb: 
psql (9.5.10)
postgres=# create user vbdocer with password 'mhikysOD7PFGwou0rGK57P8xG...1HPlr';
postgres=# create database docer;
postgres=# grant all privileges on database docer to vbdocer;
postgres=# \q

Proiectul Django 1.8 '~/docere/' conţine între altele, aplicaţia 'articole'; copiem fişierul "models.py" al acesteia în ~/envPy3/docer/blog/. Intenţionăm să facem unele modificări în definiţiile claselor 'Tag' şi 'Articol' (de exemplu, 'Tag' are câmpurile 'id', 'nume' şi 'info', unde 'id' este cheia primară adăugată automat de MySQL (de fapt, de Django) - dar trecând la PostgreSQL, este mai potrivit să redefinim 'nume' drept cheie primară, abandonând 'id'); dar deocamdată - pentru a putea face transferul menţionat - nu modificăm nimic în "models.py" (exceptând unele banalităţi: def __unicode__(self) trebuie înlocuit pentru Django 2.0 cu def __str__(self)).

Folosind acum comenzile de administrare 'makemigrations' şi 'migrate' - obţinem tabelele de date 'blog_articol' etc., asociate cu "models.py" - ceea ce putem verifica de exemplu astfel:

(envPy3) vb@Home:~/envPy3$ psql -W docer vbdocer -h 127.0.0.1 -f <(echo '\dt')
Password for user vbdocer: 
                   List of relations
 Schema |            Name            | Type  |  Owner  
--------+----------------------------+-------+---------
 public | auth_group                 | table | vbdocer
 # ...
 public | auth_user_user_permissions | table | vbdocer
 public | blog_articol               | table | vbdocer
 public | blog_articol_tags          | table | vbdocer
 public | blog_tag                   | table | vbdocer
 # ...
 public | django_migrations          | table | vbdocer
 public | django_session             | table | vbdocer
(13 rows)

Cu aceasta, am terminat pregătirile pentru noul site, bazat pe Django 2.0 şi PostgreSQL (dar tabelele create nu conţin în acest moment nicio înregistrare). Dezactivăm mediul virtual în care am lucrat până acum şi folosim comanda dumpdata pentru aplicaţia 'articole' din proiectul Django 1.8 '~/docere/':

vb@Home:~/docere$ python manage.py dumpdata \
                                   --format json \
                                   --output ~/envPy3/docer/DOC/articles.json \
                                   articole

Fişierul obţinut astfel, "articles.json", are deja 7 MB şi nu este de inspectat, cu un editor de text uzual; dar putem folosi de exemplu programul utilitar 'cat' (paginând cu 'more'):

vb@Home:~/docere$ cat ~/envPy3/docer/DOC/articles.json | more
[{"fields": {"info": "<b>Brendan Eich</b>, 1995\r\n", "nume": "javaScript"}, 
  "model": "articole.tag", "pk": 1}, ... 

Recunoaştem în reprezentarea textuală redată mai sus (specifică formatului JSON) o înregistrare din tabelul MySQL 'articole_tag':

vb@Home:~/docere14$ mysql docere -A
Welcome to the MySQL monitor.  # ...
Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved. # ...
mysql> select * from articole_tag limit 1;
+----+-----------------------------+------------+
| id | info                        | nume       |
+----+-----------------------------+------------+
|  1 | <b>Brendan Eich</b>, 1995   | javaScript |
+----+-----------------------------+------------+

Pentru proiectul Django 2.0 'docer' înfiinţasem mai sus, aplicaţia 'blog' (în loc de 'articole' cum aveam în proiectul Django 1.8 'docere') - încât în PostgreSQL avem tabelul 'blog_articol' (în loc de 'articole_articol', care nu sună bine); prin urmare, trebuie acum să înlocuim "articole" cu "blog", în toate apariţiile cheii '"model"' în fişierul "articles.json":

vb@Home:~/envPy3/docer/DOC$  \
> sed -i -e 's/"model": "articole\./"model": "blog\./g'  articles.json 

În mediul virtual în care am creat proiectul Django 2.0 'docer/' folosim comanda loaddata, indicând fişierul "articles.json":

vb@Home:~/envPy3/docer$ source ../bin/activate
(envPy3) vb@Home:~/envPy3/docer$ python manage.py loaddata --app blog  \
                                                  --format json  \
                                                  --ignorenonexistent  \ 
                                                  DOC/articles.json

Am folosit opţiunea "--ignorenonexistent" fiindcă deja eliminasem unul dintre câmpurile existente iniţial în 'Articol' (câmpul apare în "articles.json", dar lipseşte în docer/blog/models.py).

Putem verifica imediat că acum avem datele respective în PostgreSQL:

(envPy3) vb@Home:~/envPy3/docer$ psql -W docer vbdocer -h 127.0.0.1
docer=> select titlu, creat from blog_articol order by creat desc; 
                      titlu                      |   creat    
-------------------------------------------------+------------
 Un proiect PGN-games cu R, Django şi PostgreSQL | 2017-10-20
 Legăturile ecuaţiei cubice                      | 2017-10-10
 Radicalul cercului                              | 2017-09-14
 ...
 "Centralizatorul" Educaţiei Naţionale - de la Word la XML   | 2007-06-01
(174 rows)

'Articol'-lele sunt relaţionate cu 'Tag'-urile prin tabelul "invizibil" articole_articol_tags (respectiv, blog_articol_tags) - creat automat de către Django pentru câmpul definit în clasa 'Articol' (din "models.py") prin tags = models.ManyToManyField(Tag); tabelul blog_articol_tags este reprezentat în fişierul "articles.json" în mod implicit: în dicţionarul câmpurilor fiecărui articol apare şi câte o listă "tags" conţinând cheile primare ale 'Tag'-urilor asociate acelui articol.

Dacă vrem să eliminăm câmpul 'id' (indexul numeric obişnuit) din 'Tag', declarând drept cheie primară câmpul 'nume' - nu ajunge să redefinim 'nume = models.CharField(max_length = 64, primary_key=True)' (comenzile de migrare nu vor actualiza în sensul dorit, datele existente).

O soluţie pentru aceasta constă în suplimentarea clasei 'Tag' cu funcţia "def natural_key (self): return self(nume)", amânând însă redefinirea cheii primare; apoi (după migrare), folosim "dumpdata" ca mai sus, dar adăugând şi opţiunea "--natural-foreign" - urmarea fiind aceea că în fişierul JSON rezultat cheia "tags" din dicţionarul câmpurilor fiecărui articol va conţine lista numelor tag-urilor asociate (fiindcă valorilor 'id' existente li s-a aplicat metoda 'natural_key()'). Folosim apoi loaddata pentru fişierul JSON respectiv, iar după aceea redefinim cum am precizat câmpul 'nume' şi folosim iarăşi, comenzile de migrare.

vezi Cărţile mele (de programare)

docerpro | Prev | Next