Django: "Мягкий" ForeignField без проверки целостности базы данных
У меня есть проект Django с несколькими приложениями django. Один из них имеет модели для представления данных, поступающих из внешнего источника (я не контролирую эти данные).
Я хочу, чтобы мои другие приложения могли иметь ссылки на это "внешнее приложение", но я хочу избежать всех ошибок проверки целостности базы данных. Я не хочу, чтобы у db были какие-либо ограничения на эти "мягкие внешние ключи".
Знаете ли вы, как я могу закодировать настраиваемое поле, которое будет эмулировать настоящий Django ForeignKey без создания жесткого ограничения в базе данных?
Возможно, это уже существует, но мне не повезло с Google.
Заранее спасибо за помощь: -)
NB: я знаю систему generic relations с content_types. Но мне не нужны общие отношения. Я хочу, чтобы конкретные отношения были идентифицированы только без жестких ограничений целостности.
EDIT:
Я нашел связанные ссылки:
Но я не нашел правильного ответа на мой вопрос.: (
EDIT 2012, июнь 4:
Я заглянул глубоко в код django, чтобы найти, что нужно сделать, но я думаю, что просто подклассификация ForeignKey будет недостаточной. Не могли бы вы дать мне несколько советов о том, как это сделать?
NB: Я использую Юг для управления моей схемой базы данных, поэтому я полагаю, что мне тоже нужно что-то сделать. Но это может быть вне темы:)
Ответы
Ответ 1
Ребята,
Мне удалось сделать то, что я хотел.
Сначала я создал новое поле:
from django.db.models.deletion import DO_NOTHING
from django.db.models.fields.related import ForeignKey, ManyToOneRel
class SoftForeignKey(ForeignKey):
"""
This field behaves like a normal django ForeignKey only without hard database constraints.
"""
def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
ForeignKey.__init__(self, to, to_field=to_field, rel_class=rel_class, **kwargs)
self.on_delete = DO_NOTHING
no_db_constraints = True
Поскольку я использую Юг для управления моей схемой базы данных, мне пришлось добавить это:
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], [r'^ecm\.lib\.softfk\.SoftForeignKey'])
Затем я должен был перейти на юг, чтобы он учитывал параметр no_db_constraints
. В создании ограничений FK были задействованы две функции:
from django.db.models.deletion import DO_NOTHING
from django.db.models.fields.related import ForeignKey, ManyToOneRel
from django.core.management.color import no_style
from south.db.generic import DatabaseOperations, invalidate_table_constraints, flatten
def column_sql(self, table_name, field_name, field, tablespace='', with_name=True, field_prepared=False):
"""
Creates the SQL snippet for a column. Used by add_column and add_table.
"""
# If the field hasn't already been told its attribute name, do so.
...
...
...
if field.rel and self.supports_foreign_keys:
# HACK: "soft" FK handling begin
if not hasattr(field, 'no_db_constraints') or not field.no_db_constraints:
self.add_deferred_sql(
self.foreign_key_sql(
table_name,
field.column,
field.rel.to._meta.db_table,
field.rel.to._meta.get_field(field.rel.field_name).column
)
)
# HACK: "soft" FK handling end
# Things like the contrib.gis module fields have this in 1.1 and below
if hasattr(field, 'post_create_sql'):
for stmt in field.post_create_sql(no_style(), ta
....
....
# monkey patch South here
DatabaseOperations.column_sql = column_sql
и
from django.db.models.deletion import DO_NOTHING
from django.db.models.fields.related import ForeignKey, ManyToOneRel
from django.core.management.color import no_style
from south.db.generic import DatabaseOperations, invalidate_table_constraints, flatten
@invalidate_table_constraints
def alter_column(self, table_name, name, field, explicit_name=True, ignore_constraints=False):
"""
Alters the given column name so it will match the given field.
Note that conversion between the two by the database must be possible.
Will not automatically add _id by default; to have this behavour, pass
explicit_name=False.
@param table_name: The name of the table to add the column to
@param name: The name of the column to alter
@param field: The new field definition to use
"""
if self.dry_run:
if self.debug:
...
...
if not ignore_constraints:
# Add back FK constraints if needed
if field.rel and self.supports_foreign_keys:
# HACK: "soft" FK handling begin
if not hasattr(field, 'no_db_constraints') or not field.no_db_constraints:
self.execute(
self.foreign_key_sql(
table_name,
field.column,
field.rel.to._meta.db_table,
field.rel.to._meta.get_field(field.rel.field_name).column
)
)
# HACK: "soft" FK handling end
# monkey patch South here
DatabaseOperations.alter_column = alter_column
Это действительно уродливо, но я не нашел другого пути.
Теперь вы можете использовать поле SoftForeignKey точно так же, как обычный ForeignKey, за исключением того, что у вас не будет каких-либо принудительных принудительных ссылок.
Смотрите здесь полный патч обезьяны: http://eve-corp-management.org/projects/ecm/repository/entry/ecm/lib/softfk.py
Ответ 2
Если вы просто хотите отключить проверку ограничений ForeignKey в поле, просто добавьте db_constraint=False
в это поле.
user = models.ForeignKey('User', db_constraint=False)
См. также:
Django - Как предотвратить ограничение ограничения внешнего ключа базы данных
Ответ 3
Я попробовал что-то похожее на предложение Izz ad-Din Ruhulessin, но это не сработало, потому что у меня есть столбцы, отличные от столбца "fake FK". Код, который я пробовал, был:
class DynamicPkg(models.Model):
@property
def cities(self):
return City.objects.filter(dpdestinations__dynamic_pkg=self)
class DynamicPkgDestination(models.Model):
dynamic_pkg = models.ForeignKey(DynamicPkg, related_name='destinations')
# Indexed because we will be joining City.code to
# DynamicPkgDestination.city_code and we want this to be fast.
city_code = models.CharField(max_length=10, db_index=True)
class UnmanagedDynamicPkgDestination(models.Model):
dynamic_pkg = models.ForeignKey(DynamicPkg, related_name='destinations')
city = models.ForeignKey('City', db_column='city_code', to_field='code', related_name='dpdestinations')
class Meta:
managed = False
db_table = DynamicPkgDestination._meta.db_table
class City(models.Model):
code = models.CharField(max_length=10, unique=True)
а ошибки, которые я получил, были:
Error: One or more models did not validate:
travelbox.dynamicpkgdestination: Accessor for field 'dynamic_pkg' clashes with related field 'DynamicPkg.destinations'. Add a related_name argument to the definition for 'dynamic_pkg'.
travelbox.dynamicpkgdestination: Reverse query name for field 'dynamic_pkg' clashes with related field 'DynamicPkg.destinations'. Add a related_name argument to the definition for 'dynamic_pkg'.
travelbox.unmanageddynamicpkgdestination: Accessor for field 'dynamic_pkg' clashes with related field 'DynamicPkg.destinations'. Add a related_name argument to the definition for 'dynamic_pkg'.
travelbox.unmanageddynamicpkgdestination: Reverse query name for field 'dynamic_pkg' clashes with related field 'DynamicPkg.destinations'. Add a related_name argument to the definition for 'dynamic_pkg'.
Однако я придумал рабочее решение, используя прокси-модель. Мне все же пришлось взломать некоторую проверку Django, которая предотвращает включение полей в прокси-модели:
class DynamicPkg(models.Model):
@property
def cities(self):
return City.objects.filter(dpdestinations__dynamic_pkg=self)
def proxify_model(new_class, base):
"""
Like putting proxy = True in a model Meta except it doesn't spoil your
fun by raising an error if new_class contains model fields.
"""
new_class._meta.proxy = True
# Next 2 lines are what django.db.models.base.ModelBase.__new__ does when
# proxy = True (after it has done its spoil-sport validation ;-)
new_class._meta.setup_proxy(base)
new_class._meta.concrete_model = base._meta.concrete_model
class DynamicPkgDestination(models.Model):
dynamic_pkg = models.ForeignKey(DynamicPkg, related_name='destinations')
# Indexed because we will be joining City.code to
# DynamicPkgDestination.city_code and we want this to be fast.
city_code = city_code_field(db_index=True)
class ProxyDynamicPkgDestination(DynamicPkgDestination):
city = models.ForeignKey('City', db_column='city_code', to_field='code', related_name='dpdestinations')
proxify_model(ProxyDynamicPkgDestination, DynamicPkgDestination)
class City(models.Model):
code = models.CharField(max_length=10, unique=True)
Ответ 4
Вы можете попробовать использовать неуправляемую модель:
from django.db import models
class ReferencedModel(models.Model):
pass
class ManagedModel(models.Model):
my_fake_fk = models.IntegerField(
db_column='referenced_model_id'
)
class UnmanagedModel(models.Model):
my_fake_fk = models.ForeignKey(
ReferencedModel,
db_column='referenced_model_id'
)
class Meta:
managed = False
db_table = ManagedModel._meta.db_table
Указание managed=False
в классе модели Meta не создаст для него таблицу db. Однако он будет вести себя точно так же, как и другие модели.
Ответ 5
Отказываясь от комментария marianobianchi, один из вариантов для ForeignKey.on_delete -
DO_NOTHING: не предпринимайте никаких действий. Если бэкэнд базы данных обеспечивает ссылочную целостность, это вызовет IntegrityError, если вы вручную не добавите ограничение SQL ON DELETE в поле базы данных (возможно, используя начальный sql).
Это в сочетании с отключением ограничений внешнего ключа на уровне db должно сделать трюк. Из того, что я могу сказать, есть два способа сделать это. Вы можете отключить fk-ограничения полностью следующим образом:
from django.db.backend.signals import connection_created
from django.dispatch import receiver
@receiver(connection_created)
def disable_constraints(sender, connection):
connection.disable_constraint_checking()
Похоже, что django db backends также предоставляет контекстный менеджер constraint_checks_disabled, поэтому вы можете обернуть соответствующие обращения к db в коде таким образом, чтобы избежать отключения проверок:
from django.db import connection
with connection.constraint_checks_disabled():
do_stuff()
Ответ 6
Я решил это с помощью GenericForeignKey:
thing_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, blank=True, null=True)
thing_object_id = models.UUIDField(default=uuid.uuid4, blank=True, null=True)
thing = GenericForeignKey(ct_field='thing_content_type', fk_field='thing_object_id')
С положительной стороны, это готовый Django
С отрицательной стороны у вас есть три дополнительных атрибута в вашей модели.
Кроме того, обратные отношения не работают автоматически, но в моем случае я в порядке.