2012-12-11 23 views
9

Ich habe eine Legacy-Datenbank mit einer Tabelle, die Knoten im Dateisystem darstellt. Es gibt wenige Arten von Knoten, z.B. A, B, C und verschiedene Typen haben unterschiedliche Eigenschaften. Im aktuellen Datenbankentwurf gibt es eine Tabelle, die Informationen über einen Knoten enthält. Wenn der Knoten vom Typ A ist, werden nur für den Typ A relevante Felder gesetzt. Nun möchte ich die Typen A, B, C als Modelle ausdrücken. Das Problem, das sich ergibt, ist:Django mehrere Modelle, gleiche Tabelle

  1. Ich möchte dieses Verhalten haben, alle drei Typen haben eine Namenseigenschaft. Ich möchte alle Knoten im Dateisystem nach Name-Eigenschaft filtern und eine Liste von Objekten von guten Typen erhalten.

  2. Jeder Knoten als übergeordneter Link, ausgedrückt als Fremdschlüssel in der Datenbank, so sollte wahrscheinlich eine Form der Vererbung stattfinden.

Ist es in Django möglich?

+0

Möchten Sie das aktuelle Datenbankschema (das ältere) verwenden? – borges

Antwort

3

Ja, es ist möglich. Hier ein Beispiel:

models.py

from django.db import models 

# Create your models here. 
class NodeA(models.Model): 

    name_a = models.CharField(max_length=75, blank=True, null=True) 

    class Meta: 
     db_table = 'Nodes' 
     managed = False 

class NodeB(models.Model): 

    name_b = models.CharField(max_length=75, blank=True, null=True) 

    class Meta: 
     db_table = 'Nodes' 
     managed = False 

class NodeC(models.Model): 

    name_c = models.CharField(max_length=75, blank=True, null=True) 

    class Meta: 
     db_table = 'Nodes' 
     managed = False 

Datenbankschema (SQLITE)

Nodes { 
    id  integer primary key 
    name_a TEXT 
    name_b TEXT 
    name_c TEXT } 

Proof-of-Concept

import NodeA, NodeB, NodeC 

a = NodeA() 
a.name_a = 'Node A' 
a.save() 

b = NodeB() 
b.name_b = 'Node B' 
b.save() 

c = NodeC() 
c.name_c = 'Node C' 
c.save() 

Dies erzeugt:

id  name_a  name_b  name_c 
1   Node A 
2      Node B 
3          Node C 
+0

Ich habe noch nie db_table auf diese Weise verwendet gesehen. Spielt es gut mit Syncdb und/oder South? Ich hätte vielleicht etwas mit Proxy-Modellen versucht. –

+0

Es funktioniert nicht gut mit Syncdb. Es wird versuchen, die Tabelle für jedes verwendete Modell zu erstellen. Es muss manuell erstellt werden. Und ich kann nicht mit der Kompatibilität mit dem Süden sprechen. Ich kann nicht einmal mit der Gesamtkompatibilität sprechen. Ich hatte noch nie ein Schema wie dieses, aber in dem kleinen Testprojekt, das ich erstellt habe, um es zu testen, hat es gut funktioniert. Wahrscheinlich hätten auch Proxy-Modelle funktionieren können, bei denen alle benötigten Felder in einem MasterNode-Modell und die Proxy-Modelle nur die gewünschten Felder definiert hätten. –

+0

Das Hinzufügen von managed = False zum Meta der 2. und 3. Tabellendefinition wird die Probleme mit Syncdb beheben. –

4

Ich benutze einen etwas anderen Ansatz, der gut mit Süden spielt, indem er eine Perspektive erstellt. Eine Perspektive ist ein Proxy, der einige Felder im Modell umbenennt, aber den Namen der Spalte behält.

Für mich war es ein Beispiel, um die Flexibilität des django ORM zu zeigen. Ich bin mir nicht sicher, ob Sie dies im Produktionscode verwenden möchten. Daher ist es nicht genug getestet, aber es wird Ihnen eine Idee geben.

Die Idee

Eine Perspektive lassen den Benutzer verschiedene Modelle an einen Tisch zu schaffen, die ihre eigenen Methoden und haben unterschiedliche Feldnamen haben, aber das zugrunde liegende Modell und Tisch teilen.

Es kann verschiedene Typen in derselben Tabelle speichern, was für Protokollierungs- oder Ereignissysteme nützlich sein kann. Jede Perspektive kann nur ihre eigenen Einträge sehen, da sie nach einem Feldnamen action_type gefiltert wird.

Die Modelle sind nicht verwaltet, haben aber einen benutzerdefinierten Manager, so dass im Süden keine neuen Tabellen dafür erstellt werden.

Nutzungs

Die Implementierung ist eine Klasse Dekorateur, der die Metadaten des django Modell modifiziert. Es nimmt ein "Basis" -Modell und ein Wörterbuch von Alias-Feldern auf.

an einem Beispiel der ersten Blick lassen:

class UserLog(models.Model): 
""" 
A user action log system, user is not in this class, because it clutters import 
""" 
date_created = models.DateTimeField(_("Date created"), auto_now_add=True) 

# Action type is obligatory 

action_type = models.CharField(_("Action Type"), max_length=255) 
integer_field1 = models.IntegerField() 
integer_field2 = models.IntegerField() 
char_field1 = models.CharField(max_length=255) 
char_field2 = models.CharField(max_length=255) 


@ModelPerspective({ 
    'x': 'integer_field1', 
    'y': 'integer_field2', 
    'target': 'char_field1' 
}, UserLog) 
class UserClickLog(models.Model): 
    pass 

ein Modell Dies schafft, die die Eigenschaft x abbildet integer_field1, y integer_field2 und Ziel zu char_field1 und wo die zugrunde liegende Tabelle ist das gleiche wie die Tabelle als Benutzerlog

Die Verwendung unterscheidet sich nicht von anderen Modellen, und im Süden wird nur die UserLog-Tabelle erstellt.

Jetzt schauen wir uns an, wie man das implementiert.

Implementierung

Wie funktioniert es?

Wenn die Klasse ausgewertet wird, erhält der Dekorator die Klasse. Dies wird Affen die Klasse patch, so dass Instanzen die Basistabelle wie angegeben widerspiegeln.

die Aliase

hinzufügen Wenn wir etwas tiefer in den Code gehen. Das aliasierte Wörterbuch wird gelesen und für jedes Feld wird das Basisfeld nachgeschlagen. Wenn wir das Feld in der Basistabelle gefunden haben, wird der Name geändert. Dies hat einen kleinen Nebeneffekt, dass es auch die Spalte ändert. Also müssen wir die Feldspalte aus dem Basisfeld abrufen. Dann wird das Feld mit der Methode contribute_to_class zur Klasse hinzugefügt, die sich um die gesamte Buchhaltung kümmert.

Dann werden alle nicht Alias-Eigenschaften zum Modell hinzugefügt. Dies ist nicht erforderlich, aber ich habe beschlossen, sie hinzuzufügen.

Einstellen der Eigenschaften

Jetzt haben wir alle Felder, müssen wir ein paar Eigenschaften festgelegt. Die Eigenschaft managed wird beim Ignorieren der Tabelle nach Süden tricksen, hat aber einen Nebeneffekt. Die Klasse wird keinen Manager haben. (Wir werden das später beheben). Wir kopieren auch den Tabellennamen (db_table) aus dem Basismodell und machen das Feld action_type standardmäßig zum Klassennamen.

Das letzte, was wir tun müssen, ist einen Manager zur Verfügung zu stellen. Etwas Vorsicht ist geboten, da Django angibt, dass es nur einen QuerySet-Manager gibt. Wir lösen dies, indem wir den Manager mit deepcopy kopieren und dann eine Filteranweisung hinzufügen, die nach dem Klassennamen filtert.

deep (QuerySet()). Filter (ACTION_TYPE = cls. Klasse. Name)

Diese nur relevante Datensätze unseren Tisch Rückkehr lassen. Jetzt wickle es in einen Dekorateur und es ist fertig.

Dies ist der Code:

from django.db import models 
from django.db.models.query import QuerySet 

def ModelPerspective(aliases, model): 
    """ 
    This class decorator creates a perspective from a model, which is 
    a proxy with aliased fields. 

    First it will loop over all provided aliases 
    these are pairs of new_field, old_field. 
    Then it will copy the old_fields found in the 
    class to the new fields and change their name, 
    but keep their columnnames. 

    After that it will copy all the fields, which are not aliased. 

Then it will copy all the properties of the model to the new model. 

    Example: 
    @ModelPerspective({ 
     'lusername': 'username', 
     'phonenumber': 'field1' 
    }, User) 
    class Luser(models.Model): 
     pass 

    """ 
    from copy import deepcopy 

    def copy_fields(cls): 

    all_fields = set(map(lambda x: x.name, model._meta.fields)) 
    all_fields.remove('id') 
    # Copy alias fields 

    for alias_field in aliases: 

     real_field = aliases[alias_field] 

     # Get field from model 
     old_field = model._meta.get_field(real_field) 
     oldname, columnname = old_field.get_attname_column() 
     new_field = deepcopy(old_field) 

     # Setting field properties 
     new_field.name = alias_field 
     new_field.db_column = columnname 
     new_field.verbose_name = alias_field 

     new_field.contribute_to_class(cls, "_%s" % alias_field) 
     all_fields.remove(real_field) 

    for field in all_fields: 
     new_field = deepcopy(model._meta.get_field(field)) 
     new_field.contribute_to_class(cls, "_%s" % new_field.name) 

    def copy_properties(cls): 
    # Copy db table 
    cls._meta.db_table = model._meta.db_table 


    def create_manager(cls): 
    from copy import deepcopy 
    field = cls._meta.get_field('action_type') 
    field.default = cls.__name__ 
    # Only query on relevant records 
    qs = deepcopy(cls.objects) 
    cls.objects = qs.filter(action_type=cls.__name__) 

    def wrapper(cls): 

    # Set it unmanaged 
    cls._meta.managed = False 

    copy_properties(cls) 
    copy_fields(cls) 
    create_manager(cls) 

    return cls 
    return wrapper 

Ist das für die Produktion bereit?

Ich würde es nicht in Produktionscode verwenden, für mich war es eine Übung, um die Flexibilität von Django zu zeigen, aber mit ausreichenden Tests könnte es in Code verwendet werden, wenn man will.

Ein weiteres Argument gegen die Verwendung in der Produktion wäre, dass der Code eine ordentliche Menge an internen Funktionen des django ORM verwendet. Ich bin mir nicht sicher, ob die API ausreichen wird, um stabil zu sein.

Und diese Lösung ist nicht die beste Lösung, die Sie sich vorstellen können. Es gibt mehr Möglichkeiten, dieses Problem des Speicherns dynamischer Felder in einer Datenbank zu lösen.