【问题标题】:How to work around lack of support for foreign keys across databases in Django如何解决 Django 中跨数据库的外键支持不足的问题
【发布时间】:2011-07-26 11:32:17
【问题描述】:

我知道Django does not support foreign keys across multiple databases(最初是 Django 1.3 文档)

但我正在寻找解决方法。

什么不起作用

我有两个模型,每个模型都在一个单独的数据库中。

routers.py:

class NewsRouter(object):
    def db_for_read(self, model, **hints):
        if model._meta.app_label == 'news_app':
            return 'news_db'
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label == 'news_app':
            return 'news_db'
        return None

    def allow_relation(self, obj1, obj2, **hints):
        if obj1._meta.app_label == 'news_app' or obj2._meta.app_label == 'news_app':
            return True
        return None

    def allow_syncdb(self, db, model):
        if db == 'news_db':
            return model._meta.app_label == 'news_app'
        elif model._meta.app_label == 'news_app':
            return False
        return None

fruit_app/models.py 中的模型 1:

from django.db import models

class Fruit(models.Model):
    name = models.CharField(max_length=20)

news_app/models.py 中的模型 2:

from django.db import models

class Article(models.Model):
    fruit = models.ForeignKey('fruit_app.Fruit')
    intro = models.TextField()

尝试在管理员中添加“文章”会出现以下错误,因为它正在错误的数据库 ('news_db') 上查找 Fruit 模型:

DatabaseError at /admin/news_app/article/add/

(1146, "Table 'fkad_news.fruit_app_fruit' doesn't exist")

方法一:子类 IntegerField

我创建了一个自定义字段 ForeignKeyAcrossDb,它是 IntegerField 的子类。代码在 github 上:https://github.com/saltycrane/django-foreign-key-across-db-testproject/tree/integerfield_subclass

fields.py:

from django.db import models


class ForeignKeyAcrossDb(models.IntegerField):
    '''
    Exists because foreign keys do not work across databases
    '''
    def __init__(self, model_on_other_db, **kwargs):
        self.model_on_other_db = model_on_other_db
        super(ForeignKeyAcrossDb, self).__init__(**kwargs)

    def to_python(self, value):
        # TODO: this db lookup is duplicated in get_prep_lookup()
        if isinstance(value, self.model_on_other_db):
            return value
        else:
            return self.model_on_other_db._default_manager.get(pk=value)

    def get_prep_value(self, value):
        if isinstance(value, self.model_on_other_db):
            value = value.pk
        return super(ForeignKeyAcrossDb, self).get_prep_value(value)

    def get_prep_lookup(self, lookup_type, value):
        # TODO: this db lookup is duplicated in to_python()
        if not isinstance(value, self.model_on_other_db):
            value = self.model_on_other_db._default_manager.get(pk=value)

        return super(ForeignKeyAcrossDb, self).get_prep_lookup(lookup_type, value)

我将我的文章模型更改为:

class Article(models.Model):
    fruit = ForeignKeyAcrossDb(Fruit)
    intro = models.TextField()

问题是,有时我访问 Article.fruit 时,它是一个整数,有时它是 Fruit 对象。我希望它始终是一个 Fruit 对象。我需要做什么才能使访问 Article.fruit 始终返回 Fruit 对象?

作为我的解决方法,我添加了一个 fruit_obj 属性,但如果可能,我想消除它:

class Article(models.Model):
    fruit = ForeignKeyAcrossDb(Fruit)
    intro = models.TextField()

    # TODO: shouldn't need fruit_obj if ForeignKeyAcrossDb field worked properly
    @property
    def fruit_obj(self):
        if not hasattr(self, '_fruit_obj'):
            # TODO: why is it sometimes an int and sometimes a Fruit object?
            if isinstance(self.fruit, int) or isinstance(self.fruit, long):
                print 'self.fruit IS a number'
                self._fruit_obj = Fruit.objects.get(pk=self.fruit)
            else:
                print 'self.fruit IS NOT a number'
                self._fruit_obj = self.fruit
        return self._fruit_obj

    def fruit_name(self):
        return self.fruit_obj.name

方法2:子类ForeignKey字段

作为第二次尝试,我尝试对 ForeignKey 字段进行子类化。我在Fruit的模型管理器上修改ReverseSingleRelatedObjectDescriptor使用forced_using指定的数据库。我还删除了 validate() 子类上的 validate() 方法。这个方法和方法1没有同样的问题。github上的代码在:https://github.com/saltycrane/django-foreign-key-across-db-testproject/tree/foreignkey_subclass

fields.py:

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


class ReverseSingleRelatedObjectDescriptor(object):
    # This class provides the functionality that makes the related-object
    # managers available as attributes on a model class, for fields that have
    # a single "remote" value, on the class that defines the related field.
    # In the example "choice.poll", the poll attribute is a
    # ReverseSingleRelatedObjectDescriptor instance.
    def __init__(self, field_with_rel):
        self.field = field_with_rel

    def __get__(self, instance, instance_type=None):
        if instance is None:
            return self

        cache_name = self.field.get_cache_name()
        try:
            return getattr(instance, cache_name)
        except AttributeError:
            val = getattr(instance, self.field.attname)
            if val is None:
                # If NULL is an allowed value, return it.
                if self.field.null:
                    return None
                raise self.field.rel.to.DoesNotExist
            other_field = self.field.rel.get_related_field()
            if other_field.rel:
                params = {'%s__pk' % self.field.rel.field_name: val}
            else:
                params = {'%s__exact' % self.field.rel.field_name: val}

            # If the related manager indicates that it should be used for
            # related fields, respect that.
            rel_mgr = self.field.rel.to._default_manager
            db = router.db_for_read(self.field.rel.to, instance=instance)
            if getattr(rel_mgr, 'forced_using', False):
                db = rel_mgr.forced_using
                rel_obj = rel_mgr.using(db).get(**params)
            elif getattr(rel_mgr, 'use_for_related_fields', False):
                rel_obj = rel_mgr.using(db).get(**params)
            else:
                rel_obj = QuerySet(self.field.rel.to).using(db).get(**params)
            setattr(instance, cache_name, rel_obj)
            return rel_obj

    def __set__(self, instance, value):
        raise NotImplementedError()

class ForeignKeyAcrossDb(models.ForeignKey):

    def contribute_to_class(self, cls, name):
        models.ForeignKey.contribute_to_class(self, cls, name)
        setattr(cls, self.name, ReverseSingleRelatedObjectDescriptor(self))
        if isinstance(self.rel.to, basestring):
            target = self.rel.to
        else:
            target = self.rel.to._meta.db_table
        cls._meta.duplicate_targets[self.column] = (target, "o2m")

    def validate(self, value, model_instance):
        pass

fruit_app/models.py:

from django.db import models


class FruitManager(models.Manager):
    forced_using = 'default'


class Fruit(models.Model):
    name = models.CharField(max_length=20)

    objects = FruitManager()

news_app/models.py:

from django.db import models

from foreign_key_across_db_testproject.fields import ForeignKeyAcrossDb
from foreign_key_across_db_testproject.fruit_app.models import Fruit


class Article(models.Model):
    fruit = ForeignKeyAcrossDb(Fruit)
    intro = models.TextField()

    def fruit_name(self):
        return self.fruit.name

方法2a:为fruit_app添加路由器

此解决方案为fruit_app 使用额外的路由器。此解决方案不需要对方法 2 中所需的 ForeignKey 进行修改。在查看了 django.db.utils.ConnectionRouter 中 Django 的默认路由行为后,我们发现即使我们期望 fruit_app 默认在 'default' 数据库上,传递给db_for_readinstance提示用于外键查找将其放在'news_db'数据库中。我们添加了第二个路由器以确保始终从'default' 数据库中读取fruit_app 模型。 ForeignKey 子类仅用于“修复”ForeignKey.validate() 方法。 (如果 Django 想要跨数据库支持外键,我会说这是一个 Django 错误。) 代码在 github 上:https://github.com/saltycrane/django-foreign-key-across-db-testproject

routers.py:

class NewsRouter(object):
    def db_for_read(self, model, **hints):
        if model._meta.app_label == 'news_app':
            return 'news_db'
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label == 'news_app':
            return 'news_db'
        return None

    def allow_relation(self, obj1, obj2, **hints):
        if obj1._meta.app_label == 'news_app' or obj2._meta.app_label == 'news_app':
            return True
        return None

    def allow_syncdb(self, db, model):
        if db == 'news_db':
            return model._meta.app_label == 'news_app'
        elif model._meta.app_label == 'news_app':
            return False
        return None


class FruitRouter(object):
    def db_for_read(self, model, **hints):
        if model._meta.app_label == 'fruit_app':
            return 'default'
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label == 'fruit_app':
            return 'default'
        return None

    def allow_relation(self, obj1, obj2, **hints):
        if obj1._meta.app_label == 'fruit_app' or obj2._meta.app_label == 'fruit_app':
            return True
        return None

    def allow_syncdb(self, db, model):
        if db == 'default':
            return model._meta.app_label == 'fruit_app'
        elif model._meta.app_label == 'fruit_app':
            return False
        return None

fruit_app/models.py:

from django.db import models


class Fruit(models.Model):
    name = models.CharField(max_length=20)

news_app/models.py:

from django.db import models

from foreign_key_across_db_testproject.fields import ForeignKeyAcrossDb
from foreign_key_across_db_testproject.fruit_app.models import Fruit


class Article(models.Model):
    fruit = ForeignKeyAcrossDb(Fruit)
    intro = models.TextField()

    def fruit_name(self):
        return self.fruit.name

fields.py:

from django.core import exceptions
from django.db import models
from django.db import router


class ForeignKeyAcrossDb(models.ForeignKey):

    def validate(self, value, model_instance):
        if self.rel.parent_link:
            return
        models.Field.validate(self, value, model_instance)
        if value is None:
            return

        using = router.db_for_read(self.rel.to, instance=model_instance)  # is this more correct than Django's 1.2.5 version?
        qs = self.rel.to._default_manager.using(using).filter(
                **{self.rel.field_name: value}
             )
        qs = qs.complex_filter(self.rel.limit_choices_to)
        if not qs.exists():
            raise exceptions.ValidationError(self.error_messages['invalid'] % {
                'model': self.rel.to._meta.verbose_name, 'pk': value})

其他信息

更新

在对路由器进行了更多调整后,我们实施了最后一种方法。整个实施过程非常痛苦,这让我们认为我们一定做错了。 TODO 列表中正在为此编写单元测试。

【问题讨论】:

  • 我讨厌一个问题没有答案,但该死的谁能回答这个问题
  • 看来这个问题需要悬赏
  • 为什么不简单地将两个模型放在一个数据库中呢? IE。您决定反对这种(明显的)解决方法的具体原因是什么?
  • 您的表分布在多个数据库中这一事实意味着它们位于不同的域中。在这种情况下,总是由您的应用程序代码来执行“完整性检查”。这进入您的业务层。您的解决方案如此“痛苦”的原因是您试图在数据层中执行此操作。如果这些表不在不同的域中,我将支持 sampablokuper 的评论:为什么它们不在同一个数据库中?
  • 我很想知道您是否还在使用 2a:它不能很好地工作。我必须在news DB 中为fruit 的东西创建一些空表才能使其工作,如果您在管理工具中进行跨数据库引用,除非您使用类似于您的@的方法解决方法,否则它会中断GitHub 中的 987654364@ 方法。此外,south 与迁移非常混淆。

标签: django django-models django-orm


【解决方案1】:

您可以在其中包含跨数据库查询的数据库中创建一个视图,然后在单独的文件中为该视图定义模型以保持同步数据库正常工作。

快乐编程。 :)

【讨论】:

  • 这只有在两个数据库类型相同并且运行在同一个数据库服务器上时才有效...
【解决方案2】:

我知道 Djano-nosql 支持键等,尽管来自 http://www.allbuttonspressed.com/projects/django-dbindexer 的一些魔法。也许其中一些可以提供帮助。

来自描述:

“你可以告诉 dbindexer 哪些模型和字段应该支持这些查询,它会为你维护所需的索引。”

-克里

【讨论】:

    【解决方案3】:

    至于ForeignKeyAcrossDb 部分,您不能对__init__ 中的类进行一些调整吗?检查适当的字段是否为Integer,如果不是,则从数据库中加载它,或执行其他任何需要的操作。 Python __class__es 可以在运行时更改,没有太大问题。

    【讨论】:

      【解决方案4】:

      几天后,我设法让我的外键在同一家银行!

      可以对FORM进行更改以在不同的银行寻找FOREIGN KEY!

      首先,在函数__init____中直接(破解)我的表单,添加FIELDS的RECHARGE

      app.form.py

      # -*- coding: utf-8 -*-
      from django import forms
      import datetime
      from app_ti_helpdesk import models as mdp
      
      #classe para formulario de Novo HelpDesk
      class FormNewHelpDesk(forms.ModelForm):
          class Meta:
              model = mdp.TblHelpDesk
              fields = (
              "problema_alegado",
              "cod_direcionacao",
              "data_prevista",
              "hora_prevista",
              "atendimento_relacionado_a",
              "status",
              "cod_usuario",
              )
      
          def __init__(self, *args, **kwargs):
              #-------------------------------------
              #  using remove of kwargs
              #-------------------------------------
              db = kwargs.pop("using", None)
      
              # CASE use Unique Keys
              self.Meta.model.db = db
      
              super(FormNewHelpDesk, self).__init__(*args,**kwargs)
      
              #-------------------------------------
              #   recreates the fields manually
              from copy import deepcopy
              self.fields.update(deepcopy( forms.fields_for_model( self.Meta.model, self.Meta.fields, using=db ) ))
              #
              #-------------------------------------
      
              #### follows the standard template customization, if necessary
      
              self.fields['problema_alegado'].widget.attrs['rows'] = 3
              self.fields['problema_alegado'].widget.attrs['cols'] = 22
              self.fields['problema_alegado'].required = True
              self.fields['problema_alegado'].error_messages={'required': 'Necessário informar o motivo da solicitação de ajuda!'}
      
      
              self.fields['data_prevista'].widget.attrs['class'] = 'calendario'
              self.fields['data_prevista'].initial = (datetime.timedelta(4)+datetime.datetime.now().date()).strftime("%Y-%m-%d")
      
              self.fields['hora_prevista'].widget.attrs['class'] = 'hora'
              self.fields['hora_prevista'].initial =datetime.datetime.now().time().strftime("%H:%M")
      
              self.fields['status'].initial = '0'                 #aberto
              self.fields['status'].widget.attrs['disabled'] = True
      
              self.fields['atendimento_relacionado_a'].initial = '07'
      
              self.fields['cod_direcionacao'].required = True
              self.fields['cod_direcionacao'].label = "Direcionado a"
              self.fields['cod_direcionacao'].initial = '2'
              self.fields['cod_direcionacao'].error_messages={'required': 'Necessário informar para quem é direcionado a ajuda!'}
      
              self.fields['cod_usuario'].widget = forms.HiddenInput()
      

      从视图中调用表单

      app.view.py

      form = forms.FormNewHelpDesk(request.POST or None, using=banco)
      

      现在,源代码 DJANGO 的变化

      只有 ForeignKey、ManyToManyField 和 OneToOneField 类型的字段可以使用 'using',所以添加了一个 IF ...

      django.forms.models.py

      # line - 133: add using=None
      def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None, using=None):
      
      # line - 159
      
      if formfield_callback is None:
          #----------------------------------------------------
          from django.db.models.fields.related import (ForeignKey, ManyToManyField, OneToOneField)
          if type(f) in (ForeignKey, ManyToManyField, OneToOneField):
              kwargs['using'] = using
      
          formfield = f.formfield(**kwargs)
          #----------------------------------------------------
      elif not callable(formfield_callback):
          raise TypeError('formfield_callback must be a function or callable')
      else:
          formfield = formfield_callback(f, **kwargs)
      

      更改关注文件

      django.db.models.base.py

      改变

      # line 717
      qs = model_class._default_manager.filter(**lookup_kwargs)
      

      # line 717
      qs = model_class._default_manager.using(getattr(self, 'db', None)).filter(**lookup_kwargs)
      

      准备好了:D

      【讨论】:

        【解决方案5】:

        外键字段意味着您可以 - 通过加入 iefruit__name 查询关系 - 检查参照完整性 - 确保删除时的参照完整性 - 管理原始 ID 查找功能 - (更多...)

        第一个用例总是有问题的。 代码库中可能还有一些其他外键特殊情况也不起作用。

        我运行一个相当大的 django 站点,我们目前正在使用一个普通的整数字段。 现在我认为继承整数字段并将 id 添加到对象转换将是最简单的(在 1.2 中需要修补 django 的一些位,希望现在有所改进) 会让您知道我们找到了什么解决方案。

        【讨论】:

          【解决方案6】:

          遇到了需要跨多个 (5) 数据库引用(大部分)静态数据的类似问题。对 ReversedSingleRelatedObjectDescriptor 进行了轻微更新,以允许设置相关模型。它没有实现反向关系atm。

          class ReverseSingleRelatedObjectDescriptor(object):
          """
          This class provides the functionality that makes the related-object managers available as attributes on a model
          class, for fields that have a single "remote" value, on the class that defines the related field. Used with
          LinkedField.
          """
          def __init__(self, field_with_rel):
              self.field = field_with_rel
              self.cache_name = self.field.get_cache_name()
          
          def __get__(self, instance, instance_type=None):
              if instance is None:
                  return self
          
              try:
                  return getattr(instance, self.cache_name)
              except AttributeError:
                  val = getattr(instance, self.field.attname)
                  if val is None:
                      # If NULL is an allowed value, return it
                      if self.field.null:
                          return None
                      raise self.field.rel.to.DoesNotExist
                  other_field = self.field.rel.get_related_field()
                  if other_field.rel:
                      params = {'%s__pk' % self.field.rel.field_name: val}
                  else:
                      params = {'%s__exact' % self.field.rel.field_name: val}
          
                  # If the related manager indicates that it should be used for related fields, respect that.
                  rel_mgr = self.field.rel.to._default_manager
                  db = router.db_for_read(self.field.rel.to, instance=instance)
                  if getattr(rel_mgr, 'forced_using', False):
                      db = rel_mgr.forced_using
                      rel_obj = rel_mgr.using(db).get(**params)
                  elif getattr(rel_mgr, 'use_for_related_fields', False):
                      rel_obj = rel_mgr.using(db).get(**params)
                  else:
                      rel_obj = QuerySet(self.field.rel.to).using(db).get(**params)
                  setattr(instance, self.cache_name, rel_obj)
                  return rel_obj
          
          def __set__(self, instance, value):
              if instance is None:
                  raise AttributeError("%s must be accessed via instance" % self.field.name)
          
              # If null=True, we can assign null here, but otherwise the value needs to be an instance of the related class.
              if value is None and self.field.null is False:
                  raise ValueError('Cannot assign None: "%s.%s" does not allow null values.' %
                                   (instance._meta.object_name, self.field.names))
              elif value is not None and not isinstance(value, self.field.rel.to):
                  raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' %
                                   (value, instance._meta.object_name, self.field.name, self.field.rel.to._meta.object_name))
              elif value is not None:
                  # Only check the instance state db, LinkedField implies that the value is on a different database
                  if instance._state.db is None:
                      instance._state.db = router.db_for_write(instance.__class__, instance=value)
          
              # Is not used by OneToOneField, no extra measures to take here
          
              # Set the value of the related field
              try:
                  val = getattr(value, self.field.rel.get_related_field().attname)
              except AttributeError:
                  val = None
              setattr(instance, self.field.attname, val)
          
              # Since we already know what the related object is, seed the related object caches now, too. This avoids another
              # db hit if you get the object you just set
              setattr(instance, self.cache_name, value)
              if value is not None and not self.field.rel.multiple:
                  setattr(value, self.field.related.get_cache_name(), instance)
          

          class LinkedField(models.ForeignKey):
          """
          Field class used to link models across databases. Does not ensure referrential integraty like ForeignKey
          """
          def _description(self):
              return "Linked Field (type determined by related field)"
          
          def contribute_to_class(self, cls, name):
              models.ForeignKey.contribute_to_class(self, cls, name)
              setattr(cls, self.name, ReverseSingleRelatedObjectDescriptor(self))
              if isinstance(self.rel.to, basestring):
                  target = self.rel.to
              else:
                  target = self.rel.to._meta.db_table
              cls._meta.duplicate_targets[self.column] = (target, "o2m")
          
          def validate(self, value, model_instance):
              pass
          

          【讨论】:

            【解决方案7】:

            此解决方案最初是为一个具有迁移功能的托管数据库和一个或多个具有模型 Meta managed=False 在数据库级别连接到同一数据库的旧数据库编写的。如果 db_table 选项包含由 ' ` ' (MySQL) 或 ' 正确引用的数据库名称和表名称'(其他数据库),例如db_table = '"DB2"."table_b"',则Django不再引用它。查询由Django ORM正确编译,即使使用JOIN:

            class TableB(models.Model):
                ....
                class Meta:    
                    db_table = '`DB2`.`table_b`'    # for MySQL
                    # db_table = '"DB2"."table_b"'  # for all other backends
                    managed = False
            

            查询集:

            >>> qs = TableB.objects.all()
            >>> str(qs.query)
            'SELECT "DB2"."table_b"."id" FROM DB2"."table_b"'
            

            Django 中的所有数据库后端都支持。

            (看来我在duplicate new question 上开始了赏金活动,我的回答仍在继续。)

            【讨论】:

              【解决方案8】:

              受@Frans 评论的启发。我的解决方法是在业务层执行此操作。在给出这个问题的例子中。我会在Article 上设置IntegerField,因为“不在数据层中进行完整性检查”。

              class Fruit(models.Model):
                  name = models.CharField()
              
              class Article(models.Model):
                  fruit = models.IntegerField()
                  intro = models.TextField()
              

              然后在应用程序代码(业务层)中尊重引用关系。以 Django admin 为例,为了在文章的添加页面中显示水果作为选项,您需要手动填充水果选项列表。

              # admin.py in App article
              class ArticleAdmin(admin.ModelAdmin):
                  class ArticleForm(forms.ModelForm):
                      fields = ['fruit', 'intro']
              
                      # populate choices for fruit
                      choices = [(obj.id, obj.name) for obj in Fruit.objects.all()]
                      widgets = {
                          'fruit': forms.Select(choices=choices)}
              
                  form = ArticleForm
                  list_diaplay = ['fruit', 'intro']
              

              当然,您可能需要处理表单字段验证(完整性检查)。

              【讨论】:

                【解决方案9】:

                我有一个 django v1.10 的新解决方案。有两个部分。它适用于 django.admin 和 django.rest-framework。

                1. 继承ForeignKey类并创建ForeignKeyAcrossDb,并基于此ticket和此post重写validate()函数。

                class ForeignKeyAcrossDb(models.ForeignKey):
                        def validate(self, value, model_instance):
                            if self.remote_field.parent_link:
                                return
                            super(models.ForeignKey, self).validate(value, model_instance)
                            if value is None:
                                return
                            using = router.db_for_read(self.remote_field.model, instance=model_instance)
                            qs = self.remote_field.model._default_manager.using(using).filter(
                                **{self.remote_field.field_name: value}
                            )
                            qs = qs.complex_filter(self.get_limit_choices_to())
                            if not qs.exists():
                                raise exceptions.ValidationError(
                                    self.error_messages['invalid'],
                                    code='invalid',
                                    params={
                                        'model': self.remote_field.model._meta.verbose_name, 'pk': value,
                                        'field': self.remote_field.field_name, 'value': value,
                                    },  # 'pk' is included for backwards compatibility
                                )
                
                1. 在字段声明中,使用db_constraint=False,例如,

                album=ForeignKeyAcrossDb(Singer, db_constraint=False, on_delete=models.DO_NOTHING)

                【讨论】:

                • TypeError: __init__() got an unexpected keyword argument 'db_constraints'
                • @HosseinJabbari 我道歉。这是一个错字,请使用db_constraint。删除尾随的“s”。
                • 如果您复制代码,命名您的来源被认为是一种礼貌:stackoverflow.com/a/32078727/7933618
                • @masterfloda,你的链接不是我在答案中添加的链接吗?
                猜你喜欢
                • 2021-02-09
                • 2016-04-10
                • 1970-01-01
                • 2020-05-05
                • 2021-10-04
                • 2010-10-23
                • 2017-08-09
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多