citusdata / django-multitenant

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

Suport for TenantForeignKey as recursive Model #74

Open josseed opened 4 years ago

josseed commented 4 years ago

Suport for models like this:

class Task(TenantModel): tenant_id = 'business_id' business = models.ForeignKey(Business, on_delete = models.CASCADE) sub_task = TenantForeignKey( 'self', on_delete=models.CASCADE, db_index=False, blank=True, null=True )

louiseGrandjonc commented 4 years ago

Hello @josseed,

Thank you for using django-multitenant. I added tests on TenantForeignKey on self in this branch: https://github.com/citusdata/django-multitenant/tree/self-tenant-foreign-key.

As you can see, in the model Task, I added a parent field similar to yours. All the tests are passing. Could you give me details on your issue?

Thank you

johnnywell commented 4 years ago

I'm having some hard time with TenantForeignKey('self') too.

Here are examples of my models and migration.

# models
class AbstractBaseModel(models.Model):
    """
    The Abstract Base Model holds the common attributes and methods
    used for the project.
    """
    id = models.UUIDField(primary_key=True, unique=True, default=uuid.uuid4, editable=False)

    class Meta:
        abstract = True

class TenantAbstractModel(AbstractBaseModel, TenantModel):
    tenant_id = 'org_id'
    org = models.ForeignKey(Organization, on_delete=models.PROTECT)

    class Meta:
        abstract = True

class OrgUnity(TenantAbstractModel):
    """
    OrgUnity is a organizational unity, like branches, departments, tribes, etc.
    Basically a grouping of resources, it can be related to other OrgUnity.
    """
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    parent_unity = TenantForeignKey('self', on_delete=models.PROTECT, db_index=False, blank=True, null=True)
# migration

...

migrations.CreateModel(
    name='OrgUnity',
    fields=[
        ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
        ('name', models.CharField(max_length=255)),
        ('description', models.TextField(blank=True)),
        ('org', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='organization.Organization')),
        ('parent_unity', django_multitenant.fields.TenantForeignKey(blank=True, db_index=False, null=True, on_delete=django.db.models.deletion.PROTECT, to='organization.OrgUnity')),
    ],
    options={
        'abstract': False,
    },
    bases=(django_multitenant.mixins.TenantModelMixin, models.Model),
),

...

When applying the migration I'm getting the following exception.

Operations to perform:
  Apply all migrations: admin, archive, auth, contenttypes, organization, sessions
Running migrations:
  Applying organization.0001_initial...Traceback (most recent call last):
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/db/backends/utils.py", line 86, in _execute
    return self.cursor.execute(sql, params)
psycopg2.errors.InvalidForeignKey: there is no unique constraint matching given keys for referenced table "organization_orgunity"

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "manage.py", line 21, in <module>
    main()
  File "manage.py", line 17, in main
    execute_from_command_line(sys.argv)
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/core/management/__init__.py", line 401, in execute_from_command_line
    utility.execute()
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/core/management/__init__.py", line 395, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/core/management/base.py", line 328, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/core/management/base.py", line 369, in execute
    output = self.handle(*args, **options)
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/core/management/base.py", line 83, in wrapped
    res = handle_func(*args, **kwargs)
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/core/management/commands/migrate.py", line 231, in handle
    post_migrate_state = executor.migrate(
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/db/migrations/executor.py", line 117, in migrate
    state = self._migrate_all_forwards(state, plan, full_plan, fake=fake, fake_initial=fake_initial)
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/db/migrations/executor.py", line 147, in _migrate_all_forwards
    state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial)
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/db/migrations/executor.py", line 247, in apply_migration
    migration_recorded = True
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/db/backends/base/schema.py", line 115, in __exit__
    self.execute(sql)
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django_multitenant/backends/postgresql/base.py", line 86, in execute
    super(DatabaseSchemaEditor, self).execute(statement)
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/db/backends/base/schema.py", line 142, in execute
    cursor.execute(sql, params)
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/db/backends/utils.py", line 100, in execute
    return super().execute(sql, params)
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/db/backends/utils.py", line 68, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/db/backends/utils.py", line 77, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/db/backends/utils.py", line 86, in _execute
    return self.cursor.execute(sql, params)
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/db/utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/Users/johnny/Library/Caches/pypoetry/virtualenvs/ihuman-platform-UyS-abtH-py3.8/lib/python3.8/site-packages/django/db/backends/utils.py", line 86, in _execute
    return self.cursor.execute(sql, params)
django.db.utils.ProgrammingError: there is no unique constraint matching given keys for referenced table "organization_orgunity"
johnnywell commented 4 years ago

After some digging, I think I found a way around.

In order for the self foreign key to work the table must exist first. So, I separated it in two migrations, the first one to create the table without te self reference and the second to include it.

johnnywell commented 4 years ago

As I advance in the current project using django-multitenant it seems to be a problem with migrations resolution order. Every now and then I stumble on the same problem with different models and different use cases, the only solution is to delete all the migrations and recreate them from scratch including the new falling model.