Как использовать модели django с внешними ключами в разных БД?

У меня есть 2 модели для двух разных баз данных:
Базы данных создавались вручную, но ничего не меняли.

class LinkModel(models.Model): # in 'urls' database
    id = models.AutoField(primary_key=True)
    host_id = models.IntegerField()
    path = models.CharField(max_length=255)

    class Meta:
        db_table = 'links'
        app_label = 'testapp'

    def __unicode__(self):
        return self.path

class NewsModel(models.Model):  # in default database
    id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=50)
    link = models.ForeignKey(LinkModel)

    class Meta:
        db_table = 'news'
        app_label = 'test'

    def __unicode__(self):
        return self.title

После следующего кода возникает ошибка

newsItem, created = NewsModel.objects.get_or_create( title="test" )
link = LinkModel.objects.using('urls').get( id=1 )
newsItem.link = link  # error!

 Cannot assign "<LinkModel: />": instance is on database "default", value is on database "urls"

Почему я не могу использовать внешний ключ и модель для разных баз данных?

Ответ 1

Ограничения между базами данных

В настоящее время Django не поддерживает поддержку внешних ключей или отношений "многие ко многим", охватывающих несколько баз данных. Если вы использовали маршрутизатор для разделения моделей на разные базы данных, любые внешние ключи и отношения "многие ко многим", определенные этими моделями, должны быть внутренними для одной базы данных.

Django - ограничения для нескольких баз данных

Проблема

Такая же проблема. Ошибка в классе ForeignKey().

В методе validate().

Посмотреть билет

Ошибка существует в v1.2, v1.3, v1.4rc1

Решение

Попробуйте этот патч, чтобы решить эту проблему.

Ответ 2

Проблема

* Примечание: это продолжение ответа Виталия Фадеева

Из-за желания сохранить ссылочную целостность Django не позволяет использовать внешние ключи, которые охватывают несколько баз данных: https://docs.djangoproject.com/en/dev//topics/db/multi-db/#limitations-of-multiple-databases. Хотя это желательно в 99% всех приложений, в некоторых случаях полезно создать такую ​​ассоциацию в ORM, даже если вы не можете обеспечить ссылочную целостность.

Решение

Я создал Gist, который использует предложенное решение здесь Виталий Фадеев завернутый как подкласс поля Django ForeignKey. Это решение не требует изменения файлов Django Core, но вместо этого используйте этот тип поля в тех случаях, когда это необходимо.

Пример использования

# app1/models.py
from django.db import models

class ClientModel(models.Model)
    name = models.CharField()
    class Meta:
        app_label = 'app1'

# app2/models.py
from django.db import models
from some_location.related import SpanningForeignKey

class WidgetModel(models.Model):
    client = SpanningForeignKey('app1.ClientModel', default=None, null=True,
                                blank=True, verbose_name='Client')

Гист

Сущность доступна здесь: https://gist.github.com/gcko/de1383080e9f8fb7d208

Скопировано здесь для удобства доступа:

from django.core import exceptions
from django.db.models.fields.related import ForeignKey
from django.db.utils import ConnectionHandler, ConnectionRouter

connections = ConnectionHandler()
router = ConnectionRouter()


class SpanningForeignKey(ForeignKey):

    def validate(self, value, model_instance):
        if self.rel.parent_link:
            return
        # Call the grandparent rather than the parent to skip validation
        super(ForeignKey, self).validate(value, model_instance)
        if value is None:
            return

        using = router.db_for_read(self.rel.to, instance=model_instance)
        qs = self.rel.to._default_manager.using(using).filter(
            **{self.rel.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.rel.to._meta.verbose_name, 'pk': value,
                    'field': self.rel.field_name, 'value': value,
                },  # 'pk' is included for backwards compatibility
            )

Ответ 3

Как альтернатива (немного хакерская), вы можете подклассифицировать ForeignKey, чтобы проверить наличие экземпляра внутри правого db:

class CrossDbForeignKey(models.ForeignKey):
    def validate(self, value, model_instance):
        if self.rel.parent_link:
            return
        super(models.ForeignKey, self).validate(value, model_instance)
        if value is None:
            return

        # Here is the trick, get db relating to fk, not to root model
        using = router.db_for_read(self.rel.to, instance=model_instance)

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

то едва:

class NewsModel(models.Model):  # in default database
    …
    link = models.CrossDbForeignKey(LinkModel)

Обратите внимание, что это соответствует более или менее патчу, о котором говорит Виталий, но в этом случае не требуется исправлять исходный код django.

Ответ 4

Несколько раз разбив мне голову, мне удалось получить свой внешний ключ в том же банке!

Можно внести изменения в FORM, чтобы искать FOREIGN KEY в другом банке!

Сначала добавьте RECHARGE FIELDS, как напрямую, так и трещину в моей форме, в функции _init _

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 Key`s
        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 могут использовать "использование", поэтому добавлено 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)

ALTER FOLLOW FILE

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

Использование нескольких баз данных делает вещи более сложными.

Читайте: MultipleDB Django

Для таких вещей, которые вам нужны, вы должны использовать Маршрутизаторы баз данных, как описано в ссылке, насколько я знаю. Я никогда не использовал множественную настройку БД с внешними ключами между ними, но именно там я начал бы работать.