web-dev-qa-db-ja.com

Django

私は知っています Djangoは複数のデータベース間で外部キーをサポートしていません (元々Django 1.3ドキュメント)

しかし、私は回避策を探しています。

動作しないもの

それぞれ別々のデータベースに2つのモデルがあります。

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

方法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フィールド

2番目の試みとして、ForeignKeyフィールドをサブクラス化してみました。 ReverseSingleRelatedObjectDescriptorのモデルマネージャーでforced_usingによって指定されたデータベースを使用するようにFruitを変更しました。また、ForeignKeyサブクラスの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_readに渡されるinstanceヒントは、それを'news_db'データベースに配置します。 fruit_appモデルが常に'default'データベースから読み取られるように、2番目のルーターを追加しました。 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リストには、このための単体テストが書かれています。

77
saltycrane

数日頭を骨折した後、私はなんとか同じ銀行で外部キーを手に入れることができました!

別の銀行で外部キーを探すためにフォームを変更することができます!

まず、関数_init_に、両方とも直接(クラック)私のフォームにFIELDSの再充電を追加します

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)

for

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

準備完了:D

2
arannasousa

ForeignKeyAcrossDbの部分に関しては、__init__内のクラスにいくつかの調整を加えることができませんでしたか?適切なフィールドがIntegerであるかどうかを確認し、そうでない場合は、データベースからロードするか、その他必要なことを行います。 Python __class__esは、実行時に問題なく変更できます。

2
julkiewicz

データベース間クエリを含むデータベースでビューを作成してから、別のファイルでビューのモデルを定義して、syncdbが機能し続けるようにすることができます。

幸せなプログラミング。 :)

2
adorablepuppy

Djano-nosqlがキーなどをサポートしていることは知っていますが、 http://www.allbuttonspressed.com/projects/Django-dbindexer からいくつかの魔法があります。多分それのいくつかは役立つかもしれません。

説明から:

「どのモデルとフィールドがこれらのクエリをサポートする必要があるかをdbindexerに指示するだけで、必要なインデックスを維持することができます。」

-ケリー

2
Kerry Hatcher

このソリューションは元々、移行を伴う1つの管理対象データベースと、データベースレベルで同じデータベースに接続されたモデルMeta managed=Falseを備えた1つ以上のレガシーデータベース用に作成されています。 db_tableオプションにデータベース名とテーブル名quoted正しく ''(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のすべてのデータベースバックエンドでサポートされています。

(私は 重複した新しい質問 私の答えが続くところに報奨金を始めたようです。)

1
hynekcer

外部キーフィールドは、次のことができることを意味します-結合することで関係をクエリしますiefruit__name-参照整合性をチェックします-削除時に参照整合性を確認します-admin raw idルックアップ機能-(もう少し...)

最初のユースケースは常に問題があります。おそらく、コードベースには他にも機能しない外部キーの特殊なケースがいくつかあります。

私はかなり大きなDjangoサイトを実行しており、現在はプレーンな整数フィールドを使用しています。今のところ、整数フィールドをサブクラス化してIDをオブジェクトに追加するのが最も簡単だと思います(1.2ではパッチを適用する必要がありました) Djangoのビット、今までに改善されることを願っています)私たちが見つけた解決策を教えてくれます。

1
Thierry

Django v1.10の新しいソリューションがあります。2つの部分があります。Django.adminとDjango.rest-frameworkで動作します。

  1. ForeignKeyクラスを継承してForeignKeyAcrossDbを作成し、これに基づいてvalidate()関数をオーバーライドします ticket および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. フィールド宣言では、たとえば、_db_constraint=False_を使用します。

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

0
Diansheng

@Fransのコメントに触発されました。私の回避策は、これをビジネス層で行うことです。この質問を与えられた例では。 「データ層で整合性チェックを行わない」として、IntegerFieldArticleにフルーツを設定します。

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

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

次に、アプリケーションコード(ビジネス層)の参照関係を尊重します。 Django adminを例にとると、Articleの追加ページで果物を選択肢として表示するには、果物の選択肢のリストを手動で入力します。

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

もちろん、フォームフィールドの検証(整合性チェック)の面倒を見る必要があるかもしれません。

0
Sky