From 75b64abaa7cc1818aec9c29c28d93e226ec3b1cb Mon Sep 17 00:00:00 2001 From: Jason Pellerin Date: Wed, 28 Jun 2006 22:21:45 +0000 Subject: [PATCH] [multi-db] Added optional db_connection property to model Meta classes, which can be used to set the name of the connection for the model. Updated query generation in model, query and fields/related to use the model's connection and backend. Added basic model connection access to multiple db test. git-svn-id: http://code.djangoproject.com/svn/django/branches/multiple-db-support@3234 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/db/models/base.py | 22 ++++++++-- django/db/models/fields/related.py | 16 ++++--- django/db/models/options.py | 15 ++++++- django/db/models/query.py | 46 +++++++++++++++---- tests/othertests/multiple_databases.py | 61 +++++++++++++++++++++++++- 5 files changed, 138 insertions(+), 22 deletions(-) diff --git a/django/db/models/base.py b/django/db/models/base.py index 09e8eb3fc0..883ed7c81a 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -7,7 +7,7 @@ from django.db.models.fields.related import OneToOneRel, ManyToOneRel from django.db.models.related import RelatedObject from django.db.models.query import orderlist2sql, delete_objects from django.db.models.options import Options, AdminOptions -from django.db import connection, backend, transaction +from django.db import transaction from django.db.models import signals from django.db.models.loading import register_models, get_model from django.dispatch import dispatcher @@ -158,6 +158,9 @@ class Model(object): def save(self): dispatcher.send(signal=signals.pre_save, sender=self.__class__, instance=self) + info = self._meta.connection_info + connection = info.connection + backend = info.backend non_pks = [f for f in self._meta.fields if not f.primary_key] cursor = connection.cursor() @@ -205,7 +208,7 @@ class Model(object): backend.get_pk_default_value())) if self._meta.has_auto_field and not pk_set: setattr(self, self._meta.pk.attname, backend.get_last_insert_id(cursor, self._meta.db_table, self._meta.pk.column)) - transaction.commit_unless_managed() + transaction.commit_unless_managed([connection]) # Run any post-save hooks. dispatcher.send(signal=signals.post_save, sender=self.__class__, instance=self) @@ -276,6 +279,7 @@ class Model(object): return dict(field.choices).get(value, value) def _get_next_or_previous_by_FIELD(self, field, is_next, **kwargs): + backend = self._meta.connection_info.backend op = is_next and '>' or '<' where = '(%s %s %%s OR (%s = %%s AND %s.%s %s %%s))' % \ (backend.quote_name(field.column), op, backend.quote_name(field.column), @@ -290,6 +294,7 @@ class Model(object): raise self.DoesNotExist, "%s matching query does not exist." % self.__class__._meta.object_name def _get_next_or_previous_in_order(self, is_next): + backend = self._meta.connection_info.backend cachename = "__%s_order_cache" % is_next if not hasattr(self, cachename): op = is_next and '>' or '<' @@ -378,6 +383,9 @@ class Model(object): rel = rel_field.rel.to m2m_table = rel_field.m2m_db_table() this_id = self._get_pk_val() + info = self._meta.connection_info + connection = info.connection + backend = info.backend cursor = connection.cursor() cursor.execute("DELETE FROM %s WHERE %s = %%s" % \ (backend.quote_name(m2m_table), @@ -387,7 +395,7 @@ class Model(object): backend.quote_name(rel_field.m2m_column_name()), backend.quote_name(rel_field.m2m_reverse_name())) cursor.executemany(sql, [(this_id, i) for i in id_list]) - transaction.commit_unless_managed() + transaction.commit_unless_managed([connection]) ############################################ # HELPER FUNCTIONS (CURRIED MODEL METHODS) # @@ -396,6 +404,9 @@ class Model(object): # ORDERING METHODS ######################### def method_set_order(ordered_obj, self, id_list): + connection_info = ordered_obj._meta.connection_info + connection = info.connection + backend = info.backend cursor = connection.cursor() # Example: "UPDATE poll_choices SET _order = %s WHERE poll_id = %s AND id = %s" sql = "UPDATE %s SET %s = %%s WHERE %s = %%s AND %s = %%s" % \ @@ -404,9 +415,12 @@ def method_set_order(ordered_obj, self, id_list): backend.quote_name(ordered_obj._meta.pk.column)) rel_val = getattr(self, ordered_obj._meta.order_with_respect_to.rel.field_name) cursor.executemany(sql, [(i, rel_val, j) for i, j in enumerate(id_list)]) - transaction.commit_unless_managed() + transaction.commit_unless_managed([connection]) def method_get_order(ordered_obj, self): + connection_info = ordered_obj.connection_info + connection = info.connection + backend = info.backend cursor = connection.cursor() # Example: "SELECT id FROM poll_choices WHERE poll_id = %s ORDER BY _order" sql = "SELECT %s FROM %s WHERE %s = %%s ORDER BY %s" % \ diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 5ee1aec022..f7ca012351 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1,4 +1,4 @@ -from django.db import backend, connection, transaction +from django.db import transaction from django.db.models import signals, get_model from django.db.models.fields import AutoField, Field, IntegerField, get_ul_class from django.db.models.related import RelatedObject @@ -290,7 +290,7 @@ def create_many_related_manager(superclass): # source_col_name: the PK colname in join_table for the source object # target_col_name: the PK colname in join_table for the target object # *objs - objects to add - from django.db import connection + connection = self.model._meta.connection # Add the newly created or already existing objects to the join table. # First find out which items are already added, to avoid adding them twice @@ -310,13 +310,13 @@ def create_many_related_manager(superclass): cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ (self.join_table, source_col_name, target_col_name), [self._pk_val, obj_id]) - transaction.commit_unless_managed() + transaction.commit_unless_managed(connection) def _remove_items(self, source_col_name, target_col_name, *objs): # source_col_name: the PK colname in join_table for the source object # target_col_name: the PK colname in join_table for the target object # *objs - objects to remove - from django.db import connection + connection = self.model._meta.connection for obj in objs: if not isinstance(obj, self.model): @@ -327,16 +327,16 @@ def create_many_related_manager(superclass): cursor.execute("DELETE FROM %s WHERE %s = %%s AND %s = %%s" % \ (self.join_table, source_col_name, target_col_name), [self._pk_val, obj._get_pk_val()]) - transaction.commit_unless_managed() + transaction.commit_unless_managed(connection) def _clear_items(self, source_col_name): # source_col_name: the PK colname in join_table for the source object - from django.db import connection + connection = self.model._meta.connection cursor = connection.cursor() cursor.execute("DELETE FROM %s WHERE %s = %%s" % \ (self.join_table, source_col_name), [self._pk_val]) - transaction.commit_unless_managed() + transaction.commit_unless_managed(connection) return ManyRelatedManager @@ -360,6 +360,7 @@ class ManyRelatedObjectsDescriptor(object): superclass = rel_model._default_manager.__class__ RelatedManager = create_many_related_manager(superclass) + backend = rel_model._meta.connection_info.backend qn = backend.quote_name manager = RelatedManager( model=rel_model, @@ -402,6 +403,7 @@ class ReverseManyRelatedObjectsDescriptor(object): superclass = rel_model._default_manager.__class__ RelatedManager = create_many_related_manager(superclass) + backend = rel_model._meta.connection_info.backend qn = backend.quote_name manager = RelatedManager( model=rel_model, diff --git a/django/db/models/options.py b/django/db/models/options.py index 46a321f170..818bb62441 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.db import connection_info, connections from django.db.models.related import RelatedObject from django.db.models.fields.related import ManyToManyRel from django.db.models.fields import AutoField, FieldDoesNotExist @@ -11,7 +12,7 @@ import re # Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces". get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip() -DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering', +DEFAULT_NAMES = ('verbose_name', 'db_connection', 'db_table', 'ordering', 'unique_together', 'permissions', 'get_latest_by', 'order_with_respect_to', 'app_label') @@ -20,6 +21,7 @@ class Options(object): self.fields, self.many_to_many = [], [] self.module_name, self.verbose_name = None, None self.verbose_name_plural = None + self.db_connection = None self.db_table = '' self.ordering = [] self.unique_together = [] @@ -90,6 +92,17 @@ class Options(object): def __str__(self): return "%s.%s" % (self.app_label, self.module_name) + + def get_connection_info(self): + if self.db_connection: + return connections[self.db_connection] + return connection_info + connection_info = property(get_connection_info) + + def get_connection(self): + """Get the database connection for this object's model""" + return self.get_connection_info().connection + connection = property(get_connection) def get_field(self, name, many_to_many=True): "Returns the requested field by name. Raises FieldDoesNotExist on error." diff --git a/django/db/models/query.py b/django/db/models/query.py index efbe68cb66..8efdcd11e1 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1,4 +1,4 @@ -from django.db import backend, connection, transaction +from django.db import transaction from django.db.models.fields import DateField, FieldDoesNotExist from django.db.models import signals from django.dispatch import dispatcher @@ -157,8 +157,7 @@ class QuerySet(object): # self._select is a dictionary, and dictionaries' key order is # undefined, so we convert it to a list of tuples. extra_select = self._select.items() - - cursor = connection.cursor() + cursor = self.model._meta.connection.cursor() select, sql, params = self._get_sql_clause() cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params) fill_cache = self._select_related @@ -178,6 +177,9 @@ class QuerySet(object): def count(self): "Performs a SELECT COUNT() and returns the number of records as an integer." + info = self.model._meta.connection_info + backend = info.backend + connection = info.connection counter = self._clone() counter._order_by = () counter._offset = None @@ -247,6 +249,7 @@ class QuerySet(object): Returns a dictionary mapping each of the given IDs to the object with that ID. """ + backend = self.model._meta.connection_info.backend assert self._limit is None and self._offset is None, \ "Cannot use 'limit' or 'offset' with in_bulk" assert isinstance(id_list, (tuple, list)), "in_bulk() must be provided with a list of IDs." @@ -423,7 +426,7 @@ class QuerySet(object): def _get_sql_clause(self): opts = self.model._meta - + backend = opts.connection_info.backend # Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z. select = ["%s.%s" % (backend.quote_name(opts.db_table), backend.quote_name(f.column)) for f in opts.fields] tables = [quote_only_if_word(t) for t in self._tables] @@ -514,6 +517,9 @@ class ValuesQuerySet(QuerySet): columns = [f.column for f in self.model._meta.fields] field_names = [f.attname for f in self.model._meta.fields] + info = self.model._meta.connection_info + backend = info.backend + connection = info.connection cursor = connection.cursor() select, sql, params = self._get_sql_clause() select = ['%s.%s' % (backend.quote_name(self.model._meta.db_table), backend.quote_name(c)) for c in columns] @@ -533,6 +539,10 @@ class ValuesQuerySet(QuerySet): class DateQuerySet(QuerySet): def iterator(self): from django.db.backends.util import typecast_timestamp + info = self.model._meta.connection_info + backend = info.backend + connection = info.connection + self._order_by = () # Clear this because it'll mess things up otherwise. if self._field.null: self._where.append('%s.%s IS NOT NULL' % \ @@ -625,7 +635,9 @@ class QNot(Q): where2 = ['(NOT (%s))' % " AND ".join(where)] return tables, joins, where2, params -def get_where_clause(lookup_type, table_prefix, field_name, value): +def get_where_clause(opts, lookup_type, table_prefix, field_name, value): + backend = opts.connection_info.backend + if table_prefix.endswith('.'): table_prefix = backend.quote_name(table_prefix[:-1])+'.' field_name = backend.quote_name(field_name) @@ -660,6 +672,7 @@ def fill_table_cache(opts, select, tables, where, old_prefix, cache_tables_seen) Helper function that recursively populates the select, tables and where (in place) for fill-cache queries. """ + backend = opts.connection_info.backend for f in opts.fields: if f.rel and not f.null: db_table = f.rel.to._meta.db_table @@ -753,6 +766,9 @@ def lookup_inner(path, clause, value, opts, table, column): current_column = column intermediate_table = None join_required = False + info = current_opts.connection_info + backend = info.backend + connection = info.connection name = path.pop(0) # Has the primary key been requested? If so, expand it out @@ -881,7 +897,7 @@ def lookup_inner(path, clause, value, opts, table, column): else: column = field.column - where.append(get_where_clause(clause, current_table + '.', column, value)) + where.append(get_where_clause(current_opts, clause, current_table + '.', column, value)) params.extend(field.get_db_prep_lookup(clause, value)) return tables, joins, where, params @@ -891,9 +907,12 @@ def delete_objects(seen_objs): ordered_classes = seen_objs.keys() ordered_classes.reverse() - cursor = connection.cursor() - for cls in ordered_classes: + info = cls._meta.connection_info + backend = info.backend + connection = info.connection + cursor = connection.cursor() + seen_objs[cls] = seen_objs[cls].items() seen_objs[cls].sort() @@ -927,7 +946,16 @@ def delete_objects(seen_objs): pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]) # Now delete the actual data + dirty_conns = [] for cls in ordered_classes: + + info = cls._meta.connection_info + backend = info.backend + connection = info.connection + cursor = connection.cursor() + if connection not in dirty_conns: + dirty_conns.append(connection) + seen_objs[cls].reverse() pk_list = [pk for pk,instance in seen_objs[cls]] for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): @@ -947,4 +975,4 @@ def delete_objects(seen_objs): setattr(instance, cls._meta.pk.attname, None) dispatcher.send(signal=signals.post_delete, sender=cls, instance=instance) - transaction.commit_unless_managed() + transaction.commit_unless_managed(dirty_conns) diff --git a/tests/othertests/multiple_databases.py b/tests/othertests/multiple_databases.py index 4e0919ae8f..be52757406 100644 --- a/tests/othertests/multiple_databases.py +++ b/tests/othertests/multiple_databases.py @@ -50,7 +50,7 @@ with the attributes: `DatabaseError`, `backend`, `runshell`. Access connections through the `connections` property of the `django.db` module: - >>> from django.db import connections + >>> from django.db import connection, connections >>> connections['a'].settings.DATABASE_NAME == db_a True >>> connections['b'].settings.DATABASE_NAME == db_b @@ -62,6 +62,65 @@ Invalid connection names raise ImproperlyConfigured: Traceback (most recent call last): ... ImproperlyConfigured: No database connection 'bad' has been configured + +Models can define which connection to use, by name. To use a named +connection, set the `db_connection` property in the model's Meta class +to the name of the connection. The name used must be a key in +settings.DATABASES, of course. + + >>> from django.db import models + >>> class Artist(models.Model): + ... name = models.CharField(maxlength=100) + ... alive = models.BooleanField(default=True) + ... + ... def __str__(self): + ... return self.name + ... + ... class Meta: + ... app_label = 'mdb' + ... db_connection = 'a' + ... + >>> class Widget(models.Model): + ... code = models.CharField(maxlength=10, unique=True) + ... weight = models.IntegerField() + ... + ... def __str__(self): + ... return self.code + ... + ... class Meta: + ... app_label = 'mdb' + ... db_connection = 'b' + +But they don't have to. Multiple database support is entirely optional +and has no impact on your application if you don't use it. + + >>> class Vehicle(models.Model): + ... make = models.CharField(maxlength=20) + ... model = models.CharField(maxlength=20) + ... year = models.IntegerField() + ... + ... def __str__(self): + ... return "%d %s %s" % (self.year, self.make, self.model) + ... + ... class Meta: + ... app_label = 'mdb' + + >>> Artist._meta.connection.settings.DATABASE_NAME == \ + ... connections['a'].connection.settings.DATABASE_NAME + True + >>> Widget._meta.connection.settings.DATABASE_NAME == \ + ... connections['b'].connection.settings.DATABASE_NAME + True + >>> Vehicle._meta.connection.settings.DATABASE_NAME == \ + ... connection.settings.DATABASE_NAME + True + >>> Artist._meta.connection.settings.DATABASE_NAME == \ + ... Widget._meta.connection.settings.DATABASE_NAME + False + >>> Artist._meta.connection.settings.DATABASE_NAME == \ + ... Vehicle._meta.connection.settings.DATABASE_NAME + False + """ def cleanup():