Django - Verwenden Sie eine Eigenschaft als Fremdschlüssel

8

Die Datenbank meiner App wird gefüllt und mit externen Datenquellen synchronisiert. Ich habe ein abstraktes Modell, von dem alle Modelle meiner Django 2.2-App abgeleitet sind und wie folgt definiert sind:

class CommonModel(models.Model):
    # Auto-generated by Django, but included in this example for clarity.
  # id = models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')
    ORIGIN_SOURCEA = '1'
    ORIGIN_SOURCEB = '2'
    ORIGIN_CHOICES = [
        (ORIGIN_SOURCEA, 'Source A'),
        (ORIGIN_SOURCEB, 'Source B'),
    ]
    object_origin = models.IntegerField(choices=ORIGIN_CHOICES)
    object_id = models.IntegerField()

class A(CommonModel):
    some_stuff = models.CharField()

class B(CommonModel):
    other_stuff = models.IntegerField()
    to_a_fk = models.ForeignKey("myapp.A", on_delete=models.CASCADE)

class C(CommonModel):
    more_stuff = models.CharField()
    b_m2m = models.ManyToManyField("myapp.B")

Das object_idFeld kann nicht als eindeutig festgelegt werden, da jede Datenquelle, die ich in meiner App verwende, möglicherweise ein Objekt mit einem enthält object_id = 1. Daher ist es notwendig, den Ursprung des Objekts anhand des Feldes aufzuspüren object_origin.

Leider unterstützt Djangos ORM keine Fremdschlüssel mit mehr als einer Spalte.

Problem

Während halten die automatisch generierten Primärschlüssel in der Datenbank ( id), würde Ich mag an meinem Fremdschlüssel und viele-zu-viele Beziehungen passieren auf beiden machen object_idund object_originFelder anstelle des Primärschlüssels id.

Was ich versucht habe

Ich dachte darüber nach, so etwas zu tun:

class CommonModel(models.Model):
    # Auto-generated by Django, but included in this example for clarity.
  # id = models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')
    ORIGIN_SOURCEA = '1'
    ORIGIN_SOURCEB = '2'
    ORIGIN_CHOICES = [
        (ORIGIN_SOURCEA, 'Source A'),
        (ORIGIN_SOURCEB, 'Source B'),
    ]
    object_origin = models.IntegerField(choices=ORIGIN_CHOICES)
    object_id = models.IntegerField()

    def _get_composed_object_origin_id(self):
        return f"{self.object_origin}:{self.object_id}"
    composed_object_origin_id = property(_get_composed_object_origin_id)

class A(CommonModel):
    some_stuff = models.CharField()

class B(CommonModel):
    other_stuff = models.IntegerField()
    to_a_fk = models.ForeignKey("myapp.A", to_field="composed_object_origin_id", on_delete=models.CASCADE)

Aber Django beschwert sich darüber:

myapp.B.to_a_fk: (fields.E312) The to_field 'composed_object_origin_id' doesn't exist on the related model 'myapp.A'.

Und es klingt echt, Django hat das to_fieldals Datenbankfeld angegebene Feld ausgenommen . Es ist jedoch nicht erforderlich, ein neues Feld zu meinem hinzuzufügen, CommonModelda composed_object_type_ides aus zwei nicht nullbaren Feldern besteht ...

Spacebrain
quelle
2
Interessante Idee, aber das sieht aus meiner Sicht wie ein xy-Problem aus ... Warum brauchst du das?
Setzen Sie Monica

Antworten:

6

Sie haben in Ihrem Kommentar in der anderen Antwort erwähnt, dass object_id nicht eindeutig ist, aber in Kombination mit object_type eindeutig. Könnten Sie also a unique_togetherin der Metaklasse verwenden? dh

class CommonModel(models.Model):
    object_type = models.IntegerField()
    object_id = models.IntegerField()

    class Meta:
        unique_together = (
            ("object_type", "object_id"),
        )
Kathy Rindhoops
quelle
1

Haben / Können Sie das uniqueAttribut auf dem object_idFeld setzen?

class CommonModel(models.Model):
    object_type = models.IntegerField()
    object_id = models.IntegerField(unique=True)

Wenn dies nicht funktioniert, würde ich den Feldtyp in ein uuidFeld ändern :

class CommonModel(models.Model):
    object_type = models.IntegerField()
    object_uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
Victor Hug
quelle
object_idKann leider nicht als eindeutig festgelegt werden, da es Fälle gibt, in denen es nicht eindeutig ist. In der externen Datenquelle, die mir die Daten bereitstellt, die ich in meiner App verwende, besteht der Primärschlüssel aus zwei Feldern: object_typeund object_id.
Spacebrain
Wenn dies object_idnicht eindeutig ist, sollten Sie keinen Fremdschlüssel erstellen. Dies könnte zu Fehlern in der Datenbank führen, und das möchten Sie nicht. Wenn Sie stattdessen nicht das pk verwenden möchten, können Sie die Beziehung auch in den integrierten models.ModelFunktionen selbst verwalten.
Victor Hug
Nun, object_typeund object_idzusammen sind garantiert einzigartig. Aber object_idallein ist es nicht.
Spacebrain
Hier finden Sie ein Pip-Paket zum Erstellen einer Fremdschlüsseleinschränkung für eine Verbindung (zwei Schlüssel): django-composite-foreignkey.readthedocs.io/en/latest/…
Victor Hug
Leider werden Django 2.1+ Apps nicht unterstützt und es scheint, dass diese Bibliothek nicht aktiv gepflegt wird.
Spacebrain
1

Sie werden in Ihrer Frage als " Leider unterstützt Djangos ORM nicht mehr als eine Spalte Fremdschlüssel " erwähnt.

Ja, Django bietet diese Art von Unterstützung nicht an, da Django zuverlässiger ist als wir denken :)

Django bietet also eine Meta-Option, um diese Art von Problem zu lösen, und diese Option ist unique_together.

Sie können Sätze von Feldnamen angeben, die zusammengenommen in Ihrem Fall eindeutig sein müssen ...

class CommonModel(models.Model):
    # Auto-generated by Django, but included in this example for clarity.
    # id = models.AutoField(auto_created=True, primary_key=True, 
    serialize=False, verbose_name='ID')
    ORIGIN_SOURCEA = '1'
    ORIGIN_SOURCEB = '2'
    ORIGIN_CHOICES = [
        (ORIGIN_SOURCEA, 'Source A'),
        (ORIGIN_SOURCEB, 'Source B'),
    ]
    object_origin = models.IntegerField(choices=ORIGIN_CHOICES)
    object_id = models.IntegerField()

    class meta:
        unique_together = [['object_origin', 'object_id']]

Sie können eine Liste der Liste, Sätze von Sätzen oder einfache Listen, einfache Sätze zu unique_togetherOptionen von bereitstellen class meta:.

Ja, aber Django hat das gesagt ...

UniqueConstraint bietet mehr Funktionen als unique_together.

unique_together ist möglicherweise in Zukunft veraltet.

Sie können hinzufügen , UniqueConstraintstatt unique_togetherin gleichen class meta:in Ihrem Fall , dass Sie , wie unten schreiben ...

class CommonModel(models.Model):
    # Auto-generated by Django, but included in this example for clarity.
    # id = models.AutoField(auto_created=True, primary_key=True, 
    serialize=False, verbose_name='ID')
    ORIGIN_SOURCEA = '1'
    ORIGIN_SOURCEB = '2'
    ORIGIN_CHOICES = [
        (ORIGIN_SOURCEA, 'Source A'),
        (ORIGIN_SOURCEB, 'Source B'),
    ]
    object_origin = models.IntegerField(choices=ORIGIN_CHOICES)
    object_id = models.IntegerField()

    class meta:
        constraints = [ models.UniqueConstraint(fields=['object_origin', 'object_id'], name='unique_object')]

Die beste Vorgehensweise ist also, die constraintsOption anstelle von unique_togetherzu verwenden class meta:.

MK Patel
quelle
1

Sie können die Ursprungs-ID des zusammengesetzten Objekts zu einem Feld ( composed_object_origin_id) machen, das aktualisiert saveund als verwendet wird to_field.

class CommonModel(models.Model):
    ORIGIN_SOURCEA = "1"
    ORIGIN_SOURCEB = "2"
    ORIGIN_CHOICES = [
        (ORIGIN_SOURCEA, "Source A"),
        (ORIGIN_SOURCEB, "Source B"),
    ]
    object_origin = models.IntegerField(choices=ORIGIN_CHOICES)
    object_id = models.IntegerField()
    composed_object_origin_id = models.CharField(max_length=100, unique=True)

    def save(self, **kwargs):
        self.composed_object_origin_id = f"{self.object_origin}:{self.object_id}"

        # Just in case you use `update_fields`, force inclusion of the composed object origin ID.
        # NOTE: There's definitely a less error-prone way to write this `if` statement but you get
        # the gist. e.g., this does not handle passing `update_fields=None`.
        if "update_fields" in kwargs:
            kwargs["update_fields"].append("composed_object_origin_id")

        super().save(**kwargs)


class A(CommonModel):
    some_stuff = models.CharField(max_length=1)


class B(CommonModel):
    other_stuff = models.IntegerField()
    to_a_fk = models.ForeignKey(
        "myapp.A", to_field="composed_object_origin_id", on_delete=models.CASCADE
    )
Cole
quelle