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