如何重写此 postgres 约束以更符合 django 2.2?

How to rewrite this postgres constraint to be more in line with django 2.2?

这是我为检查约束为 postgres 编写的原始查询

   ALTER TABLE rea_asplinkage ADD CONSTRAINT asp_sub_project_positive_integer
        CHECK (
            jsonb_typeof(linkage-> 'root' -> 'in_sub_project') is not distinct from 'number'
        and (linkage->'root'->>'in_sub_project')::numeric % 1 = 0
        and (linkage->'root'->>'in_sub_project')::numeric > 0
        );

而我创建迁移的方式是这样的

# Generated by Django 2.2.10 on 2020-05-16 12:59

from django.db import connection, migrations



class Migration(migrations.Migration):

    dependencies = [("rea", "0029_asplinkage")]

    operations = [
        migrations.RunSQL(
            sql="""
            ALTER TABLE rea_asplinkage ADD CONSTRAINT asp_sub_project_positive_integer
            CHECK (
                jsonb_typeof(linkage-> 'root' -> 'in_sub_project') is not distinct from 'number'
            and (linkage->'root'->>'in_sub_project')::numeric % 1 = 0
            and (linkage->'root'->>'in_sub_project')::numeric > 0
            );
            """,
            reverse_sql="""
                ALTER TABLE rea_asplinkage DROP CONSTRAINT "asp_sub_project_positive_integer";
            """,
        )
    ]

这行得通。

但这意味着我的原始模型没有显示 ASPLinkage 模型class Meta中的约束

class ASPLinkage(TimeStampedModel, SoftDeletableModel, PersonStampedModel, OrganizationOwnedModel):
    linkage = JSONField(default=default_linkage_for_asp)

    objects = OrganizationOwnedSoftDeletableManager()

我已经尝试 ExpressionWrapperRawSQL 在 class Meta 中创建约束,但它仍然不起作用。

作为参考,我查看了 https://github.com/django/django/blob/master/tests/constraints/models.py#L12

中的示例

我还通过 https://realpython.com/create-django-index-without-downtime/#when-django-generates-a-new-migration

查看了单独的数据库和状态迁移

但我仍然无法让它工作

这可能吗?

更新

为了提高可读性,让我总结一下我的问题。

  1. 我想在 JSONField 上写约束。
  2. 我可以直接在 Postgres 上这样做
  3. 因此我可以在迁移文件中使用原始 sql
  4. 但是我不能使用 Django 模型元/CheckConstraint 做等效的事情,这通常是任何人做的。参见 https://docs.djangoproject.com/en/3.0/ref/models/constraints/
  5. 那么我如何重写这个原始 sql 以在 postgres 中但以 Django 方式对 jsonfield 产生约束?

为了在 Django 2.2 上实现 you'll need to register two new JSONField transforms/lookups since support for conditional expressions was only added in the upcoming 3.1 release

您首先要为 JSONField 键访问注册查找

from django.db import models
from django.db.models.lookups import Lookup
from django.contrib.postgres.fields.jsonb import (
    KeyTransform, KeyTransformTextLookupMixin
)

@KeyTransform.register_lookup
class KeyTransformIsInteger(KeyTransformTextLookupMixin, Lookup):
    lookup_name = 'is_int'
    prepare_rhs = False

    def as_sql(self, compiler, connection):
        key_expr = KeyTransform(
            self.lhs.key_name, *self.lhs.source_expressions, **self.lhs.extra
        )
        key_sql, key_params = self.process_lhs(
            compiler, connection, lhs=key_expr
        )
        lhs_sql, lhs_params = self.process_lhs(compiler, connection)
        rhs_sql, rhs_params = self.process_rhs(compiler, connection)
        sql = "(jsonb_typeof(%s) = %%s AND mod(%s::numeric, %%s) = %%s) IS %s" % (
            key_sql, lhs_sql, rhs_sql
        )
        params = [
            *key_params, 'number',
            *lhs_params, 1, 0,
            *rhs_params,
        ]
        return sql, params

@KeyTransform.register_lookup
class KeyTransformIntegerGt(KeyTransformTextLookupMixin, Lookup):
    lookup_name = 'int_gt'
    prepare_rhs = False

    def as_sql(self, compiler, connection):
        lhs_sql, lhs_params = self.process_lhs(compiler, connection)
        rhs_sql, rhs_params = self.process_rhs(compiler, connection)
        sql = "%s::int > %s" % (lhs_sql, rhs_sql)
        params = [*lhs_params, *rhs_params]
        return sql, params

一旦完成,您应该能够像

一样定义约束
CheckConstraint(
    check=Q(
        linkage__root__in_sub_project__is_int=True,
        linkage__root__in_sub_project__int_gt=0,
    ),
    name='asp_sub_project_positive_integer',
)

一旦你在 Django 3.1 上,你应该能够将 RawSQL 直接传递给 CheckConstraint.check 只要它有 output_field = models.BooleanField().

RawSQL("""
   jsonb_typeof(linkage-> 'root' -> 'in_sub_project') is not distinct from 'number'
   and (linkage->'root'->>'in_sub_project')::numeric % 1 = 0
   and (linkage->'root'->>'in_sub_project')::numeric > 0
""",
    output_field=models.BooleanField()
)