Mehrere Datenbanken nutzen

Seit Django 1.2 kann man mit mehreren Datenbanken gleichzeitig arbeiten.

Dazu tragen wir zuerst die neue Datenbank in die Datei local_settings.py ein:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(SITE_ROOT, 'cookbook.db'),
        'USER': '',
        'PASSWORD': '',
        'HOST': '',
        'PORT': '',
    },
    'newsdb': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(SITE_ROOT, 'news.db'),
    },
}

Eine neue App “news” erstellen

Diese Datenbank soll von einer News App genutzt werden. Sie soll das folgende Datenmodell haben:

digraph name {
  fontname = "Helvetica"
  fontsize = 8

  node [
    fontname = "Helvetica"
    fontsize = 8
    shape = "plaintext"
  ]
  edge [
    fontname = "Helvetica"
    fontsize = 8
  ]



subgraph cluster_news_models {
  label=<
        <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
        <TR><TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER"
        ><FONT FACE="Helvetica Bold" COLOR="Black" POINT-SIZE="12"
        >news</FONT></TD></TR>
        </TABLE>
        >
  color=olivedrab4
  style="rounded"


    cookbook_basemodels_DateTimeInfo [label=<
    <TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
     <TR><TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4"
     ><FONT FACE="Helvetica Bold" COLOR="white"
     >DateTimeInfo</FONT></TD></TR>
    
        
        <TR><TD ALIGN="LEFT" BORDER="0"
        ><FONT FACE="Helvetica Bold">date_created</FONT
        ></TD>
        <TD ALIGN="LEFT"
        ><FONT FACE="Helvetica Bold">DateTimeField</FONT
        ></TD></TR>
        
        <TR><TD ALIGN="LEFT" BORDER="0"
        ><FONT FACE="Helvetica Bold">date_updated</FONT
        ></TD>
        <TD ALIGN="LEFT"
        ><FONT FACE="Helvetica Bold">DateTimeField</FONT
        ></TD></TR>
        
    
    </TABLE>
    >]

    news_models_Article [label=<
    <TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
     <TR><TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4"
     ><FONT FACE="Helvetica Bold" COLOR="white"
     >Article<BR/>&lt;<FONT FACE="Helvetica Italic">DateTimeInfo</FONT>&gt;</FONT></TD></TR>
    
        
        <TR><TD ALIGN="LEFT" BORDER="0"
        ><FONT COLOR="#7B7B7B" FACE="Helvetica Bold">id</FONT
        ></TD>
        <TD ALIGN="LEFT"
        ><FONT COLOR="#7B7B7B" FACE="Helvetica Bold">AutoField</FONT
        ></TD></TR>
        
        <TR><TD ALIGN="LEFT" BORDER="0"
        ><FONT FACE="Helvetica Italic">date_created</FONT
        ></TD>
        <TD ALIGN="LEFT"
        ><FONT FACE="Helvetica Italic">DateTimeField</FONT
        ></TD></TR>
        
        <TR><TD ALIGN="LEFT" BORDER="0"
        ><FONT FACE="Helvetica Italic">date_updated</FONT
        ></TD>
        <TD ALIGN="LEFT"
        ><FONT FACE="Helvetica Italic">DateTimeField</FONT
        ></TD></TR>
        
        <TR><TD ALIGN="LEFT" BORDER="0"
        ><FONT FACE="Helvetica Bold">headline</FONT
        ></TD>
        <TD ALIGN="LEFT"
        ><FONT FACE="Helvetica Bold">CharField</FONT
        ></TD></TR>
        
        <TR><TD ALIGN="LEFT" BORDER="0"
        ><FONT FACE="Helvetica Bold">body</FONT
        ></TD>
        <TD ALIGN="LEFT"
        ><FONT FACE="Helvetica Bold">TextField</FONT
        ></TD></TR>
        
    
    </TABLE>
    >]


}


  

  

}

  • Ein Model Article, dass von einem abstrakten Model DateTimeInfo erbt
  • Das abstrakte Model speichert die beiden Felder automatisch

Also erstellen wir als erstes die neue App:

$ python manage.py startapp news

Der nächste Schritt ist das Anlegen des abstrakten Models. Dazu legen wir im Konfigurationsverzeichnis die Datei basemodels.py mit folgendem Inhalt an:

from django.db import models
from django.utils.timezone import now


class DateTimeInfo(models.Model):
    date_created = models.DateTimeField(editable=False)
    date_updated = models.DateTimeField(editable=False)

    class Meta:
        abstract = True

    def save(self, *args, **kwargs):
        if not self.id:
            self.date_created = now()
        self.date_updated = now()
        super(DateTimeInfo, self).save(*args, **kwargs)

Danach erstellen wir das Model Article in der Datei news/models.py:

# encoding: utf-8
from django.db import models

from cookbook.basemodels import DateTimeInfo


class Article(DateTimeInfo):
    headline = models.CharField(u'Überschrift', max_length=100)
    body = models.TextField(u'Inhalt')

    class Meta:
        verbose_name = u'Artikel'
        verbose_name_plural = u'Artikel'
        ordering = ['-date_updated']

    def __unicode__(self):
        return self.headline

Dadurch, dass das Model Article von dem Model DateTimeInfo erbt, erhält es automatisch die beiden DateTimeField Felder und deren Verhalten beim Speichern.

Jetzt brauchen wir noch eine admin.py, um das Model im Admin nutzen zu können:

from django.contrib import admin

from news.models import Article


class ArticleAdmin(admin.ModelAdmin):
    list_display = ('headline', 'date_created', 'date_updated')


admin.site.register(Article, ArticleAdmin)

Die Klasse CookbookRouter erstellen

Damit wir die neue Datenbank auch mit der App “news” nutzen können benötigen wir einen “database router”. Diesen legen wir in der Datei router.py im Konfigurationsverzeichnis an:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class CookbookRouter(object):
    """A router to control all database operations on models in the cookbook site.
    """
    def db_for_read(self, model, **hints):
        if model._meta.app_label == 'news':
            return 'newsdb'
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label == 'news':
            return 'newsdb'
        return None

    def allow_relation(self, obj1, obj2, **hints):
        if obj1._meta.app_label == 'news' or obj2._meta.app_label == 'news':
            return False
        return None

    def allow_syncdb(self, db, model):
        allowed = ['south']
        if model._meta.app_label in allowed:
            return True
        elif db == 'newsdb':
            return model._meta.app_label == 'news'
        elif model._meta.app_label == 'news':
            return False
        return None

Danach müssen wir DATABASE_ROUTERS in der Datei settings.py konfigurieren:

DATABASE_ROUTERS = ['cookbook.router.CookbookRouter']

Außerdem aktivieren wir noch die neue App “news” in den INSTALLED_APPS.

Die initiale Migration durchführen

Da wir im Kapitel Migration auf South umgestellt haben nutzen wir zum Erstellen des neue Models Article nicht mehr den Befehl syncdb, sondern wir erstellen zuerst eine Migration mit dem Kommando schemamigration:

$ python manage.py schemamigration news --initial
Creating migrations directory at '.../cookbook/news/migrations'...
Creating __init__.py in '.../cookbook/news/migrations'...
 + Added model news.Article
Created 0001_initial.py. You can now apply this migration with: ./manage.py migrate news

Da die Datenbank newsdb noch neu ist müssen wir einmalig die Tabellen für South anlegen:

$ python manage.py syncdb --noinput --database=newsdb
Syncing...
Creating tables ...
Creating table south_migrationhistory
Installing custom SQL ...
Installing indexes ...
No fixtures found.

Synced:
 > django.contrib.auth
 > django.contrib.contenttypes
 > django.contrib.sessions
 > django.contrib.sites
 > django.contrib.messages
 > django.contrib.staticfiles
 > django.contrib.admin
 > debug_toolbar
 > userauth
 > south

Not synced (use migrations):
 - recipes
 - news
(use ./manage.py migrate to migrate these)

Dabei sieht es so aus, als ob noch weitere Tabellen angelegt werden. Das ist aber nicht der Fall, denn der CookbookRouter unterbindet das anlegen der Tabellen. Wir können das auch prüfen:

$ python manage.py dbshell --database=newsdb
SQLite version 3.7.6.3
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .tables
south_migrationhistory

Jetzt führen wir die erste Migration durch:

$ python manage.py migrate news --database=newsdb
Running migrations for news:
 - Migrating forwards to 0001_initial.
 > news:0001_initial
 - Loading initial data for news.
No fixtures found.

Danach können wir den Entwicklungs-Webserver starten und einige Artikel in der neuen News App anlegen.

Eine existierende Datenbank einbinden

Seit Django 1.2 ist es auch möglich eine existierende Datenbank in Django einzubinden. Dazu müssen wir zuerst eine solche anlegen. Dafür habe ich ein Python Skript geschrieben, dass eine SQLite Datenbank mit Adressen füllt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# encoding: utf-8
import random
import sys


class Fixture(tuple):
    def get_random_id(self):
        return random.randint(1, len(self))

    def get_random_item(self):
        return self[random.randrange(0, len(self))]


def get_random_zipcode():
    return ''.join(map(str, random.sample(range(0, 10), 5)))


def drop_tables():
    sql = """DROP TABLE IF EXISTS "city";
    DROP TABLE IF EXISTS "address";
    """
    return sql


def create_tables():
    sql = """CREATE TABLE "city" (
        "id" integer NOT NULL PRIMARY KEY,
        "name" varchar(255) NOT NULL
    );
    CREATE TABLE "address" (
        "id" integer NOT NULL PRIMARY KEY,
        "first_name" varchar(100) NOT NULL,
        "last_name" varchar(100) NOT NULL,
        "street" varchar(255) NOT NULL,
        "zipcode" varchar(5) NOT NULL,
        "city_id" integer NOT NULL REFERENCES "city" ("id")
    );
    """
    return sql


def insert_cities():
    pk = 1
    sql = ''
    for city in CITIES:
        sql += """INSERT INTO "city" VALUES (
            %d, "%s"
        );
        """ % (pk, city)
        pk += 1
    return sql


def insert_addresses(count):
    pk = 1
    sql = ''
    while pk <= count:
        housenumber = random.randint(1, 100)
        data = {
            'id': pk,
            'first_name': FIRST_NAMES.get_random_item(),
            'last_name': LAST_NAMES.get_random_item(),
            'street': STREETS.get_random_item() + ' %d' % housenumber,
            'zipcode': get_random_zipcode(),
            'city_id': CITIES.get_random_id(),
        }
        sql += """INSERT INTO "address" VALUES (
            %(id)d, "%(first_name)s", "%(last_name)s", "%(street)s", "%(zipcode)s", %(city_id)d
        );
        """ % data
        pk += 1
    return sql


def write(data):
    sys.stdout.write(data)


FIRST_NAMES = Fixture(('Malte', 'Andrea', 'Peter', 'Maria', 'Michaela'))
LAST_NAMES = Fixture(('Meier', 'Schulze', 'Drescher', 'Weiland', 'Hirsch'))
STREETS = Fixture(('Alte Straße', 'Hauptstraße', 'Neuer Ring', 'Brunnengasse', 'Am Markt'))
CITIES = Fixture(('Berlin', 'Dresden', 'Hamburg', 'Bonn', 'Bremen', 'Stuttgart'))


if __name__ == '__main__':
    try:
        ADDRESS_COUNT = int(sys.argv[1])
    except IndexError:
        ADDRESS_COUNT = 10
    write('BEGIN;\n')
    write(drop_tables())
    write(create_tables())
    write(insert_cities())
    write(insert_addresses(ADDRESS_COUNT))
    write('COMMIT;')

Wenn man das Skript an der Kommandozeile aufruft, werden die erzeugten SQL Queries ausgegeben:

$ python sqltestdata.py

Man kann auch mit einem Argument die Anzahl der erzeugten Adressen bestimmen:

$ python sqltestdata.py 200

Zuerst muss aber die Datenbankverbidung in der local_settings.py angelegt werden:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(SITE_ROOT, 'cookbook.db'),
        'USER': '',
        'PASSWORD': '',
        'HOST': '',
        'PORT': '',
    },
    'newsdb': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(SITE_ROOT, 'news.db'),
    },
    'addressdb': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(SITE_ROOT, 'address.db'),
    },
}

Nun können wir die Queries mit der neuen Datenbank ausführen:

$ python sqltestdata.py 2000 | python manage.py dbshell --database=addressdb

Und uns auch gleich die Daten ansehen:

$ python manage.py dbshell --database=addressdb
SQLite version 3.7.6.3
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .tables
address  city
sqlite> select * from address join city on city_id = city.id limit 10;
1|Andrea|Schulze|Alte Straße 73|64831|5|5|Bremen
2|Malte|Schulze|Neuer Ring 35|87214|5|5|Bremen
3|Maria|Hirsch|Hauptstraße 78|68412|5|5|Bremen
4|Malte|Weiland|Brunnengasse 70|48076|2|2|Dresden
5|Andrea|Drescher|Am Markt 35|91046|1|1|Berlin
6|Maria|Drescher|Hauptstraße 13|08457|6|6|Stuttgart
7|Peter|Drescher|Hauptstraße 67|69318|3|3|Hamburg
8|Maria|Drescher|Alte Straße 89|87126|4|4|Bonn
9|Maria|Hirsch|Hauptstraße 25|41359|4|4|Bonn
10|Maria|Meier|Neuer Ring 17|95746|1|1|Berlin

Als nächstes erstellen wir eine App für die neue Datenbank:

$ python manage.py startapp addressbook

Und lassen Django mit Hilfe des Befehls inspectdb Models aus den Tabellen der Datenbank erzeugen:

$ python manage.py inspectdb --database=addressdb
# This is an auto-generated Django model module.
# You'll have to do the following manually to clean this up:
#     * Rearrange models' order
#     * Make sure each model has one field with primary_key=True
# Feel free to rename the models, but don't rename db_table values or field names.
#
# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'
# into your database.

from django.db import models

class Address(models.Model):
    id = models.IntegerField(primary_key=True)
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    street = models.CharField(max_length=255)
    zipcode = models.CharField(max_length=5)
    city = models.ForeignKey(City)
    class Meta:
        db_table = u'address'

class City(models.Model):
    id = models.IntegerField(primary_key=True)
    name = models.CharField(max_length=255)
    class Meta:
        db_table = u'city'

Diese schreiben wir dann in die Datei addressbook/models.py:

$ python manage.py inspectdb --database=addressdb > addressbook/models.py

Damit die Models auch funktionieren passen wir sie noch ein wenig an (Zeilen 5, 10, 14, 16-17, 21, 23, 26, 28-29):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from django.db import models


class Address(models.Model):
    id = models.IntegerField(primary_key=True, editable=False)
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    street = models.CharField(max_length=255)
    zipcode = models.CharField(max_length=5)
    city = models.ForeignKey('City')

    class Meta:
        db_table = u'address'
        managed = False

    def __unicode__(self):
        return '%s %s' % (self.first_name, self.last_name)


class City(models.Model):
    id = models.IntegerField(primary_key=True, editable=False)
    name = models.CharField(max_length=255)

    class Meta:
        db_table = u'city'
        managed = False

    def __unicode__(self):
        return self.name

Außerdem müssen wir den CookbookRouter erweitern (Zeilen 7-8, 14-15, 21-22):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class CookbookRouter(object):
    """A router to control all database operations on models in the cookbook site.
    """
    def db_for_read(self, model, **hints):
        if model._meta.app_label == 'news':
            return 'newsdb'
        if model._meta.app_label == 'addressbook':
            return 'addressdb'
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label == 'news':
            return 'newsdb'
        if model._meta.app_label == 'addressbook':
            return 'addressdb'
        return None

    def allow_relation(self, obj1, obj2, **hints):
        if obj1._meta.app_label == 'news' or obj2._meta.app_label == 'news':
            return False
        if obj1._meta.app_label == 'addressbook' or obj2._meta.app_label == 'addressbook':
            return False
        return None

    def allow_syncdb(self, db, model):
        allowed = ['south']
        if model._meta.app_label in allowed:
            return True
        elif db == 'newsdb':
            return model._meta.app_label == 'news'
        elif model._meta.app_label == 'news':
            return False
        return None

Jetzt benötigen wir nur noch eine addressbook/admin.py, um die Daten im Admin anzuzeigen. Wir aktivieren Suche und Filter, zeigen mehr Felder in der Liste an und machen alle Felder im Formular nur lesbar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from django.contrib import admin

from addressbook.models import Address, City


class AddressAdmin(admin.ModelAdmin):
    actions = None
    list_display = ('first_name', 'last_name', 'street', 'zipcode', 'city')
    list_display_links = ('first_name', 'last_name')
    list_filter = ('city', 'last_name')
    search_fields = ['first_name', 'last_name', 'street', 'zipcode', 'city__name']
    readonly_fields = ('first_name', 'last_name', 'street', 'zipcode', 'city')


admin.site.register(Address, AddressAdmin)
admin.site.register(City)

Zuletzt aktivieren wir noch die App addressbook in den INSTALLED_APPS in der settings.py und starten dann den Entwicklungs-Webserver, um uns die Daten anzusehen.