How to work around lack of support for foreign keys across databases in Django

后端 未结 9 1360
生来不讨喜
生来不讨喜 2020-12-07 09:59

I know Django does not support foreign keys across multiple databases (originally Django 1.3 docs)

But I\'m looking for a workaround.

What doesn\'t work

相关标签:
9条回答
  • 2020-12-07 10:16

    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.

    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)
    

    and

    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
    
    0 讨论(0)
  • 2020-12-07 10:17

    As to the ForeignKeyAcrossDb part, couldn't you possibly make some adjustments to your class inside __init__? Check if the appropriate field is Integer 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.

    0 讨论(0)
  • 2020-12-07 10:20

    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 on Article, as "not to do integrity check in data layer".

    class Fruit(models.Model):
        name = models.CharField()
    
    class Article(models.Model):
        fruit = models.IntegerField()
        intro = models.TextField()
    

    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.

    # 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']
    

    Of course you may need to take care of form field validation (integrity check).

    0 讨论(0)
提交回复
热议问题