citusdata / django-multitenant

Python/Django support for distributed multi-tenant databases like Postgres+Citus
MIT License
721 stars 119 forks source link

Adding TenantForeignKey #14

Closed gsgalloway closed 6 years ago

gsgalloway commented 6 years ago

Without TenantForeignKey...

from django.db import models
from django_multitenant import TenantModel

class Corporation(models.Model):
    class Meta:
        db_table = 'corporation'
    pass

class Store(TenantModel):
    class Meta:
        db_table = 'store'
    corporation = models.ForeignKey(Corporation)
    isStoreOpen = models.BooleanField()
    tenant_id = 'corporation_id'

class Product(TenantModel):
    class Meta:
        db_table = 'product'
    corporate_owner = models.ForeignKey(Corporation)
    store = models.ForeignKey(Store)
    tenant_id = 'corporate_owner_id'

print(Product.objects.filter(store__isStoreOpen=False).values('id').query)

...results in the query:

SELECT "product"."id"
FROM "product"
INNER JOIN "store" ON ("product"."store_id" = "store"."id")
WHERE "store"."isStoreOpen" = FALSE

ERROR:  cannot use real time executor with repartition jobs

With TenantForeignKey...

from django.db import models
from django_multitenant import TenantModel, TenantForeignKey

class Corporation(models.Model):
    class Meta:
        db_table = 'corporation'
    pass

class Store(TenantModel):
    class Meta:
        db_table = 'store'
    corporation = models.ForeignKey(Corporation)
    isStoreOpen = models.BooleanField()
    tenant_id = 'corporation_id'

class Product(TenantModel):
    class Meta:
        db_table = 'product'
    corporate_owner = models.ForeignKey(Corporation)
    store = TenantForeignKey(Store)  # <---- Change here
    tenant_id = 'corporate_owner_id'

print(Product.objects.filter(store__isStoreOpen=False).values('id').query)

...results in the query:

SELECT "product"."id"
FROM "product"
INNER JOIN "store" ON ("product"."store_id" = "store"."id" AND
                       ("product"."corporate_owner_id" = ("store"."corporation_id")))
WHERE "store"."isStoreOpen" = FALSE

Reference: https://github.com/django/django/blob/2ced2f785d5aca0354abf5841d5449b7a49509dc/django/contrib/contenttypes/fields.py#L437

gsgalloway commented 6 years ago

Now that I play with it more, I suspect that with the introduction of 1534870 you can actually delete the add_tenant_filters_with_joins method entirely, since the tenant filter is present in the ON clause.

I've had to remove the add_tenant_filters_with_joins method in my local copy of this repo, due to the following query it generated:

SELECT COUNT(*) AS "__count"
FROM "children" "child"
LEFT OUTER JOIN "parents" T2 ON ("child"."parent_id" = T2."id"
                                AND ("child"."tenant_id" = (T2."tenant_id")))
WHERE (NOT (("child"."visibility" = 'Deleted'
             OR (T2."visibility" = 'Deleted'
                 AND T2."visibility" IS NOT NULL)))
       AND "child"."id" IN (1)
       AND ("child"."tenant_id" = 2)
       AND (T2."tenant_id" = 2));    -- this line is a problem

This query above returns a count of zero when there's a child with ID 1 that has a a null parent_id because it is filtering out results where the parent's tenant_id is not 2 (even if there is no parent).

Removing the add_tenant_filters_with_joins method results in the query:

SELECT COUNT(*) AS "__count"
FROM "children" "child"
LEFT OUTER JOIN "parents" T2 ON ("child"."parent_id" = T2."id"
                                AND ("child"."tenant_id" = (T2."tenant_id")))
WHERE (NOT (("child"."visibility" = 'Deleted'
             OR (T2."visibility" = 'Deleted'
                 AND T2."visibility" IS NOT NULL)))
       AND "child"."id" IN (1)
       AND ("child"."tenant_id" = 2)

That query works whether the child has a parent_id or not. And it passes when citus.multi_task_query_log_level is set to error