From b1e28d02f7f16a3aa0eb4b718377ddff8e9fc6f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Wed, 24 Apr 2019 22:44:07 +0200 Subject: [PATCH] Improve connect/disconnect - document disconnect + sample of usage - add more test cases to prevent github issues regressions --- docs/changelog.rst | 2 +- docs/guide/connecting.rst | 42 ++++++++++++++++++++++--- mongoengine/connection.py | 3 ++ mongoengine/document.py | 6 ++-- tests/test_connection.py | 64 ++++++++++++++++++++++++++++++--------- 5 files changed, 95 insertions(+), 22 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6bd19b12..bf3bba24 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,7 +5,7 @@ Changelog Development =========== - expose `mongoengine.connection.disconnect` and `mongoengine.connection.disconnect_all` -- POTENTIAL BREAKING CHANGE: Fixes in connect/disconnect methods +- POTENTIAL BREAKING CHANGE: Fixes in connect/disconnect methods #565 #566 - calling `connect` 2 times with the same alias and different parameter will raise an error (should call disconnect first) - disconnect now clears `mongoengine.connection._connection_settings` - disconnect now clears the cached attribute `Document._collection` diff --git a/docs/guide/connecting.rst b/docs/guide/connecting.rst index 5dac6ae9..1107ee3a 100644 --- a/docs/guide/connecting.rst +++ b/docs/guide/connecting.rst @@ -4,9 +4,11 @@ Connecting to MongoDB ===================== -To connect to a running instance of :program:`mongod`, use the -:func:`~mongoengine.connect` function. The first argument is the name of the -database to connect to:: +Connections in MongoEngine are registered globally and are identified with aliases. +If no `alias` is provided during the connection, it will use "default" as alias. + +To connect to a running instance of :program:`mongod`, use the :func:`~mongoengine.connect` +function. The first argument is the name of the database to connect to:: from mongoengine import connect connect('project1') @@ -42,6 +44,9 @@ the :attr:`host` to will establish connection to ``production`` database using ``admin`` username and ``qwerty`` password. +.. note:: Calling :func:`~mongoengine.connect` without argument will establish + a connection to the "test" database by default + Replica Sets ============ @@ -71,6 +76,8 @@ is used. In the background this uses :func:`~mongoengine.register_connection` to store the data and you can register all aliases up front if required. +Documents defined in different database +--------------------------------------- Individual documents can also support multiple databases by providing a `db_alias` in their meta data. This allows :class:`~pymongo.dbref.DBRef` objects to point across databases and collections. Below is an example schema, @@ -93,6 +100,33 @@ using 3 different databases to store data:: meta = {'db_alias': 'users-books-db'} +Disconnecting an existing connection +------------------------------------ +The function :func:`~mongoengine.disconnect` can be used to +disconnect a particular connection. This can be used to change a +connection globally:: + + from mongoengine import connect, disconnect + connect('a_db', alias='db1') + + class User(Document): + name = StringField() + meta = {'db_alias': 'db1'} + + disconnect(alias='db1') + + connect('another_db', alias='db1') + +.. note:: Calling :func:`~mongoengine.disconnect` without argument + will disconnect the "default" connection + +.. note:: Since connections gets registered globally, it is important + to use the `disconnect` function from MongoEngine and not the + `disconnect()` method of an existing connection (pymongo.MongoClient) + +.. note:: :class:`~mongoengine.Document` are caching the pymongo collection. + using `disconnect` ensures that it gets cleaned as well + Context Managers ================ Sometimes you may want to switch the database or collection to query against. @@ -119,7 +153,7 @@ access to the same User document across databases:: Switch Collection ----------------- -The :class:`~mongoengine.context_managers.switch_collection` context manager +The :func:`~mongoengine.context_managers.switch_collection` context manager allows you to change the collection for a given class allowing quick and easy access to the same Group document across collection:: diff --git a/mongoengine/connection.py b/mongoengine/connection.py index 13541bd4..0597199b 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -318,6 +318,9 @@ def connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs): Multiple databases are supported by using aliases. Provide a separate `alias` to connect to a different instance of :program:`mongod`. + In order to replace a connection identified by a given alias, you'll + need to call ``disconnect`` first + See the docstring for `register_connection` for more details about all supported kwargs. diff --git a/mongoengine/document.py b/mongoengine/document.py index fd953340..753520c7 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -795,13 +795,13 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)): .. versionchanged:: 0.10.7 :class:`OperationError` exception raised if no collection available """ - col_name = cls._get_collection_name() - if not col_name: + coll_name = cls._get_collection_name() + if not coll_name: raise OperationError('Document %s has no collection defined ' '(is it abstract ?)' % cls) cls._collection = None db = cls._get_db() - db.drop_collection(col_name) + db.drop_collection(coll_name) @classmethod def create_index(cls, keys, background=False, **kwargs): diff --git a/tests/test_connection.py b/tests/test_connection.py index 4bab23c6..827829b5 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,4 +1,6 @@ import datetime + +from pymongo import MongoClient from pymongo.errors import OperationFailure try: @@ -238,12 +240,25 @@ class ConnectionTest(unittest.TestCase): conn = get_connection('testdb6') self.assertIsInstance(conn, mongomock.MongoClient) - def test_disconnect(self): - """Ensure that the disconnect() method works properly""" + def test_disconnect_cleans_globals(self): + """Ensure that the disconnect() method cleans the globals objects""" connections = mongoengine.connection._connections dbs = mongoengine.connection._dbs connection_settings = mongoengine.connection._connection_settings + connect('mongoenginetest') + + self.assertEqual(len(connections), 1) + self.assertEqual(len(dbs), 1) + self.assertEqual(len(connection_settings), 1) + + disconnect() + self.assertEqual(len(connections), 0) + self.assertEqual(len(dbs), 0) + self.assertEqual(len(connection_settings), 0) + + def test_disconnect_cleans_cached_collection_attribute_in_document(self): + """Ensure that the disconnect() method works properly""" conn1 = connect('mongoenginetest') class History(Document): @@ -252,29 +267,50 @@ class ConnectionTest(unittest.TestCase): self.assertIsNone(History._collection) History.drop_collection() + History.objects.first() # will trigger the caching of _collection attribute - self.assertIsNotNone(History._collection) - self.assertEqual(len(connections), 1) - self.assertEqual(len(dbs), 1) - self.assertEqual(len(connection_settings), 1) - disconnect() self.assertIsNone(History._collection) - self.assertEqual(len(connections), 0) - self.assertEqual(len(dbs), 0) - self.assertEqual(len(connection_settings), 0) - with self.assertRaises(MongoEngineConnectionError) as ctx_err: History.objects.first() self.assertEqual("You have not defined a default connection", str(ctx_err.exception)) - conn2 = connect('mongoenginetest') - History.objects.first() # Make sure its back on track - self.assertTrue(conn1 is not conn2) + def test_connect_disconnect_works_on_same_document(self): + """Ensure that the connect/disconnect works properly with a single Document""" + db1 = 'db1' + db2 = 'db2' + + # Ensure freshness of the 2 databases through pymongo + client = MongoClient('localhost', 27017) + client.drop_database(db1) + client.drop_database(db2) + + # Save in db1 + connect(db1) + + class User(Document): + name = StringField(required=True) + + user1 = User(name='John is in db1').save() + disconnect() + + # Make sure save doesnt work at this stage + with self.assertRaises(MongoEngineConnectionError): + User(name='Wont work').save() + + # Save in db2 + connect(db2) + user2 = User(name='Bob is in db2').save() + disconnect() + + db1_users = list(client[db1].user.find()) + self.assertEqual(db1_users, [{'_id': user1.id, 'name': 'John is in db1'}]) + db2_users = list(client[db2].user.find()) + self.assertEqual(db2_users, [{'_id': user2.id, 'name': 'Bob is in db2'}]) def test_disconnect_silently_pass_if_alias_does_not_exist(self): connections = mongoengine.connection._connections