【发布时间】:2011-07-26 11:32:17
【问题描述】:
我知道Django does not support foreign keys across multiple databases(最初是 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()
尝试在管理员中添加“文章”会出现以下错误,因为它正在错误的数据库 ('news_db') 上查找 Fruit 模型:
DatabaseError at /admin/news_app/article/add/
(1146, "Table 'fkad_news.fruit_app_fruit' doesn't exist")
方法一:子类 IntegerField
我创建了一个自定义字段 ForeignKeyAcrossDb,它是 IntegerField 的子类。代码在 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)
我将我的文章模型更改为:
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 字段进行子类化。我在Fruit的模型管理器上修改ReverseSingleRelatedObjectDescriptor使用forced_using指定的数据库。我还删除了 validate() 子类上的 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'数据库中。我们添加了第二个路由器以确保始终从'default' 数据库中读取fruit_app 模型。 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})
其他信息
- django-users 列表中的线程,其中包含大量信息:http://groups.google.com/group/django-users/browse_thread/thread/74bcd1afdeb2f0/0fdfce061124b915
- 多数据库文档的修订历史记录:http://code.djangoproject.com/log/django/trunk/docs/topics/db/multi-db.txt?verbose=on
更新
在对路由器进行了更多调整后,我们实施了最后一种方法。整个实施过程非常痛苦,这让我们认为我们一定做错了。 TODO 列表中正在为此编写单元测试。
【问题讨论】:
-
我讨厌一个问题没有答案,但该死的谁能回答这个问题
-
看来这个问题需要悬赏
-
为什么不简单地将两个模型放在一个数据库中呢? IE。您决定反对这种(明显的)解决方法的具体原因是什么?
-
您的表分布在多个数据库中这一事实意味着它们位于不同的域中。在这种情况下,总是由您的应用程序代码来执行“完整性检查”。这进入您的业务层。您的解决方案如此“痛苦”的原因是您试图在数据层中执行此操作。如果这些表不在不同的域中,我将支持 sampablokuper 的评论:为什么它们不在同一个数据库中?
-
我很想知道您是否还在使用 2a:它不能很好地工作。我必须在
newsDB 中为fruit的东西创建一些空表才能使其工作,如果您在管理工具中进行跨数据库引用,除非您使用类似于您的@的方法解决方法,否则它会中断GitHub 中的 987654364@ 方法。此外,south与迁移非常混淆。
标签: django django-models django-orm