Ich weiß, dass Django keine Fremdschlüssel in mehreren Datenbanken unterstützt (ursprünglich Django 1.3-Dokumente).
Aber ich suche nach einer Problemumgehung.
Was funktioniert nicht?
Ich habe jeweils zwei Modelle in einer separaten Datenbank.
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
Modell 1 in obst_app / models.py:
from django.db import models
class Fruit(models.Model):
name = models.CharField(max_length=20)
Modell 2 in news_app / models.py:
from django.db import models
class Article(models.Model):
fruit = models.ForeignKey('fruit_app.Fruit')
intro = models.TextField()
Der Versuch, dem Administrator einen "Artikel" hinzuzufügen, führt zu folgendem Fehler, da das Fruit
Modell in der falschen Datenbank gesucht wird ( 'news_db'
):
DatabaseError at /admin/news_app/article/add/
(1146, "Table 'fkad_news.fruit_app_fruit' doesn't exist")
Methode 1: Unterklasse IntegerField
Ich habe ein benutzerdefiniertes Feld erstellt, ForeignKeyAcrossDb, eine Unterklasse von IntegerField. Der Code befindet sich auf github unter: 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)
Und ich habe mein Artikelmodell geändert, um:
class Article(models.Model):
fruit = ForeignKeyAcrossDb(Fruit)
intro = models.TextField()
Das Problem ist, manchmal, wenn ich auf Article.fruit zugreife, ist es eine Ganzzahl, und manchmal ist es das Fruit-Objekt. Ich möchte, dass es immer ein Fruchtobjekt ist. Was muss ich tun, damit der Zugriff auf Article.fruit immer ein Fruit-Objekt zurückgibt?
Als Problemumgehung für meine Problemumgehung habe ich eine fruit_obj
Eigenschaft hinzugefügt , die ich jedoch nach Möglichkeit entfernen möchte:
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
Methode 2: Unterklasse ForeignKey-Feld
Als zweiten Versuch habe ich versucht, das ForeignKey-Feld zu unterordnen. Ich habe geändert ReverseSingleRelatedObjectDescriptor
, um die Datenbank zu verwenden, die forced_using
im Modellmanager von angegeben ist Fruit
. Ich habe auch die validate()
Methode in der ForeignKey
Unterklasse entfernt. Diese Methode hatte nicht das gleiche Problem wie Methode 1. Code auf github unter: 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
ruit_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
Methode 2a: Fügen Sie einen Router fürruit_app hinzu
Diese Lösung verwendet einen zusätzlichen Router für fruit_app
. Für diese Lösung sind keine Änderungen ForeignKey
erforderlich, die in Methode 2 erforderlich waren. Nachdem django.db.utils.ConnectionRouter
wir uns das Standard-Routing-Verhalten von Django in angesehen hatten , stellten wir fest, dass der Hinweis, der für die Suche nach Fremdschlüsseln übergeben wurde, aktiviert wurde, obwohl wir erwartet hatten fruit_app
, dass er sich 'default'
standardmäßig in der Datenbank befindet die Datenbank. Wir haben einen zweiten Router hinzugefügt, um sicherzustellen, dass Modelle immer aus der Datenbank gelesen wurden . Eine Unterklasse wird nur verwendet, um die Methode zu "reparieren" . (Wenn Django Fremdschlüssel datenbankübergreifend unterstützen wollte, würde ich sagen, dass dies ein Django-Fehler ist.) Code befindet sich auf github unter: https://github.com/saltycrane/django-foreign-key-across-db-testprojectinstance
db_for_read
'news_db'
fruit_app
'default'
ForeignKey
ForeignKey.validate()
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
ruit_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})
Zusätzliche Information
- Thread auf der Django-Benutzerliste, die viele Informationen enthält: http://groups.google.com/group/django-users/browse_thread/thread/74bcd1afdeb2f0/0fdfce061124b915
- Versionsverlauf für die Multi-DB-Dokumentation: http://code.djangoproject.com/log/django/trunk/docs/topics/db/multi-db.txt?verbose=on
Aktualisieren
Wir haben die letzte Methode implementiert, nachdem wir unsere Router weiter optimiert haben. Die gesamte Implementierung war ziemlich schmerzhaft, was uns denken lässt, dass wir es falsch machen müssen. Auf der TODO-Liste stehen dazu Unit-Tests.
quelle
fruit
Dinge in dernews
Datenbank erstellen , damit es funktioniert. Wenn Sie im Admin-Tool eine DB-übergreifende Referenz erstellen , wird diese unterbrochen, es sei denn, Sie verwenden die Methodenumgehung, die Ihrerfruit_name
Methode in GitHub ähnelt . Auchsouth
wird schrecklich mit Migrationen verwechselt.Antworten:
Sie können eine Ansicht in der Datenbank erstellen, in der sich die datenbankübergreifende Abfrage befindet, und dann das Modell für die Ansicht in einer separaten Datei definieren, damit die Synchronisierung weiterhin funktioniert.
Viel Spaß beim Programmieren. :) :)
quelle
Ich weiß, dass Djano-nosql Schlüssel und dergleichen unterstützt, obwohl etwas Magie von http://www.allbuttonspressed.com/projects/django-dbindexer . Vielleicht könnte etwas davon helfen.
Aus der Beschreibung:
"Sie können dem Datenbankindexer einfach mitteilen, welche Modelle und Felder diese Abfragen unterstützen sollen, und er sorgt dafür, dass die erforderlichen Indizes für Sie verwaltet werden."
-Kerry
quelle
Könnten
ForeignKeyAcrossDb
Sie nicht einige Anpassungen an Ihrer Klasse vornehmen__init__
? Überprüfen Sie, ob das entsprechende Feld vorhanden istInteger
, laden Sie es aus der Datenbank oder führen Sie andere erforderliche Schritte aus. Python__class__
es kann zur Laufzeit ohne große Probleme geändert werden.quelle
Nachdem ich mir einige Tage den Kopf gebrochen hatte, gelang es mir, meinen Fremdschlüssel auf die gleiche Bank zu bringen!
Kann über das FORMULAR geändert werden, um einen AUSLÄNDISCHEN SCHLÜSSEL in einer anderen Bank zu suchen!
Fügen Sie zunächst in Funktion ____init____ eine RECHARGE of FIELDS hinzu, beide direkt (knacken) meine Form
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()
Aufrufen des Formulars aus der Ansicht
app.view.py
form = forms.FormNewHelpDesk(request.POST or None, using=banco)
Nun die Änderung im Quellcode DJANGO
Nur Felder vom Typ ForeignKey, ManyToManyField und OneToOneField können das 'using' verwenden. Fügen Sie daher eine IF hinzu ...
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
ändern
# line 717 qs = model_class._default_manager.filter(**lookup_kwargs)
zum
# line 717 qs = model_class._default_manager.using(getattr(self, 'db', None)).filter(**lookup_kwargs)
Bereit: D.
quelle
Ein Fremdschlüsselfeld impliziert, dass Sie - die Beziehung abfragen können, indem Sie z. B. Fruchtname beitreten - die referenzielle Integrität überprüfen - die referenzielle Integrität beim Löschen sicherstellen - die Funktion zum Nachschlagen der Raw-ID des Administrators - (einige weitere ...)
Der erste Anwendungsfall wäre immer problematisch. Wahrscheinlich gibt es einige andere Fremdschlüssel-Sonderfälle in der Codebasis, die ebenfalls nicht funktionieren würden.
Ich betreibe eine ziemlich große Django-Site und wir verwenden derzeit ein einfaches Ganzzahlfeld. Im Moment würde ich denken, dass es am einfachsten wäre, das Integer-Feld zu unterklassifizieren und die ID zur Objektkonvertierung hinzuzufügen (in 1.2, in der einige Django-Bits gepatcht werden mussten, hoffe, dass sich dies inzwischen verbessert hat). Lassen Sie Sie wissen, welche Lösung wir finden.
quelle
Es trat ein ähnliches Problem auf, (meistens) statische Daten über mehrere (5) Datenbanken hinweg referenzieren zu müssen. Der ReversedSingleRelatedObjectDescriptor wurde geringfügig aktualisiert, um das zugehörige Modell festlegen zu können. Die umgekehrte Beziehung atm wird nicht implementiert.
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)
und
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
quelle
Diese Lösung wurde ursprünglich für eine verwaltete Datenbank mit Migrationen und eine oder mehrere Legacy-Datenbanken mit Meta-Modellen geschrieben,
managed=False
die auf Datenbankebene mit derselben Datenbank verbunden sind. Wenn einedb_table
Option , um einen Datenbanknamen und Tabellennamen enthält zitiert korrekt durch ‚`‘ (MySQL) oder durch ‚"‘ (andere db), zum Beispieldb_table = '"DB2"."table_b"'
, dann ist es nicht mehr von Django zitiert. Abfragen werden korrekt von Django ORM zusammengestellt, auch mit 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
Abfragesatz:
>>> qs = TableB.objects.all() >>> str(qs.query) 'SELECT "DB2"."table_b"."id" FROM DB2"."table_b"'
Das wird von allen DB-Backends in Django unterstützt.
(Es scheint, dass ich eine Prämie für eine doppelte neue Frage gestartet habe , bei der meine Antwort fortgesetzt wird.)
quelle
Inspiriert von @Frans 'Kommentar. Meine Problemumgehung besteht darin, dies in der Geschäftsschicht zu tun. Im Beispiel dieser Frage. Ich würde Früchte auf ein Set
IntegerField
aufArticle
, als „nicht Integritätsprüfung in Datenschicht zu tun“.class Fruit(models.Model): name = models.CharField() class Article(models.Model): fruit = models.IntegerField() intro = models.TextField()
Beachten Sie dann die Referenzbeziehung im Anwendungscode (Business Layer). Nehmen wir zum Beispiel den Django-Administrator. Um Obst als Auswahl auf der Seite zum Hinzufügen von Artikeln anzuzeigen, füllen Sie manuell eine Liste mit Auswahlmöglichkeiten für Obst aus.
# 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']
Natürlich müssen Sie sich möglicherweise um die Validierung der Formularfelder (Integritätsprüfung) kümmern.
quelle
Ich habe eine neue Lösung für Django v1.10. Es gibt zwei Teile. Es funktioniert mit django.admin und django.rest-framework.
ForeignKey
Klasse und erstellenForeignKeyAcrossDb
und überschreiben Sie dievalidate()
Funktion basierend auf diesem Ticket und diesem Beitrag .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 )
db_constraint=False
in der Felddeklaration beispielsweisealbum=ForeignKeyAcrossDb(Singer, db_constraint=False, on_delete=models.DO_NOTHING)
quelle
TypeError: __init__() got an unexpected keyword argument 'db_constraints'
db_constraint
. Entfernen Sie die nachgestellten 's'.