mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #7732 -- Added support for connection pools on Oracle.
This commit is contained in:
		| @@ -14,6 +14,7 @@ from django.conf import settings | ||||
| from django.core.exceptions import ImproperlyConfigured | ||||
| from django.db import IntegrityError | ||||
| from django.db.backends.base.base import BaseDatabaseWrapper | ||||
| from django.db.backends.oracle.oracledb_any import is_oracledb | ||||
| from django.db.backends.utils import debug_transaction | ||||
| from django.utils.asyncio import async_unsafe | ||||
| from django.utils.encoding import force_bytes, force_str | ||||
| @@ -235,6 +236,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): | ||||
|     introspection_class = DatabaseIntrospection | ||||
|     ops_class = DatabaseOperations | ||||
|     validation_class = DatabaseValidation | ||||
|     _connection_pools = {} | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
| @@ -243,10 +245,52 @@ class DatabaseWrapper(BaseDatabaseWrapper): | ||||
|         ) | ||||
|         self.features.can_return_columns_from_insert = use_returning_into | ||||
|  | ||||
|     @property | ||||
|     def is_pool(self): | ||||
|         return self.settings_dict["OPTIONS"].get("pool", False) | ||||
|  | ||||
|     @property | ||||
|     def pool(self): | ||||
|         if not self.is_pool: | ||||
|             return None | ||||
|  | ||||
|         if self.settings_dict.get("CONN_MAX_AGE", 0) != 0: | ||||
|             raise ImproperlyConfigured( | ||||
|                 "Pooling doesn't support persistent connections." | ||||
|             ) | ||||
|  | ||||
|         pool_key = (self.alias, self.settings_dict["USER"]) | ||||
|         if pool_key not in self._connection_pools: | ||||
|             connect_kwargs = self.get_connection_params() | ||||
|             pool_options = connect_kwargs.pop("pool") | ||||
|             if pool_options is not True: | ||||
|                 connect_kwargs.update(pool_options) | ||||
|  | ||||
|             pool = Database.create_pool( | ||||
|                 user=self.settings_dict["USER"], | ||||
|                 password=self.settings_dict["PASSWORD"], | ||||
|                 dsn=dsn(self.settings_dict), | ||||
|                 **connect_kwargs, | ||||
|             ) | ||||
|             self._connection_pools.setdefault(pool_key, pool) | ||||
|  | ||||
|         return self._connection_pools[pool_key] | ||||
|  | ||||
|     def close_pool(self): | ||||
|         if self.pool: | ||||
|             self.pool.close(force=True) | ||||
|             pool_key = (self.alias, self.settings_dict["USER"]) | ||||
|             del self._connection_pools[pool_key] | ||||
|  | ||||
|     def get_database_version(self): | ||||
|         return self.oracle_version | ||||
|  | ||||
|     def get_connection_params(self): | ||||
|         # Pooling feature is only supported for oracledb. | ||||
|         if self.is_pool and not is_oracledb: | ||||
|             raise ImproperlyConfigured( | ||||
|                 "Pooling isn't supported by cx_Oracle. Use python-oracledb instead." | ||||
|             ) | ||||
|         conn_params = self.settings_dict["OPTIONS"].copy() | ||||
|         if "use_returning_into" in conn_params: | ||||
|             del conn_params["use_returning_into"] | ||||
| @@ -254,6 +298,8 @@ class DatabaseWrapper(BaseDatabaseWrapper): | ||||
|  | ||||
|     @async_unsafe | ||||
|     def get_new_connection(self, conn_params): | ||||
|         if self.pool: | ||||
|             return self.pool.acquire() | ||||
|         return Database.connect( | ||||
|             user=self.settings_dict["USER"], | ||||
|             password=self.settings_dict["PASSWORD"], | ||||
| @@ -345,6 +391,12 @@ class DatabaseWrapper(BaseDatabaseWrapper): | ||||
|         else: | ||||
|             return True | ||||
|  | ||||
|     def close_if_health_check_failed(self): | ||||
|         if self.pool: | ||||
|             # The pool only returns healthy connections. | ||||
|             return | ||||
|         return super().close_if_health_check_failed() | ||||
|  | ||||
|     @cached_property | ||||
|     def oracle_version(self): | ||||
|         with self.temporary_connection(): | ||||
|   | ||||
| @@ -205,13 +205,15 @@ class DatabaseCreation(BaseDatabaseCreation): | ||||
|         Destroy a test database, prompting the user for confirmation if the | ||||
|         database already exists. Return the name of the test database created. | ||||
|         """ | ||||
|         self.connection.settings_dict["USER"] = self.connection.settings_dict[ | ||||
|             "SAVED_USER" | ||||
|         ] | ||||
|         self.connection.settings_dict["PASSWORD"] = self.connection.settings_dict[ | ||||
|             "SAVED_PASSWORD" | ||||
|         ] | ||||
|         if not self.connection.is_pool: | ||||
|             self.connection.settings_dict["USER"] = self.connection.settings_dict[ | ||||
|                 "SAVED_USER" | ||||
|             ] | ||||
|             self.connection.settings_dict["PASSWORD"] = self.connection.settings_dict[ | ||||
|                 "SAVED_PASSWORD" | ||||
|             ] | ||||
|         self.connection.close() | ||||
|         self.connection.close_pool() | ||||
|         parameters = self._get_test_db_params() | ||||
|         with self._maindb_connection.cursor() as cursor: | ||||
|             if self._test_user_create(): | ||||
| @@ -223,6 +225,7 @@ class DatabaseCreation(BaseDatabaseCreation): | ||||
|                     self.log("Destroying test database tables...") | ||||
|                 self._execute_test_db_destruction(cursor, parameters, verbosity) | ||||
|         self._maindb_connection.close() | ||||
|         self._maindb_connection.close_pool() | ||||
|  | ||||
|     def _execute_test_db_creation(self, cursor, parameters, verbosity, keepdb=False): | ||||
|         if verbosity >= 2: | ||||
|   | ||||
| @@ -139,6 +139,25 @@ class DatabaseFeatures(BaseDatabaseFeatures): | ||||
|                     }, | ||||
|                 } | ||||
|             ) | ||||
|         if self.connection.is_pool: | ||||
|             skips.update( | ||||
|                 { | ||||
|                     "Pooling does not support persistent connections": { | ||||
|                         "backends.base.test_base.ConnectionHealthChecksTests." | ||||
|                         "test_health_checks_enabled", | ||||
|                         "backends.base.test_base.ConnectionHealthChecksTests." | ||||
|                         "test_health_checks_enabled_errors_occurred", | ||||
|                         "backends.base.test_base.ConnectionHealthChecksTests." | ||||
|                         "test_health_checks_disabled", | ||||
|                         "backends.base.test_base.ConnectionHealthChecksTests." | ||||
|                         "test_set_autocommit_health_checks_enabled", | ||||
|                         "servers.tests.LiveServerTestCloseConnectionTest." | ||||
|                         "test_closes_connections", | ||||
|                         "backends.oracle.tests.TransactionalTests." | ||||
|                         "test_password_with_at_sign", | ||||
|                     }, | ||||
|                 } | ||||
|             ) | ||||
|         if is_oracledb and self.connection.oracledb_version >= (2, 1, 2): | ||||
|             skips.update( | ||||
|                 { | ||||
|   | ||||
| @@ -994,7 +994,7 @@ Oracle notes | ||||
| ============ | ||||
|  | ||||
| Django supports `Oracle Database Server`_ versions 19c and higher. Version | ||||
| 1.3.2 or higher of the `oracledb`_ Python driver is required. | ||||
| 2.3.0 or higher of the `oracledb`_ Python driver is required. | ||||
|  | ||||
| .. deprecated:: 5.0 | ||||
|  | ||||
| @@ -1105,6 +1105,46 @@ Example of a full DSN string:: | ||||
|         "(CONNECT_DATA=(SERVICE_NAME=orclpdb1)))" | ||||
|     ) | ||||
|  | ||||
| .. _oracle-pool: | ||||
|  | ||||
| Connection pool | ||||
| --------------- | ||||
|  | ||||
| .. versionadded:: 5.2 | ||||
|  | ||||
| To use a connection pool with `oracledb`_, set ``"pool"`` to ``True`` in the | ||||
| :setting:`OPTIONS` part of your database configuration. This uses the driver's | ||||
| `create_pool()`_ default values:: | ||||
|  | ||||
|     DATABASES = { | ||||
|         "default": { | ||||
|             "ENGINE": "django.db.backends.oracle", | ||||
|             # ... | ||||
|             "OPTIONS": { | ||||
|                 "pool": True, | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
|  | ||||
| To pass custom parameters to the driver's `create_pool()`_  function, you can | ||||
| alternatively set ``"pool"`` to be a dict:: | ||||
|  | ||||
|     DATABASES = { | ||||
|         "default": { | ||||
|             "ENGINE": "django.db.backends.oracle", | ||||
|             # ... | ||||
|             "OPTIONS": { | ||||
|                 "pool": { | ||||
|                     "min": 1, | ||||
|                     "max": 10, | ||||
|                     # ... | ||||
|                 } | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
|  | ||||
| .. _`create_pool()`: https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html#connection-pooling | ||||
|  | ||||
| Threaded option | ||||
| --------------- | ||||
|  | ||||
|   | ||||
| @@ -197,6 +197,9 @@ Database backends | ||||
|   instead of ``utf8``, which is an alias for the deprecated character set | ||||
|   ``utf8mb3``. | ||||
|  | ||||
| * Oracle backends now support :ref:`connection pools <oracle-pool>`, by setting | ||||
|   ``"pool"`` in the :setting:`OPTIONS` part of your database configuration. | ||||
|  | ||||
| Decorators | ||||
| ~~~~~~~~~~ | ||||
|  | ||||
| @@ -464,6 +467,9 @@ Miscellaneous | ||||
|   * :meth:`.QuerySet.update_or_create` | ||||
|   * :meth:`.QuerySet.aupdate_or_create` | ||||
|  | ||||
| * The minimum supported version of ``oracledb`` is increased from 1.3.2 to | ||||
|   2.3.0. | ||||
|  | ||||
| .. _deprecated-features-5.2: | ||||
|  | ||||
| Features deprecated in 5.2 | ||||
|   | ||||
| @@ -1,12 +1,28 @@ | ||||
| import copy | ||||
| import unittest | ||||
| from unittest import mock | ||||
|  | ||||
| from django.db import DatabaseError, NotSupportedError, connection | ||||
| from django.core.exceptions import ImproperlyConfigured | ||||
| from django.db import DatabaseError, NotSupportedError, ProgrammingError, connection | ||||
| from django.db.models import BooleanField | ||||
| from django.test import TestCase, TransactionTestCase | ||||
|  | ||||
| from ..models import Square, VeryLongModelNameZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ | ||||
|  | ||||
| try: | ||||
|     from django.db.backends.oracle.oracledb_any import is_oracledb | ||||
| except ImportError: | ||||
|     is_oracledb = False | ||||
|  | ||||
|  | ||||
| def no_pool_connection(alias=None): | ||||
|     new_connection = connection.copy(alias) | ||||
|     new_connection.settings_dict = copy.deepcopy(connection.settings_dict) | ||||
|     # Ensure that the second connection circumvents the pool, this is kind | ||||
|     # of a hack, but we cannot easily change the pool connections. | ||||
|     new_connection.settings_dict["OPTIONS"]["pool"] = False | ||||
|     return new_connection | ||||
|  | ||||
|  | ||||
| @unittest.skipUnless(connection.vendor == "oracle", "Oracle tests") | ||||
| class Tests(TestCase): | ||||
| @@ -69,6 +85,76 @@ class Tests(TestCase): | ||||
|             connection.check_database_version_supported() | ||||
|         self.assertTrue(mocked_get_database_version.called) | ||||
|  | ||||
|     @unittest.skipUnless(is_oracledb, "Pool specific tests") | ||||
|     def test_pool_set_to_true(self): | ||||
|         new_connection = no_pool_connection(alias="default_pool") | ||||
|         new_connection.settings_dict["OPTIONS"]["pool"] = True | ||||
|         try: | ||||
|             self.assertIsNotNone(new_connection.pool) | ||||
|         finally: | ||||
|             new_connection.close_pool() | ||||
|  | ||||
|     @unittest.skipUnless(is_oracledb, "Pool specific tests") | ||||
|     def test_pool_reuse(self): | ||||
|         new_connection = no_pool_connection(alias="default_pool") | ||||
|         new_connection.settings_dict["OPTIONS"]["pool"] = { | ||||
|             "min": 0, | ||||
|             "max": 2, | ||||
|         } | ||||
|         self.assertIsNotNone(new_connection.pool) | ||||
|  | ||||
|         connections = [] | ||||
|  | ||||
|         def get_connection(): | ||||
|             # copy() reuses the existing alias and as such the same pool. | ||||
|             conn = new_connection.copy() | ||||
|             conn.connect() | ||||
|             connections.append(conn) | ||||
|             return conn | ||||
|  | ||||
|         try: | ||||
|             connection_1 = get_connection()  # First connection. | ||||
|             get_connection()  # Get the second connection. | ||||
|             sql = "select sys_context('userenv', 'sid') from dual" | ||||
|             sids = [conn.cursor().execute(sql).fetchone()[0] for conn in connections] | ||||
|             connection_1.close()  # Release back to the pool. | ||||
|             connection_3 = get_connection() | ||||
|             sid = connection_3.cursor().execute(sql).fetchone()[0] | ||||
|             # Reuses the first connection as it is available. | ||||
|             self.assertEqual(sid, sids[0]) | ||||
|         finally: | ||||
|             # Release all connections back to the pool. | ||||
|             for conn in connections: | ||||
|                 conn.close() | ||||
|             new_connection.close_pool() | ||||
|  | ||||
|     @unittest.skipUnless(is_oracledb, "Pool specific tests") | ||||
|     def test_cannot_open_new_connection_in_atomic_block(self): | ||||
|         new_connection = no_pool_connection(alias="default_pool") | ||||
|         new_connection.settings_dict["OPTIONS"]["pool"] = True | ||||
|         msg = "Cannot open a new connection in an atomic block." | ||||
|         new_connection.in_atomic_block = True | ||||
|         new_connection.closed_in_transaction = True | ||||
|         with self.assertRaisesMessage(ProgrammingError, msg): | ||||
|             new_connection.ensure_connection() | ||||
|  | ||||
|     @unittest.skipUnless(is_oracledb, "Pool specific tests") | ||||
|     def test_pooling_not_support_persistent_connections(self): | ||||
|         new_connection = no_pool_connection(alias="default_pool") | ||||
|         new_connection.settings_dict["OPTIONS"]["pool"] = True | ||||
|         new_connection.settings_dict["CONN_MAX_AGE"] = 10 | ||||
|         msg = "Pooling doesn't support persistent connections." | ||||
|         with self.assertRaisesMessage(ImproperlyConfigured, msg): | ||||
|             new_connection.pool | ||||
|  | ||||
|     @unittest.skipIf(is_oracledb, "cx_oracle specific tests") | ||||
|     def test_cx_Oracle_not_support_pooling(self): | ||||
|         new_connection = no_pool_connection() | ||||
|         new_connection.settings_dict["OPTIONS"]["pool"] = True | ||||
|         msg = "Pooling isn't supported by cx_Oracle. Use python-oracledb instead." | ||||
|         with self.assertRaisesMessage(ImproperlyConfigured, msg): | ||||
|             new_connection.connect() | ||||
|  | ||||
|  | ||||
| @unittest.skipUnless(connection.vendor == "oracle", "Oracle tests") | ||||
| class TransactionalTests(TransactionTestCase): | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| oracledb >= 1.3.2 | ||||
| oracledb >= 2.3.0 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user