|
1 |
| -import threading |
| 1 | +from contextvars import ContextVar |
2 | 2 | from functools import wraps
|
3 | 3 | from uuid import uuid4
|
4 |
| -from contextvars import ContextVar |
5 |
| - |
6 | 4 | from django.db import connections
|
7 | 5 |
|
8 |
| -THREAD_LOCAL = threading.local() |
9 |
| - |
| 6 | +# Define context variables for read and write database settings |
| 7 | +# These variables will maintain database preferences per context |
10 | 8 | DB_FOR_READ_OVERRIDE = ContextVar('DB_FOR_READ_OVERRIDE', default='default')
|
11 | 9 | DB_FOR_WRITE_OVERRIDE = ContextVar('DB_FOR_WRITE_OVERRIDE', default='default')
|
12 |
| - |
13 | 10 |
|
14 |
| -class DynamicDbRouter(object): |
15 |
| - """A router that decides what db to read from based on a variable |
16 |
| - local to the current thread. |
| 11 | + |
| 12 | +class DynamicDbRouter: |
| 13 | + """ |
| 14 | + A router that dynamically determines which database to perform read and write operations |
| 15 | + on based on the current execution context. It supports both synchronous and asynchronous code. |
17 | 16 | """
|
18 |
| - |
| 17 | + |
19 | 18 | def db_for_read(self, model, **hints):
|
20 | 19 | return DB_FOR_READ_OVERRIDE.get()
|
21 |
| - # return getattr(THREAD_LOCAL, 'DB_FOR_READ_OVERRIDE', ['default'])[-1] |
22 |
| - |
| 20 | + |
23 | 21 | def db_for_write(self, model, **hints):
|
24 | 22 | return DB_FOR_WRITE_OVERRIDE.get()
|
25 |
| - # return getattr(THREAD_LOCAL, 'DB_FOR_WRITE_OVERRIDE', ['default'])[-1] |
26 |
| - |
| 23 | + |
27 | 24 | def allow_relation(self, *args, **kwargs):
|
28 | 25 | return True
|
29 |
| - |
| 26 | + |
30 | 27 | def allow_syncdb(self, *args, **kwargs):
|
31 | 28 | return None
|
32 |
| - |
| 29 | + |
33 | 30 | def allow_migrate(self, *args, **kwargs):
|
34 | 31 | return None
|
35 | 32 |
|
36 |
| - |
37 |
| -class in_database(object): |
38 |
| - """A decorator and context manager to do queries on a given database. |
39 |
| -
|
40 |
| - :type database: str or dict |
41 |
| - :param database: The database to run queries on. A string |
42 |
| - will route through the matching database in |
43 |
| - ``django.conf.settings.DATABASES``. A dictionary will set up a |
44 |
| - connection with the given configuration and route queries to it. |
45 |
| -
|
46 |
| - :type read: bool, optional |
47 |
| - :param read: Controls whether database reads will route through |
48 |
| - the provided database. If ``False``, reads will route through |
49 |
| - the ``'default'`` database. Defaults to ``True``. |
50 |
| -
|
51 |
| - :type write: bool, optional |
52 |
| - :param write: Controls whether database writes will route to |
53 |
| - the provided database. If ``False``, writes will route to |
54 |
| - the ``'default'`` database. Defaults to ``False``. |
55 |
| -
|
56 |
| - When used as eithe a decorator or a context manager, `in_database` |
57 |
| - requires a single argument, which is the name of the database to |
58 |
| - route queries to, or a configuration dictionary for a database to |
59 |
| - route to. |
60 |
| -
|
61 |
| - Usage as a context manager: |
62 |
| -
|
63 |
| - .. code-block:: python |
64 |
| -
|
65 |
| - from my_django_app.utils import tricky_query |
66 |
| -
|
67 |
| - with in_database('Database_A'): |
68 |
| - results = tricky_query() |
69 |
| -
|
70 |
| - Usage as a decorator: |
71 |
| -
|
72 |
| - .. code-block:: python |
73 |
| -
|
74 |
| - from my_django_app.models import Account |
75 |
| -
|
76 |
| - @in_database('Database_B') |
77 |
| - def lowest_id_account(): |
78 |
| - Account.objects.order_by('-id')[0] |
79 |
| -
|
80 |
| - Used with a configuration dictionary: |
81 |
| -
|
82 |
| - .. code-block:: python |
83 |
| -
|
84 |
| - db_config = {'ENGINE': 'django.db.backends.sqlite3', |
85 |
| - 'NAME': 'path/to/mydatabase.db'} |
86 |
| - with in_database(db_config): |
87 |
| - # Run queries |
| 33 | +class in_database: |
| 34 | + """ |
| 35 | + A context manager and decorator for setting a specific database for the duration of a block of code. |
88 | 36 | """
|
89 | 37 | def __init__(self, database, read=True, write=False):
|
90 | 38 | self.read = read
|
91 | 39 | self.write = write
|
92 | 40 | self.database = database
|
93 | 41 | self.created_db_config = False
|
94 |
| - if isinstance(database, dict): |
| 42 | + |
| 43 | + # Handle database parameter either as a string (alias) or as a dict (configuration) |
| 44 | + if isinstance(database, str): |
| 45 | + self.database = database |
| 46 | + elif isinstance(database, dict): |
| 47 | + # If it's a dict, create a unique database configuration |
95 | 48 | self.created_db_config = True
|
96 | 49 | self.unique_db_id = str(uuid4())
|
97 | 50 | connections.databases[self.unique_db_id] = database
|
98 | 51 | self.database = self.unique_db_id
|
99 |
| - |
| 52 | + else: |
| 53 | + raise ValueError("database must be an identifier (str) for an existing db, " |
| 54 | + "or a complete configuration (dict).") |
| 55 | + |
100 | 56 | def __enter__(self):
|
| 57 | + # Capture the current database settings |
101 | 58 | self.original_read_db = DB_FOR_READ_OVERRIDE.get()
|
102 | 59 | self.original_write_db = DB_FOR_WRITE_OVERRIDE.get()
|
| 60 | + |
| 61 | + # Override the database settings for the duration of the context |
103 | 62 | if self.read:
|
104 | 63 | DB_FOR_READ_OVERRIDE.set(self.database)
|
105 | 64 | if self.write:
|
106 | 65 | DB_FOR_WRITE_OVERRIDE.set(self.database)
|
107 | 66 | return self
|
108 |
| - |
| 67 | + |
109 | 68 | def __exit__(self, exc_type, exc_value, traceback):
|
| 69 | + # Restore the original database settings after the context. |
110 | 70 | DB_FOR_READ_OVERRIDE.set(self.original_read_db)
|
111 | 71 | DB_FOR_WRITE_OVERRIDE.set(self.original_write_db)
|
| 72 | + |
| 73 | + # Close and delete created database configuration |
112 | 74 | if self.created_db_config:
|
113 | 75 | connections[self.unique_db_id].close()
|
114 | 76 | del connections.databases[self.unique_db_id]
|
115 |
| - |
| 77 | + |
116 | 78 | def __call__(self, querying_func):
|
| 79 | + # Allow the object to be used as a decorator |
117 | 80 | @wraps(querying_func)
|
118 | 81 | def inner(*args, **kwargs):
|
119 | 82 | with self:
|
|
0 commit comments