2012-05-16 13 views
10

Tengo un proyecto de Django que tiene varias "aplicaciones" de django. Uno de ellos tiene modelos para representar datos provenientes de una fuente externa (no controlo estos datos).Django: ForeignField "suave" sin verificación de integridad de la base de datos

Quiero que mis otras aplicaciones puedan tener referencias a esta "aplicación externa", pero quiero evitar toda la confusión de las comprobaciones de integridad de la base de datos. No quiero que el DB tenga ninguna restricción sobre estas "claves externas suaves".

¿Sabes cómo puedo codificar un campo personalizado que emulará un Django ForeignKey real sin crear una restricción dura en la base de datos?

Quizás esto ya exista, pero no tuve suerte en Google.

Gracias de antemano por la ayuda :-)

NB: Soy consciente del sistema generic relations con los content_types. Pero no quiero relaciones genéricas. Deseo relaciones específicas con modelos identificados solo sin restricciones de integridad.

EDIT:

he encontrado enlaces relacionados:

Pero no encontraron una respuesta correcta a la pregunta. :(

EDITAR 2012, junio 4:

He mirado profundamente en el código de Django para encontrar lo que hay que hacer, pero creo que simplemente la subclasificación ForeignKey no será suficiente Podría darme algunas direcciones. sobre cómo hacer esto

NB:? yo uso del Sur para la gestión de mi esquema de base de datos, por lo que figura voy a tener que hacer algo acerca de eso también pero puede estar fuera del tema aquí :)

+1

En realidad, no es una clave foránea, ¿verdad? –

+0

Bueno, quiero beneficiarme de todas las características de la ForeignKey django sin la restricción db. – Robin

+0

Por ejemplo, quiero poder eliminar una fila de una tabla a la que hace referencia este 'SoftForeignKey' sin tener que poner en cascada o establecer la clave en' NULL'. Y si un objeto tiene una referencia a una fila no existente en la tabla de destino, debería generar una excepción 'ObjectDoesNotExist'. Pero quiero que la base de datos acepte este tipo de estado. – Robin

Respuesta

3

Yo chicos,

logré hacer lo que quería.

En primer lugar, he creado un nuevo campo:

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 

Desde que uso del Sur para gestionar mi esquema de base de datos, he tenido que añadir esto:

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

Entonces, tuve que mono parche al sur para que tenga en cuenta el parámetro no_db_constraints. Había dos funciones implicadas en la creación de restricciones FK:

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 

Y:

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 

Esto es realmente feo, pero que no encontrar otra manera.

Ahora puede usar el campo SoftForeignKey exactamente como una ForeignKey normal excepto que no tendrá ninguna aplicación de integridad de referencia.

ve aquí para la completa mono-patch: http://eve-corp-management.org/projects/ecm/repository/entry/ecm/lib/softfk.py

1

. Puede intentar utilizando un modelo no administrado:

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 

La especificación de managed=False en una clase Meta de modelo no creará una tabla db para ella. Sin embargo, se comportará exactamente como otros modelos.

1

Piggybacking fuera del comentario de marianobianchi, una de las opciones para ForeignKey.on_delete es

DO_NOTHING: No realizar ninguna acción. Si el back-end de su base de datos impone la integridad referencial, esto causará un IntegrityError a menos que agregue manualmente una restricción SQL ON DELETE al campo de la base de datos (quizás usando sql inicial).

Esto combinado con la desactivación de las restricciones de la clave externa en el nivel db debería ser el truco. Por lo que puedo decir, hay dos formas de hacer esto.Se podría desactivar las restricciones fk enteramente así:

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() 

Parece que los backends Django db ofrecen un gestor de contexto constraint_checks_disabled, también, por lo que podría envolver el db relevante accesos en código como este para evitar la desactivación de los controles a lo largo:

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

que hemos probado algo similar a la sugerencia de Izz ad-Din de Ruhulessin pero no funcionó porque tengo columnas distintas de la columna de "falso FK". El código que probé fue:

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) 

y los errores que recibió fueron:

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'. 

Sin embargo, yo he venido para arriba con una solución de trabajo mediante el uso de un modelo de poder. Yo todavía tengo que rompiera alguna validación Django que impide que los campos de ser incluido en los modelos de proxy:

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

Tu problema es usar: related_name = 'destinations' para que ambos ForeignKeys usen managed_destinations y unmanaged_destinations para cada uno, respectivamente. –

0

He resuelto mediante el uso de un GenericForeignKey:

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') 

En el lado positivo, está fuera de la caja Django

En el lado negativo, tienes tres atributos adicionales en tu modelo.

Además, las relaciones inversas no funcionan automáticamente, pero en mi caso, estoy de acuerdo con eso.

Cuestiones relacionadas