Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django proxy model to different database

Situation


We have a few different applications which use tickets from a ticket support system for different kinds of functionality.

First of all we have an application which has a few models that represent the models of our ticket support system Kayako. This application should not know anything about other applications that make use of it and should remain as generic as possible. Since this application is using existing tables of Kayako we have it running on the same database. Let's call this application kayakodb.

One application links customers from our customer database to tickets in the ticket support system. Previously this system had it's own representation of the tickets inside our ticket support system, querying for tickets by using an API provided by kayakodb. It then used this representation of tickets to link customers and domains to. This however was too complex and not very logical. So we opted to switch it to a proxy model and move the models that represent the links to customers and domains to kayakodb. Let's call this application sidebar.

Another, new application, shows the tickets from the ticket support system in a clear overview alongside calls so our support department can easily see which calls and tickets are related to which customers. This system has a proxy model to the proxy model of sidebar, because some of the functionality provided by the sidebar model are also required for this application alongside some others which the new proxy model declare. Let's call this project WOW.

The sidebar and WOW applications are both part of the same project/repository. We'll call this repository Coneybeach which has its own database. However, kayakodb is a completely unrelated project. It is included in Coneybeach via a requirements file which we install via pip.

Problem


When creating migrations for the new setup Django creates a proxy model migration for the installed kayakodb which is, of course, a no go. Any time we would install a new version of kayakodb it would overwrite this migration. Let alone the fact that kayakodb shouldn't know anything about which models make use of it.

Code


The Ticket model inside kayakodb:
class Ticket(models.Model):
    """
    This model is a representation of the data stored in the "kayako" database table "swtickets". Minus a lot of stuff
    we don't use. If you add a field make sure it has the same name as the field in kayako.swtickets.
    """
    # Fields, functions and manager etc.

    class Meta:
        db_table = 'swtickets'
        managed = False

The SidebarTicket proxy model inside sidebar:

from kayakodb.models import Ticket    

class SidebarTicket(Ticket):
    class Meta:
        # Since this class is a wrapper we don't want to create a table for it. We only want to access the original
        # model as we always do, but provide a different interface (when it comes to functions). Proxy models allow us
        # to do this: https://docs.djangoproject.com/en/1.10/topics/db/models/#proxy-models
        proxy = True

        # Don't look for this model in the sidebar tables, but in the kayakodb tables.
        app_label = 'kayakodb'

    # Some extra functions

The Contact class TicketWrapper inherits from (as requested by Hynekcer). This model is used as base model for TicketWrapper and another model representing calls (though there are not issues with this model as far as I'm aware):

class Contact(models.Model):
    type = None

    class Meta:
        abstract = True

    def __getattr__(self, attr):
        if attr in ['customers', 'add_customer_id', 'remove_all_customers', 'byters', 'domainnames', 'add_domain_name',
                    'remove_domain_name', 'add_text', 'remove_text', 'texts', 'creation_date', 'add_tag', 'get_tags',
                    'remove_tag', 'identifier']:
            raise NotImplementedError('You should implement {}'.format(attr))
        raise AttributeError(attr)

The TicketWrapper proxy model inside WOW:

from sidebar.models import SidebarTicket

class TicketWrapper(Contact, SidebarTicket):
    class Meta:
        # Since this class is a wrapper we don't want to create a table for it. We only want to access the original
        # model as we always do, but provide a different interface (when it comes to functions). Proxy models allow us
        # to do this: https://docs.djangoproject.com/en/1.10/topics/db/models/#proxy-models
        proxy = True

        # Don't look for this model in the WOW database, but in the kayakodb database.
        app_label = 'kayakodb'

    # Some extra functions

What have I tried


  • I've tried not specifying the app_label for both the proxy models. This creates correct migrations, but causes the proxy models to look for the kayakodb.Ticket model in the Coneybeach database.
  • I've tried specifying abstract = True for the subclasses, but wasn't sure this was the because I still want to be able to make use of the manager for the models.
  • I considered moving the migration that gets created currently to the actual kayakodb project, but I don't think this is a good solution. kayakodb shouldn't know anything about the implementations of its models or where they are used.
  • ./manage.py check returns 0 issues.

Question


How can I create a proxy model for a model that's located in a different database or project?

Edit


After setting the kayakodb.Ticket model to be unmanaged the WOW project tries to create a migration for all models in kayakodb. Result:
Migrations for 'sidebar':
  0004_auto_20170116_1210.py:
    - Delete model Ticket

Migrations for 'kayakodb':
  0001_initial.py:
    - Create model Staff
    - Create model Tag
    - Create model Ticket
    - Create model TicketPost
    - Create model TicketTag
    - Create model TicketCustomer
    - Create model TicketDomain
    - Create proxy model SidebarTicket
    - Alter unique_together for ticketdomain (1 constraint(s))
    - Alter unique_together for ticketcustomer (1 constraint(s))
    - Create proxy model TicketWrapper
like image 422
Bono Avatar asked Jan 11 '17 11:01

Bono


People also ask

Can Django project connect to multiple databases?

Django's admin doesn't have any explicit support for multiple databases. If you want to provide an admin interface for a model on a database other than that specified by your router chain, you'll need to write custom ModelAdmin classes that will direct the admin to use a specific database for content.

What is the purpose of proxy models in Django?

Proxy models allow us to change the Python behavior of a model without changing the database. Proxy models are declared just like normal models. In our example, we tell Django that Honda is a proxy model by setting the proxy attribute of the Honda Meta class to True .


2 Answers

As @hynekcer said if kayakodb is an existing database, you need to set managed = False for all its models. However, that still leaves the problem of the migration for the proxy model created inside the wrong app (kayakodb).

The hacky fix that might work is changing the app_label of the proxy model to whatever app is okay to put the migration to (sidebar in this case), and making a router that will point this proxy model to read and write from kayakodb.

E.g. the proxy model:

# in sidebar/models.py

class SidebarTicket(KayakoTicket):
    class Meta:
        proxy = True
        app_label = 'sidebar'

and the router inside the project that uses it:

from django.conf import settings
from kayakodb.models import Ticket

class ProxyDatabaseRouter(object):
    def allow_proxy_to_different_db(self, obj_):
        # check if this is a sidebar proxy to the Ticket model in kayakodb
        return isinstance(obj_, Ticket) and obj_._meta.proxy and obj_._meta.app_label == 'sidebar'

    def db_for_read(self, model, **hints):
        if issubclass(model, Ticket) and model._meta.proxy and model._meta.app_label == 'sidebar':
            return 'kayakodb'
        # the rest of the method goes here

    def db_for_write(self, model, **hints):
        if issubclass(model, Ticket) and model._meta.proxy and model._meta.app_label == 'sidebar':
            return 'kayakodb'
        return None
        # the rest of the method goes here

    def allow_relation(self, obj1, obj2, **hints):
        if self.allow_proxy_to_different_db(obj1) or self.allow_proxy_to_different_db(obj2):
            return True
        # the rest of the method goes here
like image 120
railla Avatar answered Oct 24 '22 14:10

railla


tl;dr but I grepped your question for a word router which is not mentioned, so I'd think the thing you are looking for are Database Routers

like image 20
yedpodtrzitko Avatar answered Oct 24 '22 14:10

yedpodtrzitko