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.