2012-05-16 80 views
10

我有一個Django項目,它有多個django「應用程序」。其中一個模型可以表示來自外部數據源的數據(我不控制這些數據)。Django:沒有數據庫完整性檢查的「Soft」ForeignField

我希望我的其他應用程序能夠引用這個「外部應用程序」,但我想避免所有的數據庫完整性檢查模糊。我不希望數據庫對這些「軟外鍵」有任何限制。

你知道我該如何編寫一個自定義字段來模擬一個真正的Django ForeignKey而不會在數據庫上創建一個硬約束?

也許這已經存在了,但我在Google上沒有任何運氣。

在此先感謝您的幫助:-)

注:我知道與CONTENT_TYPES的generic relations系統。但我不想要通用關係。我只想要特定的關係到已識別的模型而沒有硬性完整性約束

編輯:

我發現相關鏈接:

但我沒有找到一個合適的回答我的問題。 :(

EDIT 2012年,6月4日:

我已經看了深入Django的代碼,找出需要做什麼,但我認爲這只是繼承ForeignKey的是不夠的,你能給我一些指示。如何做到這一點

注:我用南來管理我的數據庫架構,所以我想我需要做的東西太多,但它可能是出題目的在這裏:)

+1

那麼它不是一個真正的外鍵,是嗎? –

+0

那麼我想從沒有db約束的django ForeignKey的所有功能中受益。 – Robin

+0

例如,我希望能夠從該SoftForeignKey引用的表中刪除一行,而不必級聯或將鍵設置爲NULL。如果一個對象有一個對目標表中不存在的行的引用,它應該引發一個'ObjectDoesNotExist'異常。但我希望數據庫接受這種狀態。 – Robin

回答

3

呦傢伙,

我設法讓我想要的東西。

首先,我創建了一個新的領域:

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 

由於我使用南來管理我的數據庫架構,我不得不補充一點:

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

於是,我只好猴子向南補丁,以便將no_db_constraints參數考慮在內。有參與創造的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 

和:

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 

這實在是太醜了,但我沒有找到另一種方式。

現在您可以使用SoftForeignKey字段完全像正常的ForeignKey字段,除了您不會有任何參考完整性實施。

在這裏看到完整的猴子補丁:http://eve-corp-management.org/projects/ecm/repository/entry/ecm/lib/softfk.py

1

。您可以嘗試使用非託管型號:

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 

在Model Meta類中指定managed=False不會爲其創建數據庫表。但是,它的行爲與其他模式完全相同。

1

捎帶關閉marianobianchi的評論中,對ForeignKey.on_delete的選項之一是

DO_NOTHING:不採取任何行動。如果數據庫後端強制執行參照完整性,則會導致IntegrityError,除非您手動將SQL ON DELETE約束添加到數據庫字段(可能使用初始sql)。

這與在db級別禁用外鍵約束結合起來應該會有效。從我所知道的情況來看,有兩種方法可以做到這一點。你可以完全禁用FK約束這樣的:

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

它看起來像Django的數據庫後端提供constraint_checks_disabled上下文管理也一樣,所以你可以包裝相關的數據庫中的代碼訪問這樣避免整個禁用檢查:

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

我試過類似伊茨廣告錠Ruhulessin的建議的東西,但它沒有工作,因爲我有比「假FK」欄目等欄目。我試過的代碼是:

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) 

,我得到的錯誤是:

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

但是我沒有使用代理模型拿出一個可行的解決方案。我還是要砍周圍,防止從場中所包含的代理模型的一些Django的驗證:

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

你的問題是使用:related_name ='destinations'爲ForeignKeys分別使用managed_destinations和unmanaged_destinations。 –

0

我解決了這個用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') 

從積極的一面,這是超出現成的Django

在消極的一面,你有三個額外的屬性在你的模型中。

此外,反向關係不會自動工作,但在我的情況下,我沒關係。