programing tip

Django의 데이터베이스에서 외래 키에 대한 지원 부족을 해결하는 방법

itbloger 2020. 10. 20. 07:24
반응형

Django의 데이터베이스에서 외래 키에 대한 지원 부족을 해결하는 방법


Django가 여러 데이터베이스에서 외래 키를 지원하지 않는다는 것을 알고 있습니다 (원래 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()

관리자에서 "문서"를 추가하려고 Fruit하면 잘못된 데이터베이스 ( 'news_db') 에서 모델을 찾고 있기 때문에 다음 오류가 발생합니다 .

DatabaseError at /admin/news_app/article/add/

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

방법 1 : IntegerField 하위 클래스

IntegerField의 하위 클래스 인 사용자 지정 필드 ForeignKeyAcrossDb를 만들었습니다. 코드는 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)

그리고 Article 모델을 다음과 같이 변경했습니다.

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 필드의 서브 클래 싱을 시도했습니다. 의 모델 관리자에 ReverseSingleRelatedObjectDescriptor지정된 데이터베이스를 사용하도록 수정 했습니다 . 또한 하위 클래스 에서 메서드를 제거했습니다 . 이 방법은 방법 1과 같은 문제가 없었습니다. github의 코드 : https://github.com/saltycrane/django-foreign-key-across-db-testproject/tree/foreignkey_subclassforced_usingFruitvalidate()ForeignKey

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. 이 솔루션은 ForeignKey방법 2에서 필요한 수정이 필요하지 않습니다. 에서 Django의 기본 라우팅 동작을 살펴본 django.db.utils.ConnectionRouter결과 , 기본적으로 데이터베이스에 fruit_app있을 것으로 예상했지만 외래 키 조회 위해 전달 'default'instance힌트가이를 설정 했음을 발견 db_for_read했습니다. 'news_db'데이터베이스. fruit_app모델이 항상 'default'데이터베이스 에서 읽혀 지도록 두 번째 라우터를 추가했습니다 . 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 목록에는 이에 대한 단위 테스트가 작성되어 있습니다.


데이터베이스에 교차 데이터베이스 쿼리가있는보기를 만든 다음 별도의 파일에서보기에 대한 모델을 정의하여 syncdb가 계속 작동하도록 할 수 있습니다.

즐거운 프로그래밍. :)


Djano-nosql은 http://www.allbuttonspressed.com/projects/django-dbindexer의 일부 마법에도 불구하고 키 등을 지원한다는 것을 알고 있습니다. 그 중 일부가 도움이 될 수 있습니다.

설명에서 :

"dbindexer에게 이러한 쿼리를 지원해야하는 모델과 필드를 알려 주면 필요한 인덱스를 유지 관리 할 수 ​​있습니다."

-케리


받는 사람으로 ForeignKeyAcrossDb부분, 당신은 아마도 당신의 클래스 내부에 약간의 조정을 할 수 없었다 __init__? 적절한 필드가 Integer아닌지 확인 하고 데이터베이스에서로드하거나 필요한 다른 작업을 수행합니다. Python __class__es는 많은 문제없이 런타임에 변경할 수 있습니다.


며칠 동안 머리를 부러 뜨린 후 같은 은행에서 외래 키를 얻을 수있었습니다!

다른 은행에서 FOREIGN KEY를 찾기 위해 FORM을 변경할 수 있습니다!

먼저, 함수 _ 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 = 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


외래 키 필드는 다음을 수행 할 수 있음을 의미합니다. 예를 들어 fruit__name에 조인하여 관계에 대한 쿼리-참조 무결성 확인-삭제시 참조 무결성 확인-관리 원시 ID 조회 기능-(좀 더 ...)

첫 번째 사용 사례는 항상 문제가 될 것입니다. 아마도 코드베이스에는 작동하지 않는 다른 외래 키 특수 사례가있을 것입니다.

나는 다소 큰 장고 사이트를 운영하고 있으며 현재 일반 정수 필드를 사용하고 있습니다. 지금은 integerfield를 서브 클래 싱하고 id를 객체 변환에 추가하는 것이 가장 쉬울 것이라고 생각할 것입니다.


여러 (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

This solution is originally written for one managed database with migrations and one or more legacy databases with models Meta managed=False connected at database level to the same database. If a db_table option contains a database name plus table name quoted correctly by ' ` ' (MySQL) or by ' " ' (other db), e.g. db_table = '"DB2"."table_b"', then it is not quoted any more by Django. Queries are compiled by Django ORM correctly, even with JOINs:

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

Query set:

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

That is supported by all db backends in Django.

(It seems that I started a bounty on a duplicate new question where my answer continues.)


Inspired by @Frans ' comment. My workaround is to do this in business layer. In the example given this question. I would set fruit to an IntegerField on Article, as "not to do integrity check in data layer".

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

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

Then honor reference relation in application code (business layer). Take Django admin for example, in order to display fruit as a choice in Article's add page, you populate a list of choices for fruit manually.

# 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']

Of course you may need to take care of form field validation (integrity check).


I have a new solution for django v1.10. There are two parts. It works with django.admin and django.rest-framework.

  1. Inherit the ForeignKey class and create ForeignKeyAcrossDb, and override the validate() function, based on this ticket and this post.

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. In field declaration, use db_constraint=False, for example,

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

참고URL : https://stackoverflow.com/questions/5493241/how-to-work-around-lack-of-support-for-foreign-keys-across-databases-in-django

반응형