queryset - Cómo evitar la falta de soporte para claves externas en las bases de datos en Django
django models tutorial (9)
¡Después de romperme la cabeza algunos días, logré obtener mi Foreign Key EN EL MISMO BANCO!
¡Se puede hacer un cambio sobre el FORMULARIO para buscar una LLAVE EXTRAÑA en un banco diferente!
Primero, agregue una RECARGA de CAMPOS, ambos directamente (crack) mi formulario, en la función _ init _
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 = 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()
llamando al formulario desde la vista
app.view.py
form = forms.FormNewHelpDesk(request.POST or None, using=banco)
Ahora, el cambio en el código fuente DJANGO
Solo los campos de tipo ForeignKey, ManyToManyField y OneToOneField pueden usar el ''uso'', por lo que se agrega un IF ...
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
alterar
# line 717
qs = model_class._default_manager.filter(**lookup_kwargs)
para
# line 717
qs = model_class._default_manager.using(getattr(self, ''db'', None)).filter(**lookup_kwargs)
Listo: D
Sé que Django no admite claves externas en múltiples bases de datos (originalmente Django 1.3 docs)
Pero estoy buscando una solución alternativa.
Lo que no funciona
Tengo dos modelos cada uno en una base de datos separada.
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
Modelo 1 en fruit_app / models.py:
from django.db import models
class Fruit(models.Model):
name = models.CharField(max_length=20)
Modelo 2 en news_app / models.py:
from django.db import models
class Article(models.Model):
fruit = models.ForeignKey(''fruit_app.Fruit'')
intro = models.TextField()
Al intentar agregar un "Artículo" en el administrador aparece el siguiente error porque está buscando el modelo de Fruit
en la base de datos incorrecta ( ''news_db''
):
DatabaseError at /admin/news_app/article/add/
(1146, "Table ''fkad_news.fruit_app_fruit'' doesn''t exist")
Método 1: subclase IntegerField
Creé un campo personalizado, ForeignKeyAcrossDb, que es una subclase de IntegerField. El código está en github en: 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)
Y cambié mi modelo de artículo para que sea:
class Article(models.Model):
fruit = ForeignKeyAcrossDb(Fruit)
intro = models.TextField()
El problema es que, a veces, cuando accedo a Article.fruit, es un número entero, y algunas veces es el objeto Fruit. Quiero que siempre sea un objeto de fruta. ¿Qué debo hacer para que el acceso a Article.fruit siempre devuelva un objeto Fruit?
Como una solución para mi solución, agregué una propiedad fruit_obj
, pero me gustaría eliminar esto si es posible:
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
Método 2: campo de ForeignKey de subclase
Como segundo intento, intenté subclasificar el campo ForeignKey. ReverseSingleRelatedObjectDescriptor
para usar la base de datos especificada por forced_using
en el administrador de modelos de Fruit
. También eliminé el método validate()
en la subclase ForeignKey
. Este método no tuvo el mismo problema que el método 1. Código en github en: 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
Método 2a: Agregue un enrutador para fruit_app
Esta solución usa un enrutador adicional para fruit_app
. Esta solución no requiere las modificaciones a ForeignKey
que se requerían en el Método 2. Después de observar el comportamiento de enrutamiento predeterminado de Django en django.db.utils.ConnectionRouter
, encontramos que a pesar de que esperábamos que fruit_app
estuviera en la base de datos ''default''
por defecto , la sugerencia de instance
pasada a db_for_read
para búsquedas de claves externas la coloca en la base ''news_db''
datos ''news_db''
. fruit_app
un segundo enrutador para garantizar que los modelos de fruit_app
siempre se leen desde la base ''default''
datos ''default''
. Una subclase ForeignKey
solo se usa para "corregir" el método ForeignKey.validate()
. (Si Django quería admitir claves externas en las bases de datos, diría que esto es un error de Django.) El código está en github en: 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})
Información Adicional
- Subraye en la lista de usuarios de django que tiene mucha información: http://groups.google.com/group/django-users/browse_thread/thread/74bcd1afdeb2f0/0fdfce061124b915
- Historial de revisiones para la documentación multi-db: http://code.djangoproject.com/log/django/trunk/docs/topics/db/multi-db.txt?verbose=on
Actualizar
Implementamos el último método después de ajustar un poco más nuestros enrutadores. Toda la implementación ha sido bastante dolorosa, lo que nos hace pensar que debemos hacerlo mal. En la lista TODO está escribiendo pruebas de unidad para esto.
En cuanto a la parte ForeignKeyAcrossDb
, ¿no podrías posiblemente hacer algunos ajustes a tu clase dentro de __init__
? Compruebe si el campo apropiado es Integer
si no es así, cárguelo de la base de datos o haga cualquier otra cosa que sea necesaria. Python __class__
es se puede cambiar en el tiempo de ejecución sin mucho problema.
Esta solución se escribió originalmente para una base de datos administrada con migraciones y una o más bases de datos heredadas con modelos Meta managed=False
connected en el nivel de base de datos a la misma base de datos. Si una opción db_table
contiene un nombre de base de datos más un nombre de tabla citado correctamente por '''' '' (MySQL) o por '' '''' (otro db), por ejemplo db_table = ''"DB2"."table_b"''
, entonces ya no se cita por Django. Las consultas son compiladas por Django ORM correctamente, incluso con 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
Conjunto de consulta:
>>> qs = TableB.objects.all()
>>> str(qs.query)
''SELECT "DB2"."table_b"."id" FROM DB2"."table_b"''
Eso es compatible con todos los back-end db en Django.
(Parece que comencé una recompensa por una nueva pregunta duplicada donde mi respuesta continúa).
Inspirado por el comentario de @Frans. Mi solución es hacer esto en la capa de negocios. En el ejemplo dado a esta pregunta. Me gustaría dar fruto a un IntegerField
en Article
, como "no hacer la verificación de integridad en la capa de datos".
class Fruit(models.Model):
name = models.CharField()
class Article(models.Model):
fruit = models.IntegerField()
intro = models.TextField()
A continuación, respete la relación de referencia en el código de la aplicación (capa empresarial). Tomemos el administrador de Django, por ejemplo, para mostrar la fruta como una opción en la página de agregar artículos, llene manualmente una lista de opciones para frutas.
# 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'']
Por supuesto, es posible que deba ocuparse de la validación de campo de formulario (verificación de integridad).
Puede hacer una vista en la base de datos que tiene la consulta de base de datos cruzada, y luego definir el modelo para la vista en un archivo separado para mantener el funcionamiento de syncdb.
Feliz programacion :)
Sé que Djano-nosql tiene soporte para claves y algo de magia de http://www.allbuttonspressed.com/projects/django-dbindexer . Tal vez algo de eso podría ayudar.
De la descripción:
"Simplemente puede decirle al dbindexer qué modelos y campos deben admitir estas consultas y se ocupará de mantener los índices necesarios para usted".
-Kerry
Se encontró con un problema similar de necesidad de referencia (principalmente) de datos estáticos en múltiples (5) bases de datos. Se realizó una ligera actualización del descriptor ReverseedSingleRelatedObject para permitir la configuración del modelo relacionado. No implementa la relación inversa atm.
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)
y
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
Tengo una nueva solución para django v1.10. Hay dos partes Funciona con django.admin y django.rest-framework.
- Heredar la clase
ForeignKey
y crearForeignKeyAcrossDb
, y anular la funciónvalidate()
función de este ticket y esta post .
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
)
- En la declaración de campo, use
db_constraint=False
, por ejemplo,
album=ForeignKeyAcrossDb(Singer, db_constraint=False, on_delete=models.DO_NOTHING)
Un campo de clave foránea implica que puede: consultar sobre la relación uniendo, es decir, fruit__name, comprobar la integridad referencial, garantizar la integridad referencial tras las eliminaciones, la funcionalidad de búsqueda de id sin formato del administrador (algunos más ...)
El primer caso de uso siempre sería problemático. Probablemente haya otros casos especiales de clave externa en la base de código que tampoco funcionarían.
Tengo un sitio de django bastante grande y actualmente estamos usando un campo entero simple. Por ahora, creo que subclasificar el campo entero y agregar la id a la conversión de objeto sería más fácil (en 1.2 que requería parchar algunos trozos de django, espero que esto haya mejorado). Te permitirá saber qué solución encontramos.