Прокси-модель Django для разных баз данных

Ситуация


У нас есть несколько различных приложений, которые используют билеты из системы поддержки билетов для различных видов функциональности.

Прежде всего, у нас есть приложение, в котором есть несколько моделей, которые представляют модели нашей системы поддержки билетов Kayako. Это приложение не должно знать ничего о других приложениях, которые его используют, и должны оставаться как можно более универсальными. Поскольку это приложение использует существующие таблицы Kayako, мы запускаем его в одной базе данных. Позвоните в это приложение kayakodb.

Одно приложение связывает клиентов из нашей клиентской базы с билетами в системе поддержки билетов. Ранее эта система имела собственное представление билетов внутри нашей системы поддержки билетов, запрашивая билеты, используя API, предоставленный kayakodb. Затем он использовал это представление билетов для связи клиентов и доменов. Однако это было слишком сложно и не очень логично. Поэтому мы решили переключить его на прокси-модель и переместить модели, представляющие ссылки на клиентов и домены, на kayakodb. Позвоните в это приложение sidebar.

Другое новое приложение показывает билеты из системы поддержки билетов в четком обзоре наряду с звонками, поэтому наш отдел поддержки может легко увидеть, какие звонки и билеты связаны с теми клиентами. Эта система имеет прокси-модель для прокси-модели sidebar, потому что некоторые функции, предоставляемые моделью sidebar, также требуются для этого приложения наряду с некоторыми другими, объявленными новой прокси-моделью. Позвольте назвать этот проект WOW.

Приложения sidebar и WOW являются частью одного и того же проекта/репозитория. Мы будем называть этот репозиторий Coneybeach который имеет свою собственную базу данных. Однако kayakodb - полностью не связанный проект. Он входит в Coneybeach через файл требований, который мы устанавливаем через pip.

Проблема


При создании миграции для новой настройки Django создает миграцию прокси-модели для установленного kayakodb, который, конечно же, не идет. Каждый раз, когда мы будем устанавливать новую версию kayakodb, она перезапишет эту миграцию. Не говоря уже о том, что kayakodb не должен знать ничего о том, какие модели используют его.

Код


Модель Ticket внутри 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

Прокси-модель SidebarTicket внутри 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

Класс Contact TicketWrapper наследуется от (по запросу Hynekcer). Эта модель используется в качестве базовой модели для TicketWrapper и другой модели, представляющей вызовы (хотя, насколько мне известно, проблем с этой моделью нет):

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)

Прокси-модель TicketWrapper внутри 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

Что я пробовал


  • Я пробовал не указывать app_label для прокси-моделей. Это создает правильные миграции, но заставляет прокси-модели искать модель kayakodb.Ticket в базе данных Coneybeach.
  • Я попытался указать abstract = True для подклассов, но не был уверен, что это было потому, что я все еще хочу иметь возможность использовать диспетчер для моделей.
  • Я думал о переносе миграции, которая создается в настоящее время в фактическом проекте kayakodb, но я не думаю, что это хорошее решение. kayakodb не должен знать ничего о реализации своих моделей или о том, где они используются.
  • ./manage.py check возвращает 0 проблем.

Вопрос


Как создать прокси-модель для модели, расположенной в другой базе данных или проекте?

Изменить


После того, как модель kayakodb.Ticket будет неуправляемой, проект WOW пытается создать миграцию для всех моделей в kayakodb. Результат:
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

Ответ 1

Как сказал @hynekcer, если kayakodb - это существующая база данных, вам нужно установить managed = False для всех своих моделей. Однако это все еще оставляет проблему миграции для прокси-модели, созданной внутри неправильного приложения (kayakodb).

Хакерное исправление, которое может сработать, изменяет app_label прокси-модели на любое приложение, в котором можно переносить переход (sidebar в этом случае), и создание маршрутизатора, который будет указывать эту прокси-модель для чтения и пишите из kayakodb.

например. прокси-модель:

# in sidebar/models.py

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

и маршрутизатор внутри используемого проекта:

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

Ответ 2

tl; dr, но я поставил ваш вопрос на слово router, о котором не упоминалось, поэтому я думаю, что вы ищете, Маршрутизаторы баз данных

Ответ 3

tl; dr Используйте

class Meta:
    managed = False

для всех моделей в db, которые не должны управляться Django. (т.е. kayakodb)


Следует отметить, что PyPI Kayako - это API-интерфейс Python (без Djago) для некоторых приложений Kayako, написанных на другом языке.

Полезно знать от вас, что Kayako и WOW находятся в разных базах данных, но это не фундаментальная информация. Django позволяет, например, что одна модель присутствует в двух базах данных: первичный и вторичный db. Наиболее важными являются мета-опции, опция managed = False в этом случае. Это для случая Интеграция Django с устаревшей базой данных. Это не означает только очень старый проект, но проект, который не написан на Python + Django или не поддерживает миграции, и не передает информацию, какие миграции еще не применяются. Возможно, если новая версия Kayako добавит новые поля в базу данных, вам не нужно читать это поле, или если вы добавите его в модель, Django не несет ответственности за добавление поля в базу данных, поскольку он контролируется обновлением Kayako.

В качестве альтернативы вы можете использовать маршрутизаторы баз данных для создания таблиц для приложения kayakodb (нигде), а также нет таблиц в базе данных kayakodb, например. для пользователей и групп Django:

файл myrouter.py или аналогичный

class MyRouter(object):
    allow_migrate(db, app_label, model_name=None, **hints):
        if app_label == 'kayakodb' or db == 'kayakodb':
            return False

файл settings.py

DATABASE_ROUTERS = ['path.to.myrouter.MyRouter',...]
# ... if another router has been defined previously

Преимущество заключается в том, что это отключает перенос для любой текущей или будущей модели, но я рекомендую добавить текст class Meta: managed = False также где-нибудь к комментариям в kayakodb/models.py, чтобы дать понять любому более позднему разработчику, потому что он может легко забудьте сначала прочитать маршрутизаторы.

Вы можете написать зависимость версий вашего проекта от минимальной и максимальной версии API Kayako, но она не может быть в виде миграции.


Другая проблема заключается в том, что Прокси-модель должна наследовать только с одного не-абстрактного класса модели.... ", но ваша прокси-модель TicketWrapper наследуется от Contact и SidebarTicket. Это похоже на вздор, и мне интересно, что вы не видите ошибки. TypeError: Proxy model 'TicketWrapper' has more than one non-abstract model base class. Тот же самый контакт может использоваться другими билетами. (Не является ли он зарегистрированным пользователем? Не может ли он что-либо изменить в своем профиле пользователя во время истории проблемы?) Это должен быть, вероятно, внешний ключ для контакта, а не множественное наследование.