63 votes

Comment contourner l'absence de prise en charge des clés étrangères entre les bases de données dans Django ?

Je sais que Django ne prend pas en charge les clés étrangères dans plusieurs bases de données : http://docs.djangoproject.com/en/1.3/topics/db/multi-db/#cross-database-relations

Mais je cherche une solution de rechange.

Ce qui ne marche pas

J'ai deux modèles, chacun sur une base de données distincte.

routeurs.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

Modèle 1 dans fruit_app/models.py :

from django.db import models

class Fruit(models.Model):
    name = models.CharField(max_length=20)

Modèle 2 dans news_app/models.py :

from django.db import models

class Article(models.Model):
    fruit = models.ForeignKey('fruit_app.Fruit')
    intro = models.TextField()

Si vous essayez d'ajouter un "article" dans l'administration, vous obtenez l'erreur suivante, car le système recherche l'adresse suivante Fruit sur la mauvaise base de données ( 'news_db' ):

DatabaseError at /admin/news_app/article/add/

(1146, "Table 'fkad_news.fruit_app_fruit' doesn't exist")

Méthode 1 : sous-classe IntegerField

J'ai créé un champ personnalisé, ForeignKeyAcrossDb, qui est une sous-classe de IntegerField. Le code est sur 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)

Et j'ai changé mon modèle d'article pour être :

class Article(models.Model):
    fruit = ForeignKeyAcrossDb(Fruit)
    intro = models.TextField()

Le problème est que, parfois, lorsque j'accède à Article.fruit, il s'agit d'un nombre entier, et parfois de l'objet Fruit. Je veux que ce soit toujours un objet Fruit. Que dois-je faire pour que l'accès à Article.fruit renvoie toujours un objet Fruit ?

Pour pallier à mon problème, j'ai ajouté une fonction fruit_obj mais je voudrais l'éliminer si possible :

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éthode 2 : sous-classez le champ ForeignKey

Dans un deuxième temps, j'ai essayé de sous-classer le champ ForeignKey. J'ai modifié ReverseSingleRelatedObjectDescriptor pour utiliser la base de données spécifiée par forced_using sur le gestionnaire de modèles de Fruit . J'ai également retiré le validate() sur la méthode ForeignKey sous-classe. Cette méthode n'a pas eu le même problème que la méthode 1. Code sur 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

Méthode 2a : Ajouter un routeur pour fruit_app

Cette solution utilise un routeur supplémentaire pour fruit_app . Cette solution ne nécessite pas de modifier ForeignKey qui étaient nécessaires dans la méthode 2. Après avoir examiné le comportement de routage par défaut de Django dans la section django.db.utils.ConnectionRouter nous avons constaté que, même si nous nous attendions fruit_app pour être sur le 'default' par défaut, la base de données instance transmis à db_for_read pour les recherches de clés étrangères, mettez-le sur le 'news_db' base de données. Nous avons ajouté un second routeur pour assurer fruit_app ont toujours été lus à partir de l 'default' base de données. A ForeignKey n'est utilisée que pour "réparer" la classe ForeignKey.validate() méthode. (Si Django voulait prendre en charge les clés étrangères à travers les bases de données, je dirais que c'est un bug de Django). Le code est sur github à : https://github.com/saltycrane/django-foreign-key-across-db-testproject

routeurs.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})

Informations complémentaires

Mise à jour

Nous avons appliqué la dernière méthode après avoir modifié nos routeurs. L'ensemble de l'implémentation a été assez pénible, ce qui nous fait penser que nous devons nous tromper. Sur la liste des choses à faire, il faut écrire des tests unitaires pour cette méthode.

2voto

Kerry Hatcher Points 69

Je sais que Djano-nosql prend en charge les clés et d'autres éléments par le biais d'une magie provenant de http://www.allbuttonspressed.com/projects/django-dbindexer . Peut-être qu'un peu de ça pourrait aider.

D'après la description :

"Vous pouvez simplement indiquer au dbindexer quels modèles et champs doivent supporter ces requêtes et il se chargera de maintenir les index requis pour vous."

-Kerry

2voto

julkiewicz Points 3444

Quant à la ForeignKeyAcrossDb ne pourriez-vous pas faire quelques ajustements à votre classe à l'intérieur __init__ ? Vérifiez si le champ approprié est Integer sinon, le charger depuis la base de données, ou faire toute autre chose nécessaire. Python __class__ es peuvent être modifiées en cours d'exécution sans grand problème.

2voto

arannasousa Points 49

Après m'être cassé la tête quelques jours, j'ai réussi à obtenir ma clé étrangère sur la même banque !

On peut faire un changement sur le FORMULAIRE pour chercher une CLÉ ÉTRANGÈRE dans une autre banque !

Tout d'abord, ajouter une RECHARGE de FIELDS, à la fois directement (crack) ma forme, dans la fonction _ 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()

appeler le formulaire à partir de la vue

app.view.py

form = forms.FormNewHelpDesk(request.POST or None, using=banco)

Maintenant, le changement dans le code source DJANGO

Seuls les champs de type ForeignKey, ManyToManyField et OneToOneField peuvent utiliser l'option 'using', nous avons donc ajouté 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)

MODIFIER LE FICHIER DE SUIVI

django.db.models.base.py

modifier

# line 717
qs = model_class._default_manager.filter(**lookup_kwargs)

pour

# line 717
qs = model_class._default_manager.using(getattr(self, 'db', None)).filter(**lookup_kwargs)

Prêt :D

1voto

adorablepuppy Points 827

Vous pouvez créer une vue dans la base de données qui contient la requête inter-bases de données, puis définir le modèle de la vue dans un fichier séparé pour que syncdb continue à fonctionner.

Bonne programmation :)

1voto

Thierry Points 907

Un champ de clé étrangère implique que vous pouvez - faire une requête sur la relation en joignant ie fruit__name - vérifier l'intégrité référentielle - assurer l'intégrité référentielle lors des suppressions - fonctionnalité de recherche de l'identifiant brut de l'administrateur - (un peu plus...)

Le premier cas d'utilisation serait toujours problématique. Il existe probablement d'autres cas particuliers de clés étrangères dans le codebase qui ne fonctionneraient pas non plus.

Je gère un site django assez important et nous utilisons actuellement un simple integerfield. Pour l'instant, je penserais que sous-classer l'integerfield et ajouter la conversion de l'id en objet serait le plus simple (dans la 1.2, cela nécessitait Parcheando quelques bits de django, j'espère que cela s'est amélioré depuis). Je vous ferai savoir quelle solution nous trouverons.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X