2012-05-16 7 views
10

Ich habe ein Django-Projekt, das mehrere django "-Apps" hat. Einer von ihnen hat Modelle, um Daten von einer externen Quelle darzustellen (ich kontrolliere diese Daten nicht).Django: "Soft" ForeignField ohne Datenbankintegritätsprüfungen

Ich möchte, dass meine anderen Apps Referenzen auf diese "externe App" haben können, aber ich möchte alle Fuzz der Datenbankintegritätsprüfungen vermeiden. Ich möchte nicht, dass die Datenbank irgendwelche Beschränkungen für diese "weichen Fremdschlüssel" hat.

Wissen Sie, wie ich ein benutzerdefiniertes Feld codieren kann, das einen echten Django ForeignKey emuliert, ohne eine harte Einschränkung für die Datenbank zu erzeugen?

Vielleicht existiert das schon, aber ich hatte kein Glück bei Google.

Vielen Dank im Voraus für die Hilfe :-)

NB: Ich bin mir bewusst, das generic relations System mit dem content_types. Aber ich möchte keine generischen Beziehungen. Ich möchte spezifische Beziehungen zu identifizierten Modellen nur ohne harte Integritätsbedingungen haben.

EDIT:

Ich fand RELATED_LINKS:

Aber ich habe keine richtige Antwort auf meine Frage finden. :(

EDIT 2012, 4. Juni:

Ich habe tief in Djangos Code sah zu finden, was getan werden muss, aber ich denke, dass Subklassen ForeignKey einfach nicht genug sein Können Sie mir einige Richtungen geben. auf, wie dies zu tun

NB: ich benutze Süd für meine Datenbankschema der Verwaltung, so dass ich denke, ich werde zu etwas dagegen tun müssen, aber es kann hier aus dem Thema sein :)

+1

Es ist nicht wirklich ein Fremdschlüssel dann, innit? –

+0

Nun möchte ich von allen Funktionen des django ForeignKey ohne die db-Einschränkung profitieren. – Robin

+0

Zum Beispiel, ich möchte in der Lage sein, eine Zeile aus einer Tabelle zu löschen, die von diesem 'SoftForeignKey' referenziert wird, ohne eine Kaskade zu erzeugen oder den Schlüssel auf' NULL' zu setzen. Und wenn ein Objekt einen Verweis auf eine nicht vorhandene Zeile in der Zieltabelle hat, sollte es eine "ObjectDoesNotExist" -Ausnahme auslösen. Aber ich möchte, dass die Datenbank diesen Zustand akzeptiert. – Robin

Antwort

3

Yo Leute,

Ich konnte machen, was ich wollte.

Zuerst habe ich ein neues Feld erstellt:

from django.db.models.deletion import DO_NOTHING 
from django.db.models.fields.related import ForeignKey, ManyToOneRel 

class SoftForeignKey(ForeignKey): 
    """ 
    This field behaves like a normal django ForeignKey only without hard database constraints. 
    """ 
    def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): 
     ForeignKey.__init__(self, to, to_field=to_field, rel_class=rel_class, **kwargs) 
     self.on_delete = DO_NOTHING 

    no_db_constraints = True 

Da ich Süden verwenden, um mein Datenbank-Schema zu verwalten, ich dies hinzugefügt hatte:

from south.modelsinspector import add_introspection_rules 
add_introspection_rules([], [r'^ecm\.lib\.softfk\.SoftForeignKey']) 

Dann musste ich Affen Patch Süden, so dass es den Parameter berücksichtigt. Es gab zwei in der Schaffung von FK Einschränkungen beteiligten Funktionen:

from django.db.models.deletion import DO_NOTHING 
from django.db.models.fields.related import ForeignKey, ManyToOneRel 
from django.core.management.color import no_style 
from south.db.generic import DatabaseOperations, invalidate_table_constraints, flatten 

def column_sql(self, table_name, field_name, field, tablespace='', with_name=True, field_prepared=False): 
    """ 
    Creates the SQL snippet for a column. Used by add_column and add_table. 
    """ 

    # If the field hasn't already been told its attribute name, do so. 
... 
... 
... 

     if field.rel and self.supports_foreign_keys: 
      # HACK: "soft" FK handling begin 
      if not hasattr(field, 'no_db_constraints') or not field.no_db_constraints: 
       self.add_deferred_sql(
        self.foreign_key_sql(
         table_name, 
         field.column, 
         field.rel.to._meta.db_table, 
         field.rel.to._meta.get_field(field.rel.field_name).column 
        ) 
       ) 
      # HACK: "soft" FK handling end 

    # Things like the contrib.gis module fields have this in 1.1 and below 
    if hasattr(field, 'post_create_sql'): 
     for stmt in field.post_create_sql(no_style(), ta 
.... 
.... 

# monkey patch South here 
DatabaseOperations.column_sql = column_sql 

Und:

from django.db.models.deletion import DO_NOTHING 
from django.db.models.fields.related import ForeignKey, ManyToOneRel 
from django.core.management.color import no_style 
from south.db.generic import DatabaseOperations, invalidate_table_constraints, flatten 

@invalidate_table_constraints 
def alter_column(self, table_name, name, field, explicit_name=True, ignore_constraints=False): 
    """ 
    Alters the given column name so it will match the given field. 
    Note that conversion between the two by the database must be possible. 
    Will not automatically add _id by default; to have this behavour, pass 
    explicit_name=False. 

    @param table_name: The name of the table to add the column to 
    @param name: The name of the column to alter 
    @param field: The new field definition to use 
    """ 

    if self.dry_run: 
     if self.debug: 
... 
... 
    if not ignore_constraints: 
     # Add back FK constraints if needed 
     if field.rel and self.supports_foreign_keys: 
      # HACK: "soft" FK handling begin 
      if not hasattr(field, 'no_db_constraints') or not field.no_db_constraints: 
       self.execute(
        self.foreign_key_sql(
         table_name, 
         field.column, 
         field.rel.to._meta.db_table, 
         field.rel.to._meta.get_field(field.rel.field_name).column 
        ) 
       ) 
      # HACK: "soft" FK handling end 

# monkey patch South here 
DatabaseOperations.alter_column = alter_column 

Das ist wirklich hässlich, aber ich habe nicht einen anderen Weg finden.

Jetzt können Sie das SoftForeignKey-Feld genau wie einen normalen ForeignKey verwenden, außer dass Sie keine referenzielle Integritätserzwingung haben.

Sehen Sie hier für den kompletten Affen-Patch: http://eve-corp-management.org/projects/ecm/repository/entry/ecm/lib/softfk.py

1

. Sie können versuchen, ein nicht verwaltetes Modell zu verwenden:

from django.db import models 


class ReferencedModel(models.Model): 
    pass 


class ManagedModel(models.Model): 
    my_fake_fk = models.IntegerField(
     db_column='referenced_model_id' 
    ) 


class UnmanagedModel(models.Model): 
    my_fake_fk = models.ForeignKey(
     ReferencedModel, 
     db_column='referenced_model_id' 
    ) 

    class Meta: 
     managed = False 
     db_table = ManagedModel._meta.db_table 

Die Angabe managed=False in einer Model Meta-Klasse wird keine db-Tabelle dafür erstellen. Es verhält sich jedoch genau wie andere Modelle.

1

Huckepack off Kommentar von marianobianchi ist, eine der Optionen für ForeignKey.on_delete ist

do_nothing: Keine Aktion durchführen. Wenn das Datenbank-Backend die referenzielle Integrität erzwingt, führt dies zu IntegrityError, es sei denn, Sie fügen dem Datenbankfeld manuell eine SQL ON DELETE-Integritätsbedingung hinzu (möglicherweise mithilfe von initial sql).

Dies sollte in Kombination mit der Deaktivierung von Fremdschlüsseleinschränkungen auf der db-Ebene ausreichen. Soweit ich das beurteilen kann, gibt es zwei Möglichkeiten, dies zu tun.

from django.db.backend.signals import connection_created 
from django.dispatch import receiver 

@receiver(connection_created) 
def disable_constraints(sender, connection): 
    connection.disable_constraint_checking() 

Es sieht aus wie die django db Backends bieten eine constraint_checks_disabled Kontext-Manager auch, so dass Sie die entsprechende db greift in Code wie folgt durchgehend die Kontrollen zu deaktivieren zu vermeiden, wickeln könnte: Sie könnten fk Einschränkungen ganz wie diese deaktivieren:

from django.db import connection 
with connection.constraint_checks_disabled(): 
    do_stuff() 
2

habe ich versucht, etwas ähnliches zu Izz ad-Din Ruhulessin Vorschlag, aber es hat nicht funktioniert, weil ich Spalten andere als die „fake FK“ Spalte. Der Code, den ich versuchte, war:

class DynamicPkg(models.Model): 
    @property 
    def cities(self): 
     return City.objects.filter(dpdestinations__dynamic_pkg=self) 


class DynamicPkgDestination(models.Model): 
    dynamic_pkg = models.ForeignKey(DynamicPkg, related_name='destinations') 
    # Indexed because we will be joining City.code to 
    # DynamicPkgDestination.city_code and we want this to be fast. 
    city_code = models.CharField(max_length=10, db_index=True) 


class UnmanagedDynamicPkgDestination(models.Model): 
    dynamic_pkg = models.ForeignKey(DynamicPkg, related_name='destinations') 
    city = models.ForeignKey('City', db_column='city_code', to_field='code', related_name='dpdestinations') 

    class Meta: 
     managed = False 
     db_table = DynamicPkgDestination._meta.db_table 


class City(models.Model): 
    code = models.CharField(max_length=10, unique=True) 

und die Fehler, die ich erhielt, waren:

Error: One or more models did not validate: 
travelbox.dynamicpkgdestination: Accessor for field 'dynamic_pkg' clashes with related field 'DynamicPkg.destinations'. Add a related_name argument to the definition for 'dynamic_pkg'. 
travelbox.dynamicpkgdestination: Reverse query name for field 'dynamic_pkg' clashes with related field 'DynamicPkg.destinations'. Add a related_name argument to the definition for 'dynamic_pkg'. 
travelbox.unmanageddynamicpkgdestination: Accessor for field 'dynamic_pkg' clashes with related field 'DynamicPkg.destinations'. Add a related_name argument to the definition for 'dynamic_pkg'. 
travelbox.unmanageddynamicpkgdestination: Reverse query name for field 'dynamic_pkg' clashes with related field 'DynamicPkg.destinations'. Add a related_name argument to the definition for 'dynamic_pkg'. 

aber ich habe durch die Verwendung eines Proxy-Modell mit einer Arbeitslösung kommen. Ich habe immer noch um einiges Django Validierung hacken, die Felder verhindert, in Proxy-Modellen enthalten sind:

class DynamicPkg(models.Model): 
    @property 
    def cities(self): 
     return City.objects.filter(dpdestinations__dynamic_pkg=self) 



def proxify_model(new_class, base): 
    """ 
    Like putting proxy = True in a model's Meta except it doesn't spoil your 
    fun by raising an error if new_class contains model fields. 
    """ 
    new_class._meta.proxy = True 
    # Next 2 lines are what django.db.models.base.ModelBase.__new__ does when 
    # proxy = True (after it has done its spoil-sport validation ;-) 
    new_class._meta.setup_proxy(base) 
    new_class._meta.concrete_model = base._meta.concrete_model 


class DynamicPkgDestination(models.Model): 
    dynamic_pkg = models.ForeignKey(DynamicPkg, related_name='destinations') 
    # Indexed because we will be joining City.code to 
    # DynamicPkgDestination.city_code and we want this to be fast. 
    city_code = city_code_field(db_index=True) 


class ProxyDynamicPkgDestination(DynamicPkgDestination): 
    city = models.ForeignKey('City', db_column='city_code', to_field='code', related_name='dpdestinations') 


proxify_model(ProxyDynamicPkgDestination, DynamicPkgDestination) 


class City(models.Model): 
    code = models.CharField(max_length=10, unique=True) 
+0

Ihr Problem verwendet: related_name = 'Destinationen' für beide ForeignKeys verwaltete verwaltete_Ziele und unmanaged_destinations für jeweils. –

0

Ich löste dies durch eine GenericForeignKey mit:

thing_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, blank=True, null=True) 
thing_object_id = models.UUIDField(default=uuid.uuid4, blank=True, null=True) 

thing = GenericForeignKey(ct_field='thing_content_type', fk_field='thing_object_id') 

Auf der positiven Seite, es ist out-of-the-box Django

On Auf der negativen Seite haben Sie drei zusätzliche Attribute in Ihrem Modell.

Darüber hinaus funktionieren umgekehrte Beziehungen nicht automatisch, aber in meinem Fall bin ich damit einverstanden.