mirror of
https://github.com/django/django.git
synced 2024-12-22 09:05:43 +00:00
Fixed #7732 -- Added support for connection pools on Oracle.
This commit is contained in:
parent
2249370c86
commit
0d9872fc9a
@ -14,6 +14,7 @@ from django.conf import settings
|
|||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.backends.base.base import BaseDatabaseWrapper
|
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.db.backends.utils import debug_transaction
|
||||||
from django.utils.asyncio import async_unsafe
|
from django.utils.asyncio import async_unsafe
|
||||||
from django.utils.encoding import force_bytes, force_str
|
from django.utils.encoding import force_bytes, force_str
|
||||||
@ -235,6 +236,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||||||
introspection_class = DatabaseIntrospection
|
introspection_class = DatabaseIntrospection
|
||||||
ops_class = DatabaseOperations
|
ops_class = DatabaseOperations
|
||||||
validation_class = DatabaseValidation
|
validation_class = DatabaseValidation
|
||||||
|
_connection_pools = {}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -243,10 +245,52 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||||||
)
|
)
|
||||||
self.features.can_return_columns_from_insert = use_returning_into
|
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):
|
def get_database_version(self):
|
||||||
return self.oracle_version
|
return self.oracle_version
|
||||||
|
|
||||||
def get_connection_params(self):
|
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()
|
conn_params = self.settings_dict["OPTIONS"].copy()
|
||||||
if "use_returning_into" in conn_params:
|
if "use_returning_into" in conn_params:
|
||||||
del conn_params["use_returning_into"]
|
del conn_params["use_returning_into"]
|
||||||
@ -254,6 +298,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||||||
|
|
||||||
@async_unsafe
|
@async_unsafe
|
||||||
def get_new_connection(self, conn_params):
|
def get_new_connection(self, conn_params):
|
||||||
|
if self.pool:
|
||||||
|
return self.pool.acquire()
|
||||||
return Database.connect(
|
return Database.connect(
|
||||||
user=self.settings_dict["USER"],
|
user=self.settings_dict["USER"],
|
||||||
password=self.settings_dict["PASSWORD"],
|
password=self.settings_dict["PASSWORD"],
|
||||||
@ -345,6 +391,12 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||||||
else:
|
else:
|
||||||
return True
|
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
|
@cached_property
|
||||||
def oracle_version(self):
|
def oracle_version(self):
|
||||||
with self.temporary_connection():
|
with self.temporary_connection():
|
||||||
|
@ -205,13 +205,15 @@ class DatabaseCreation(BaseDatabaseCreation):
|
|||||||
Destroy a test database, prompting the user for confirmation if the
|
Destroy a test database, prompting the user for confirmation if the
|
||||||
database already exists. Return the name of the test database created.
|
database already exists. Return the name of the test database created.
|
||||||
"""
|
"""
|
||||||
self.connection.settings_dict["USER"] = self.connection.settings_dict[
|
if not self.connection.is_pool:
|
||||||
"SAVED_USER"
|
self.connection.settings_dict["USER"] = self.connection.settings_dict[
|
||||||
]
|
"SAVED_USER"
|
||||||
self.connection.settings_dict["PASSWORD"] = self.connection.settings_dict[
|
]
|
||||||
"SAVED_PASSWORD"
|
self.connection.settings_dict["PASSWORD"] = self.connection.settings_dict[
|
||||||
]
|
"SAVED_PASSWORD"
|
||||||
|
]
|
||||||
self.connection.close()
|
self.connection.close()
|
||||||
|
self.connection.close_pool()
|
||||||
parameters = self._get_test_db_params()
|
parameters = self._get_test_db_params()
|
||||||
with self._maindb_connection.cursor() as cursor:
|
with self._maindb_connection.cursor() as cursor:
|
||||||
if self._test_user_create():
|
if self._test_user_create():
|
||||||
@ -223,6 +225,7 @@ class DatabaseCreation(BaseDatabaseCreation):
|
|||||||
self.log("Destroying test database tables...")
|
self.log("Destroying test database tables...")
|
||||||
self._execute_test_db_destruction(cursor, parameters, verbosity)
|
self._execute_test_db_destruction(cursor, parameters, verbosity)
|
||||||
self._maindb_connection.close()
|
self._maindb_connection.close()
|
||||||
|
self._maindb_connection.close_pool()
|
||||||
|
|
||||||
def _execute_test_db_creation(self, cursor, parameters, verbosity, keepdb=False):
|
def _execute_test_db_creation(self, cursor, parameters, verbosity, keepdb=False):
|
||||||
if verbosity >= 2:
|
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):
|
if is_oracledb and self.connection.oracledb_version >= (2, 1, 2):
|
||||||
skips.update(
|
skips.update(
|
||||||
{
|
{
|
||||||
|
@ -994,7 +994,7 @@ Oracle notes
|
|||||||
============
|
============
|
||||||
|
|
||||||
Django supports `Oracle Database Server`_ versions 19c and higher. Version
|
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
|
.. deprecated:: 5.0
|
||||||
|
|
||||||
@ -1105,6 +1105,46 @@ Example of a full DSN string::
|
|||||||
"(CONNECT_DATA=(SERVICE_NAME=orclpdb1)))"
|
"(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
|
Threaded option
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
@ -197,6 +197,9 @@ Database backends
|
|||||||
instead of ``utf8``, which is an alias for the deprecated character set
|
instead of ``utf8``, which is an alias for the deprecated character set
|
||||||
``utf8mb3``.
|
``utf8mb3``.
|
||||||
|
|
||||||
|
* Oracle backends now support :ref:`connection pools <oracle-pool>`, by setting
|
||||||
|
``"pool"`` in the :setting:`OPTIONS` part of your database configuration.
|
||||||
|
|
||||||
Decorators
|
Decorators
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
@ -464,6 +467,9 @@ Miscellaneous
|
|||||||
* :meth:`.QuerySet.update_or_create`
|
* :meth:`.QuerySet.update_or_create`
|
||||||
* :meth:`.QuerySet.aupdate_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:
|
.. _deprecated-features-5.2:
|
||||||
|
|
||||||
Features deprecated in 5.2
|
Features deprecated in 5.2
|
||||||
|
@ -1,12 +1,28 @@
|
|||||||
|
import copy
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
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.db.models import BooleanField
|
||||||
from django.test import TestCase, TransactionTestCase
|
from django.test import TestCase, TransactionTestCase
|
||||||
|
|
||||||
from ..models import Square, VeryLongModelNameZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
|
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")
|
@unittest.skipUnless(connection.vendor == "oracle", "Oracle tests")
|
||||||
class Tests(TestCase):
|
class Tests(TestCase):
|
||||||
@ -69,6 +85,76 @@ class Tests(TestCase):
|
|||||||
connection.check_database_version_supported()
|
connection.check_database_version_supported()
|
||||||
self.assertTrue(mocked_get_database_version.called)
|
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")
|
@unittest.skipUnless(connection.vendor == "oracle", "Oracle tests")
|
||||||
class TransactionalTests(TransactionTestCase):
|
class TransactionalTests(TransactionTestCase):
|
||||||
|
@ -1 +1 @@
|
|||||||
oracledb >= 1.3.2
|
oracledb >= 2.3.0
|
||||||
|
Loading…
Reference in New Issue
Block a user