【问题标题】:Django: "Soft" ForeignField without database integrity checksDjango:没有数据库完整性检查的“软”ForeignField
【发布时间】:2012-05-24 07:54:23
【问题描述】:

我有一个 Django 项目,其中包含多个 django“应用程序”。其中一个有模型来表示来自外部源的数据(我不控制这些数据)。

我希望我的其他应用程序能够引用这个“外部应用程序”,但我想避免数据库完整性检查的所有模糊。我不希望数据库对这些“软外键”有任何限制。

你知道我如何编写一个自定义字段来模拟真实的 Django ForeignKey,而不会对数据库产生硬约束吗?

也许这已经存在,但我在 Google 上没有运气。

提前感谢您的帮助:-)

注意:我知道带有 content_types 的 generic relations 系统。但我不想要泛型关系。我只希望与已识别模型的特定关系没有硬完整性约束。

编辑:

我找到了相关链接:

但是我没有找到我的问题的正确答案。 :(

2012 年 6 月 4 日编辑:

我已经深入研究了 django 的代码以找到需要做的事情,但我认为仅仅继承 ForeignKey 是不够的。你能给我一些关于如何做到这一点的指导吗?

注意:我使用 South 来管理我的数据库架构,所以我想我也需要对此做一些事情。但这可能超出了这里的主题:)

【问题讨论】:

  • 那不是真正的外键,innit?
  • 好吧,我想从没有 db 约束的 django ForeignKey 的所有功能中受益。
  • 例如,我希望能够从SoftForeignKey 引用的表中删除一行,而不必级联或将键设置为NULL。如果一个对象引用了目标表中不存在的行,它应该引发ObjectDoesNotExist 异常。但我希望数据库接受这种状态。
  • 也许this 有帮助。

标签: database django-models foreign-keys constraints


【解决方案1】:

如果您只想禁用某个字段的 ForeignKey 约束检查,则只需将 db_constraint=False 添加到该字段即可。

user = models.ForeignKey('User', db_constraint=False)

另请参阅: Django - How to prevent database foreign key constraint creation

【讨论】:

  • 正是我想要的。这与一个巨大的分区表相结合,PG12(仍然)depesz.com/2018/11/04/foreign-key-to-partitioned-table-part-3 和自定义 ForeignKeyClass 需要此修复以覆盖默认 instance.fieldname 以在查询时包含分区约束。覆盖 models.ForeignKey 上的 get_extra_descriptor_filter。现在我在 Django + PG 12 中得到了一个类似 ForeignKey 的对象,用于大量分区表
【解决方案2】:

借鉴 marianobianchi 的评论,ForeignKey.on_delete 的选项之一是

DO_NOTHING:不采取任何行动。如果您的数据库后端强制执行引用完整性,这将导致 IntegrityError,除非您手动将 SQL ON DELETE 约束添加到数据库字段(可能使用初始 sql)。

这与在数据库级别禁用外键约束相结合应该可以解决问题。据我所知,有两种方法可以做到这一点。您可以像这样完全禁用 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 db 后端也提供了一个 constraint_checks_disabled 上下文管理器,因此您可以将相关的 db 访问封装在这样的代码中,以避免在整个过程中禁用检查:

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

【讨论】:

    【解决方案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
    

    由于我使用 South 来管理我的数据库架构,我必须添加以下内容:

    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
    

    这真的很难看,但我没有找到其他方法。

    现在您可以像使用普通 ForeignKey 一样使用 SoftForeignKey 字段,只是您不会执行任何引用完整性。

    查看完整的猴子补丁:http://eve-corp-management.org/projects/ecm/repository/entry/ecm/lib/softfk.py

    【讨论】:

      【解决方案4】:

      我尝试了类似于 Izz ad-Din 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)
      

      【讨论】:

      • 您的问题是使用:related_name='destinations' 对于两个 ForeignKeys 分别使用 managed_destinations 和 unmanaged_destinations。
      【解决方案5】:

      您可以尝试使用非托管模型:

      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
      

      在模型元类中指定managed=False 不会为其创建数据库表。但是,它的行为与其他模型完全一样。

      【讨论】:

        【解决方案6】:

        我通过使用 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

        不利的一面是,您的模型中有三个附加属性。

        此外,反向关系不会自动起作用,但就我而言,我可以接受。

        【讨论】:

        • 这种方法是否适用于其他应用具有外键的应用/包?
        • @EliasPrado 我很确定答案是肯定的,但你能举个例子吗?您希望与哪些应用建立通用关系?
        猜你喜欢
        • 2011-06-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-12-17
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多