Django GeneratedField как ForeignKey со ссылочной целостностью

Я пытаюсь создать генерируемое поле, которое также является внешним ключом к другой таблице, сохраняя при этом ссылочную целостность на уровне базы данных.

По сути, я пытаюсь добиться того же эффекта, что и следующий SQL, но в Django

CREATE TABLE parent(
  id TEXT PRIMARY KEY
);

CREATE TABLE child(
  id TEXT PRIMARY KEY,
  data JSONB,
  parent_id TEXT GENERATED ALWAYS AS (data->>'parent') STORED REFERENCES parent(id)
);

Мне успешно удалось создать генерируемое поле с помощью Django 5.0 GeneratedField

class Parent(models.Model):
    id = models.TextField(primary_key=True)

class Child(models.Model):
    id = models.TextField(primary_key=True)
    data = models.JSONField()
    parnet_id = models.GeneratedField(expression=models.fields.json.KT('data__parent'), output_field=models.TextField(), db_persist=True)

Теперь проблема: как сделать это поле также внешним ключом? Потому что использование ForeignKey создаст новый столбец в базе данных, который не генерируется.

Я попытался использовать ForeignObject, поскольку он поддерживает использование существующего поля в качестве внешнего ключа, но ограничение внешнего ключа не было создано на уровне базы данных.

class Parent(models.Model):
    id = models.TextField(primary_key=True)

class Child(models.Model):
    id = models.TextField(primary_key=True)
    data = models.JSONField()
    parnet_id = models.GeneratedField(expression=models.fields.json.KT('data__parent'), output_field=models.TextField(), db_persist=True)
    parent = models.ForeignObject(Parent, from_fields=['parnet_id'], to_fields=['id'], on_delete=models.CASCADE)

Это порождает следующий SQL, в котором отсутствует ограничение внешнего ключа

CREATE TABLE "myapp_parent" ("id" text NOT NULL PRIMARY KEY);
CREATE TABLE "myapp_child" ("id" text PRIMARY KEY, "data" jsonb NOT NULL, "parent_id" text GENERATED ALWAYS AS (("data" ->> 'parent_id')) STORED);

Примечания:

  • Я использую Postgres.
  • Я не хочу зависеть от сигналов pre_save и post_save, потому что вставлять буду с помощью bulk_create, который их не поддерживает.
  • Я хочу сохранить сгенерированный столбец, чтобы сохранить целостность. Пожалуйста, не предлагайте не использовать генерируемые столбцы.
  • Я предпочитаю не писать пользовательский SQL в миграциях вручную, если это не является абсолютно необходимым.

Заранее спасибо <3

Просто создайте пользовательский класс ограничений GeneratedForeignKey, который расширяет класс Django BaseConstraint. Этот класс будет генерировать SQL для создания ограничения внешнего ключа для нашего генерируемого поля. Также добавьте constraints список к Child классу Meta модели, который включает в себя наш пользовательский GeneratedForeignKey класс ограничений.

from django.db import models
from django.db.models import F
from django.db.models.constraints import BaseConstraint

class GeneratedForeignKey(BaseConstraint):
    def __init__(self, field, to, to_field):
        self.field = field
        self.to = to
        self.to_field = to_field
        super().__init__(name=f'fk_{field}_{to._meta.db_table}')

    def constraint_sql(self, model, schema_editor):
        remote_table = self.to._meta.db_table
        remote_col = self.to_field
        local_col = self.field
        return f'FOREIGN KEY ({local_col}) REFERENCES {remote_table}({remote_col})'

    def create_sql(self, model, schema_editor):
        return f'ALTER TABLE {model._meta.db_table} ADD CONSTRAINT {self.name} {self.constraint_sql(model, schema_editor)}'

    def remove_sql(self, model, schema_editor):
        return f'ALTER TABLE {model._meta.db_table} DROP CONSTRAINT {self.name}'

    def deconstruct(self):
        path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
        return (path, (), {'field': self.field, 'to': self.to, 'to_field': self.to_field})

class Parent(models.Model):
    id = models.TextField(primary_key=True)

class Child(models.Model):
    id = models.TextField(primary_key=True)
    data = models.JSONField()
    parent_id = models.GeneratedField(
        expression=models.functions.Cast(F('data__parent'), output_field=models.TextField()),
        output_field=models.TextField(),
        db_persist=True
    )

    class Meta:
        constraints = [
            GeneratedForeignKey(field='parent_id', to=Parent, to_field='id')
        ]
Вернуться на верх