如何解决 Django 中缺乏跨数据库外键支持的问题
我知道 Django 不支持外国跨多个数据库的键(最初是 Django 1.3 文档)
但我正在寻找解决方法。
什么不起作用
我有两个模型,每个模型都在一个单独的数据库上。
routers.py:fruit_app/models.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
模型 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()
尝试在管理中添加“文章”会出现以下错误,因为它正在寻找 Fruit
在错误的数据库上建立模型 ('news_db'
):
DatabaseError at /admin/news_app/article/add/
(1146, "Table 'fkad_news.fruit_app_fruit' doesn't exist")
方法 1:子类 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 字段进行子类化。我修改了 ReverseSingleRelatedObjectDescriptor
以使用 Fruit
模型管理器上的 forced_using
指定的数据库。我还删除了 ForeignKey
子类上的 validate()
方法。该方法没有与方法1相同的问题。github上的代码位于: https://github.com/saltycrane/django-foreign-key-across-db-testproject/tree/foreignkey_subclass
fields.py:fruit_app/models.py:news_app/models.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
:
from django.db import models
class FruitManager(models.Manager):
forced_using = 'default'
class Fruit(models.Model):
name = models.CharField(max_length=20)
objects = FruitManager()
“
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
,我们发现即使我们期望 < code>fruit_app 默认位于 'default'
数据库中,将 instance
提示传递给 db_for_read
进行外键查找它位于'news_db'
数据库中。我们添加了第二个路由器,以确保始终从 'default'
数据库读取 fruit_app
模型。 ForeignKey
子类仅用于“修复”ForeignKey.validate()
方法。 (如果 Django 想支持跨数据库的外键,我会说这是 Django 的 bug。)
代码位于 github 上: https://github.com/saltycrane/ django-foreign-key-across-db-testproject
:
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
routers.py:fruit_app/models.py:news_app/models.py:fields.py
from django.db import models
class Fruit(models.Model):
name = models.CharField(max_length=20)
django -
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
附加
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})
信息
- 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 列表中正在为此编写单元测试。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
您可以在数据库中创建一个包含跨数据库查询的视图,然后在单独的文件中定义该视图的模型以保持syncdb 正常工作。
快乐的编程。 :)
You could make a view in the database that has the cross database query in it, then define the model for the view in a separate file to keep syncdb working.
Happy programming. :)
我知道 Djano-nosql 支持键,尽管有一些来自 http://www.allbuttonspressed 的魔法.com/projects/django-dbindexer。也许其中一些可以有所帮助。
根据描述:
“您只需告诉 dbindexer 哪些模型和字段应该支持这些查询,它就会为您维护所需的索引。”
-凯瑞
I know that Djano-nosql has support for keys and such though some magic from http://www.allbuttonspressed.com/projects/django-dbindexer. Maybe some of that could help.
From the description:
"you can just tell the dbindexer which models and fields should support these queries and it'll take care of maintaining the required indexes for you."
-Kerry
至于
ForeignKeyAcrossDb
部分,你不能对__init__
中的类进行一些调整吗?检查适当的字段是否为Integer
,如果不是,请从数据库加载它,或执行任何其他需要的操作。 Python__class__
es 可以在运行时更改,没有太大问题。As to the
ForeignKeyAcrossDb
part, couldn't you possibly make some adjustments to your class inside__init__
? Check if the appropriate field isInteger
if not, load it from the database, or do anything else that is required. Python__class__
es can be changed at runtime without much problem.在我伤透了几天之后,我设法在同一个银行得到了我的外键!
可以更改表格以在不同的银行寻求外键!
首先,添加一个RECHARGE的FIELDS,都直接(破解)我的表单,在函数____init____
app.form.py
调用表单,
中从视图app.view.py
现在,在源代码DJANGO中更改
ForeignKey类型的字段, ManyToManyField 和 OneToOneField 可以使用“using”,因此添加了一个 IF ...
django.forms.models.py
ALTER FOLLOW FILE
django.db.models.base.py
alter
for
Ready :D
After breaking my head some days, I managed to get my Foreign Key ON THE SAME BANK!
Can be made a change over the FORM to seek a FOREIGN KEY in a different bank!
First, add a RECHARGE of FIELDS, both directly (crack) my form, in function ____init____
app.form.py
calling the Form from the View
app.view.py
Now, the change in the source Code DJANGO
Only fields of type ForeignKey, ManyToManyField and OneToOneField can use the 'using', so added an IF ...
django.forms.models.py
ALTER FOLLOW FILE
django.db.models.base.py
alter
for
Ready :D
外键字段意味着您可以
- 通过加入iefruit__name查询关系
- 检查引用完整性
- 确保删除时的引用完整性
- 管理原始ID查找功能
-(更多...)
第一个用例总是有问题的。
代码库中可能还有一些其他外键特殊情况也不起作用。
我运行一个相当大的 django 站点,我们目前使用一个普通的整数字段。
现在我认为子类化整数字段并将 id 添加到对象转换将是最简单的(在 1.2 中需要修补 django 的一些位,希望现在有所改进)
让您知道我们找到了什么解决方案。
A foreign key field implies that you can
- query on the relationship by joining ie fruit__name
- check referential integrity
- ensure referential integrity upon deletes
- admin raw id lookup functionality
- (some more...)
The first use case would always be problematic.
Probably there are some other foreign key special cases in the codebase which also wouldn't work.
I run a rather large django site and we are currently using a plain integerfield.
For now i would think subclassing the integerfield and adding the id to object conversion would be easiest (in 1.2 that required patching some bits of django, hope that improved by now)
Will let you know what solution we find.
遇到了类似的问题,需要跨多个 (5) 个数据库引用(大部分)静态数据。对 ReversedSingleRelatedObjectDescriptor 进行了轻微更新,以允许设置相关模型。它没有实现反向关系atm。
和
Ran into a similar problem of needing to reference (mostly) static data across multiple (5) databases. Made a slight update to the ReversedSingleRelatedObjectDescriptor to allow setting the related model. It doesn't implement the reverse relationship atm.
and
该解决方案最初是为一个具有迁移功能的托管数据库以及一个或多个具有在数据库级别连接到同一数据库的模型 Meta
managed=False
的旧数据库而编写的。如果db_table
选项包含数据库名称加上表名称正确引用'`'(MySQL)或' " '(其他db),例如db_table = '"DB2"."table_b"'
,则Django不再引用它。查询由Django编译正确的 ORM,即使是JOIN:查询集:
Django 中的所有数据库后端都支持它
(看来我在 重复新问题,我的答案继续。)
This solution is originally written for one managed database with migrations and one or more legacy databases with models Meta
managed=False
connected at database level to the same database. If adb_table
option contains a database name plus table name quoted correctly by ' ` ' (MySQL) or by ' " ' (other db), e.g.db_table = '"DB2"."table_b"'
, then it is not quoted any more by Django. Queries are compiled by Django ORM correctly, even with JOINs:Query set:
That is supported by all db backends in Django.
(It seems that I started a bounty on a duplicate new question where my answer continues.)
受到@Frans 评论的启发。我的解决方法是在业务层执行此操作。在给出这个问题的例子中。我会将结果设置为
Article
上的IntegerField
,作为“不在数据层中进行完整性检查”。然后尊重应用程序代码(业务层)中的引用关系。以 Django admin 为例,为了在文章的添加页面中显示水果作为选项,您需要手动填充水果的选项列表。
当然,您可能需要处理表单字段验证(完整性检查)。
Inspired by @Frans ' comment. My workaround is to do this in business layer. In the example given this question. I would set fruit to an
IntegerField
onArticle
, as "not to do integrity check in data layer".Then honor reference relation in application code (business layer). Take Django admin for example, in order to display fruit as a choice in Article's add page, you populate a list of choices for fruit manually.
Of course you may need to take care of form field validation (integrity check).
我有一个 django v1.10 的新解决方案。有两个部分。它与 django.admin 和 django.rest-framework 一起使用。
ForeignKey
类并创建ForeignKeyAcrossDb
,并基于此票 和此 帖子。db_constraint=False
,例如album=ForeignKeyAcrossDb(Singer, db_constraint=False, on_delete=models.DO_NOTHING)
I have a new solution for django v1.10. There are two parts. It works with django.admin and django.rest-framework.
ForeignKey
class and createForeignKeyAcrossDb
, and override thevalidate()
function, based on this ticket and this post.db_constraint=False
, for example,album=ForeignKeyAcrossDb(Singer, db_constraint=False, on_delete=models.DO_NOTHING)