Многопользовательские приложения Django: изменение соединения с базой данных по запросу?
Я ищу рабочий код и идеи от других, которые пытались создать многопользовательское приложение Django с использованием изоляции на уровне базы данных.
Обновление/Решение: Я закончил решение этого в новом проекте с открытым исходным кодом: см. django-db-multitenant
цель
моя цель-мультиплексировать запросы, когда они поступают на один сервер приложений (интерфейс WSGI, например gunicorn), на основе имени хоста запроса или пути запроса (для например, foo.example.com/
устанавливает соединение Django для использования базы данных foo
и bar.example.com/
использует базу данных bar
).
прецедент
Я знаю о нескольких существующих решениях для multi tenancy в Django:
-
Джанго-арендатор-схемы: это очень близко к тому, что я хочу: вы устанавливаете его промежуточное ПО с наивысшим приоритетом, и он отправляет
SET search_path
команда в БД. К сожалению, это Postgres, и я застрял с Для MySQL. -
Джанго-просто-мультитенантный: стратегия здесь состоит в том, чтобы добавить внешний ключ "арендатора" ко всем моделям и настроить всю бизнес-логику приложения на ключ. В основном каждая строка индексируется
(id, tenant_id)
, а не(id)
. Я пробовал и мне не нравится этот подход по ряду причин: он делает приложение более сложным, может привести к труднодоступным ошибкам и не обеспечивает изоляцию на уровне базы данных. - один {сервер приложений, файл настроек django с соответствующим db} для каждого клиента. Он же многоквартирный дом бедняка (на самом деле богатый человек, учитывая ресурсы, которые он включает). Я не хочу создавать новый сервер приложений для каждого клиента, и для масштабируемости я хочу, чтобы любой сервер приложений мог отправлять запросы для любого клиента.
идеи
моя лучшая идея до сих пор сделать что-то вроде django-tenant-schemas
: в первом middleware, захватить django.db.connection
и возиться с выбором базы данных, а не схемы. Я не совсем продумали, что это означает с точки зрения Объединенных/постоянных соединений
еще один тупик, который я преследовал, был префиксами таблиц для арендаторов: кроме того, мне нужно, чтобы они были динамическими, даже глобальный префикс таблицы не легко достигается в Django (см. отклонен билет 5000, среди других).
Наконец-То, Джанго поддержка нескольких баз данных позволяет определить несколько именованных баз данных и mux среди них на основе экземпляра тип и режим чтения/записи. Не полезно, Так как нет возможности выбрать БД на основе каждого запроса.
вопрос
кто-нибудь управлял чем-то подобным? Если да, то как вы его осуществили?
3 ответов
Я сделал что-то подобное, что ближе всего к точке 1, но вместо использования промежуточного ПО для установки соединения по умолчанию используются маршрутизаторы базы данных Django. Это позволяет логике приложения использовать несколько баз данных, если это необходимо для каждого запроса. Логика приложения позволяет выбрать подходящую базу данных для каждого запроса, и это большой недостаток этого подхода.
при этой настройке все базы данных перечислены в settings.DATABASES
, включая базы данных, которые могут совместно использоваться клиенты. Каждая модель, специфичная для клиента, помещается в приложение Django с определенной меткой приложения.
например. Следующий класс определяет модель, которая существует во всех клиентских базах данных.
class MyModel(Model):
....
class Meta:
app_label = 'customer_records'
managed = False
маршрутизатор базы данных помещен в settings.DATABASE_ROUTERS
цепи запросу маршруту app_label
, что-то вроде этого (не полный) пример:
class AppLabelRouter(object):
def get_customer_db(self, model):
# Route models belonging to 'myapp' to the 'shared_db' database, irrespective
# of customer.
if model._meta.app_label == 'myapp':
return 'shared_db'
if model._meta.app_label == 'customer_records':
customer_db = thread_local_data.current_customer_db()
if customer_db is not None:
return customer_db
raise Exception("No customer database selected")
return None
def db_for_read(self, model, **hints):
return self.get_customer_db(model, **hints)
def db_for_write(self, model, **hints):
return self.get_customer_db(model, **hints)
специальная часть об этом маршрутизаторе -thread_local_data.current_customer_db()
звонок. Прежде чем маршрутизатор будет выполнен, вызывающий абонент / приложение должно настроить текущую клиентскую БД в thread_local_data
. Для этой цели можно использовать контекстный менеджер Python для нажатия/pop текущей базы данных клиентов.
при всей этой настройке код приложения выглядит примерно так, где UseCustomerDatabase
является контекстным менеджером для push / pop текущего имени базы данных клиентов в thread_local_data
, так что thread_local_data.current_customer_db()
возвращает правильное имя базы данных, когда маршрутизатор в конечном итоге нажмите:
class MyView(DetailView):
def get_object(self):
db_name = determine_customer_db_to_use(self.request)
with UseCustomerDatabase(db_name):
return MyModel.object.get(pk=1)
это вполне уже сложная установка. Это работает, но я попытаюсь суммировать то, что я вижу как преимущества и недостатки:
преимущества
- выбор базы данных является гибким. Он позволяет использовать несколько баз данных в одном запросе, в запросе могут использоваться как клиентские, так и общие базы данных.
- выбор базы данных является явным (не уверен, является ли это преимуществом или недостатком). При попытке выполнить запрос, который попадает в клиента база данных, но приложение не выбрало один, произойдет исключение, указывающее на ошибку программирования.
- использование маршрутизатора базы данных позволяет различным базам данных существовать на разных хостах, а не полагаться на
USE db;
оператор, который предполагает, что все базы данных доступны через одно соединение.
недостатки
- это сложно настроить, и есть довольно много слоев, чтобы получить его функционирующий.
- необходимость и использование локальных данных потока неясны.
- представления усеяны кодом выбора базы данных. Это может быть абстрагировано с помощью представлений на основе классов для автоматического выбора базы данных на основе параметров запроса таким же образом, как промежуточное ПО выбирает базу данных по умолчанию.
- менеджер контекста для выбора базы данных должен быть обернут вокруг набора запросов таким образом, чтобы менеджер контекста оставался активным, когда запрос оцененный.
предложения
если вы хотите гибкий доступ к базе данных, я бы предложил использовать маршрутизаторы базы данных Django. Используйте Middleware или view Mixin, который автоматически настраивает базу данных по умолчанию для подключения на основе параметров запроса. Возможно, вам придется прибегнуть к потоковым локальным данным для хранения базы данных по умолчанию, чтобы при попадании маршрутизатора он знал, к какой базе данных следует маршрутизировать. Это позволяет Django использовать его существующий постоянные подключения к базе данных (которая может находиться на разных хостах, если требуется) и выбирает базу данных для использования на основе маршрутизации, настроенной в запросе.
этот подход имеет то преимущество, что база данных для запроса может быть изменено в случае необходимости с помощью QuerySet using()
вы можете создать простое промежуточное программное обеспечение, которое определило имя базы данных из вашего поддомена или что-то еще, а затем выполнило использовать оператор на курсоре базы данных для каждого запроса. Глядя на код схемы django-tenants-schema, это, по сути, то, что он делает. Это подкласс psycopg2 и выдача postgres, эквивалентных использованию, "set search_path XXX". Вы можете создать модель для управления и создания своих арендаторов, но тогда вы будете много переписывать из django-арендаторы-схемы.
не должно быть никакого штрафа производительности или ресурса в MySQL для переключения схемы (имя БД). Это просто установка параметра сеанса связи.
для записи я решил реализовать вариант моей первой идеи: выпустить USE <dbname>
в начале промежуточного запроса. Я также устанавливаю префикс кэша таким же образом.
Я использую его на небольшом производственном сайте, ища имя арендатора из базы данных Redis на основе хоста запроса. Пока я вполне доволен результатами.
Я превратил его в (надеюсь, resuable) проект github здесь:https://github.com/mik3y/django-db-multitenant