Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save samukasmk/deab4fc770c0f864e147eca1e8e20d00 to your computer and use it in GitHub Desktop.
Save samukasmk/deab4fc770c0f864e147eca1e8e20d00 to your computer and use it in GitHub Desktop.

Proof of Concept: Implementing MongoEngine lib with support for multi database connections

Reason:

Currently MongoEngine library only support a single database connection, managing collections by Document classes in global scope.

It has a support to change database connections by switch_db but it's made by a global class attribute called ._meta['db_alias']

When you define many sub ReferenceFields in a hierarchical tree, only first Document object has its ._meta['db_alias'] atttibute changed, not working to query Documents that implement other documents.

Goal:

Implementing MongoEngine lib for multi database connections support hierarchical tree of many sub ReferenceFields

How to run:

1.) Create files bellow:

2.) Execute PoC script:

$ python execute_poc.py
[db connection: first-db] Account (selecting null values): {}
[db connection: first-db] Phone (selecting null values): {}
[db connection: first-db] Chat (selecting null values): {}
---
[db connection: second-db] Account (selecting null values): {}
[db connection: second-db] Phone (selecting null values): {}
[db connection: second-db] Chat (selecting null values): {}
---
[db connection: second-db] Account (created new value): {"_id": {"$oid": "6616c9862dff85db3d3c90a8"}, "name": "account 1"}
[db connection: second-db] Phone (created new value): {"_id": {"$oid": "6616c9862dff85db3d3c90a9"}, "number": "11 98765-4321", "account": {"$oid": "6616c9862dff85db3d3c90a8"}}
[db connection: second-db] Chat (created new value): {"_id": {"$oid": "6616c9862dff85db3d3c90aa"}, "name": "my first chat", "phone": {"$oid": "6616c9862dff85db3d3c90a9"}}
---
[db connection: second-db] Account (selecting all values): ['{"_id": {"$oid": "6616c9862dff85db3d3c90a8"}, "name": "account 1"}']
[db connection: second-db] Phone (selecting all values): ['{"_id": {"$oid": "6616c9862dff85db3d3c90a9"}, "number": "11 98765-4321", "account": {"$oid": "6616c9862dff85db3d3c90a8"}}']
[db connection: second-db] Chat (selecting all values): ['{"_id": {"$oid": "6616c9862dff85db3d3c90aa"}, "name": "my first chat", "phone": {"$oid": "6616c9862dff85db3d3c90a9"}}']
---
[db connection: second-db] Account (selecting cross values): {"_id": {"$oid": "6616c9862dff85db3d3c90a8"}, "name": "account 1"}
[db connection: second-db] Phone (selecting cross values): {"_id": {"$oid": "6616c9862dff85db3d3c90a9"}, "number": "11 98765-4321", "account": {"$oid": "6616c9862dff85db3d3c90a8"}}
[db connection: second-db] Chat (selecting cross values): {"_id": {"$oid": "6616c9862dff85db3d3c90aa"}, "name": "my first chat", "phone": {"$oid": "6616c9862dff85db3d3c90a9"}}
---
[db connection: first-db] Account (selecting null values again): {}
[db connection: first-db] Phone (selecting null values again): {}
[db connection: first-db] Chat (selecting null values again): {}
---
""" file: execute_poc.py """
from db.connection import register_db_connection, registered_models
#
# registering connections
#
register_db_connection(alias='first-db', db='my_database', host='mongodb://user:password@first-db:27017/')
register_db_connection(alias='second-db', db='my_database', host='mongodb://user:password@second-db:27017/')
register_db_connection(alias='third-db', db='my_database', host='mongodb://user:password@third-db:27017/')
#
# getting specific model classes from db connection alias: 'first-db'
#
Account = registered_models['first-db']['Account']
Phone = registered_models['first-db']['Phone']
Chat = registered_models['first-db']['Chat']
#
# cleaning first-db objects
#
Account.objects.all().delete()
Phone.objects.all().delete()
Chat.objects.all().delete()
#
# selecting null values
#
account = Account.objects.first()
print('[db connection: first-db] Account (selecting null values):', account.to_json() if account else {})
phone = Phone.objects.first()
print('[db connection: first-db] Phone (selecting null values):', phone.to_json() if phone else {})
chat = Chat.objects.first()
print('[db connection: first-db] Chat (selecting null values):', chat.to_json() if chat else {})
print('---')
#
# getting specific model classes from db connection alias: 'second-db'
#
Account = registered_models['second-db']['Account']
Phone = registered_models['second-db']['Phone']
Chat = registered_models['second-db']['Chat']
#
# cleaning database
#
Account.objects.all().delete()
Phone.objects.all().delete()
Chat.objects.all().delete()
#
# selecting null values
#
account = Account.objects.first()
print('[db connection: second-db] Account (selecting null values):', account.to_json() if account else {})
phone = Phone.objects.first()
print('[db connection: second-db] Phone (selecting null values):', phone.to_json() if phone else {})
chat = Chat.objects.first()
print('[db connection: second-db] Chat (selecting null values):', chat.to_json() if chat else {})
print('---')
#
# creating objects
#
account = Account(name='account 1')
account.save()
print('[db connection: second-db] Account (created new value):', account.to_json())
phone = Phone(number='11 98765-4321', account=account)
phone.save()
print('[db connection: second-db] Phone (created new value):', phone.to_json())
chat = Chat(name='my first chat', phone=phone)
chat.save()
print('[db connection: second-db] Chat (created new value):', chat.to_json())
print('---')
#
# selecting all values
#
accounts = Account.objects.all()
print('[db connection: second-db] Account (selecting all values):',
[account.to_json() if account else {} for account in accounts])
phones = Phone.objects.all()
print('[db connection: second-db] Phone (selecting all values):',
[phone.to_json() if phone else {} for phone in phones])
chats = Chat.objects.all()
print('[db connection: second-db] Chat (selecting all values):', [chat.to_json() if chat else {} for chat in chats])
print('---')
#
# selecting cross values
#
account = Account.objects.first()
print('[db connection: second-db] Account (selecting cross values):', account.to_json() if account else {})
phone = Phone.objects(account=account).first()
print('[db connection: second-db] Phone (selecting cross values):', phone.to_json() if phone else {})
chat = Chat.objects(phone=phone).first()
print('[db connection: second-db] Chat (selecting cross values):', chat.to_json() if chat else {})
print('---')
#
# getting specific model classes from db connection alias: 'first-db'
#
Account = registered_models['first-db']['Account']
Phone = registered_models['first-db']['Phone']
Chat = registered_models['first-db']['Chat']
#
# selecting null values
#
account = Account.objects.first()
print('[db connection: first-db] Account (selecting null values again):', account.to_json() if account else {})
phone = Phone.objects.first()
print('[db connection: first-db] Phone (selecting null values again):', phone.to_json() if phone else {})
chat = Chat.objects.first()
print('[db connection: first-db] Chat (selecting null values again):', chat.to_json() if chat else {})
print('---')
""" file: db/connection.py """
from mongoengine.connection import register_connection
from mongoengine.document import BaseDocument
registered_models = {}
#
# Example of populated dict bellow:
#
# registered_models = {
# 'first-db': {
# 'Account': <Account class [for 'first-db' db connection]>,
# 'Phone': <Phone class [for 'first-db' db connection]>,
# 'Chat': <Chat class [for 'first-db' db connection]>
# },
# 'second-db': {
# 'Account': <Account class [for 'second-db' db connection]>,
# 'Phone': <Phone class [for 'second-db' db connection]>,
# 'Chat': <Chat class [for 'second-db' db connection]>
# },
# 'third-db': {
# 'Account': <Account class [for 'third-db' db connection]>,
# 'Phone': <Phone class [for 'third-db' db connection]>,
# 'Chat': <Chat class [for 'third-db' db connection]>
# }
# }
def register_db_connection(alias, db, host):
# regiter connection in mongoengine
register_connection(alias=alias, db=db, host=host)
# create db alias key in registered_models
registered_models[alias] = {}
# import and register models for this specific db alias
from models import account, phone, chat
try:
model_class = chat.build_model_class(alias)
register_model_class(model_class, alias)
except ModelBuildingHasPendingReference:
...
model_class = account.build_model_class(alias)
register_model_class(alias, model_class)
model_class = phone.build_model_class(alias)
register_model_class(alias, model_class)
model_class = chat.build_model_class(alias)
register_model_class(alias, model_class)
def register_model_class(db_alias: str, model_class: BaseDocument) -> None:
# inject db alias in created model class
model_class._meta['db_alias'] = db_alias
model_class._collection = None
# get class name
class_name = model_class.__name__
# save new class in global object for specific db connection
global registered_models
registered_models[db_alias][class_name] = model_class
def get_referenced_model(class_name, db_alias):
global registered_models
try:
return registered_models[db_alias][class_name]
except KeyError:
raise ModelBuildingHasPendingReference('please wait...')
class ModelBuildingHasPendingReference(Exception):
...
""" file: models/account.py """
from mongoengine.document import BaseDocument, Document
from mongoengine.fields import StringField
def build_model_class(db_alias: str) -> BaseDocument:
class Account(Document):
name = StringField(max_lenght=50)
meta = {
'collection': 'accounts'
}
return Account
""" file: models/phone.py """
from mongoengine.document import BaseDocument, Document
from mongoengine.fields import StringField, ReferenceField
from db.connection import get_referenced_model
def build_model_class(db_alias: str) -> BaseDocument:
Account = get_referenced_model('Account', db_alias)
class Phone(Document):
number = StringField(max_lenght=13)
account = ReferenceField(Account)
meta = {
'collection': 'phones'
}
return Phone
""" file: models/chat.py """
from mongoengine.document import BaseDocument, Document
from mongoengine.fields import StringField, ReferenceField
from db.connection import get_referenced_model
def build_model_class(db_alias: str) -> BaseDocument:
Phone = get_referenced_model('Phone', db_alias)
class Chat(Document):
name = StringField(max_lenght=22)
phone = ReferenceField(Phone)
meta = {
'collection': 'chats'
}
return Chat
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment