#373: Add support for multiple-column primary keys
------------------------+---------------------------------------------------
   Reporter:  jacob     |                Owner:  dcramer   
     Status:  assigned  |            Component:  Metasystem
    Version:            |           Resolution:            
   Keywords:  database  |                Stage:  Accepted  
  Has_patch:  0         |           Needs_docs:  0         
Needs_tests:  0         |   Needs_better_patch:  0         
------------------------+---------------------------------------------------
Comment (by [EMAIL PROTECTED]):

 Sorry if this isn't the right place to post this, but I'm a noob... I've
 only used Python and Django for a week...

 > Currently, you can "fake" it by declaring one of the keys to be primary
 in Django and adding a unique constraint to the model.  You'd have to do
 without the auto-generated SQL that Django gives you, but if you're smart
 enough to know how and why to use multiple primary keys, you can probably
 write the schema by hand.  Still, this is less than ideal.
 >

 No you really can't "fake" it... You can't even with custom sql do it.

 The problem is in the save function, because when you save an object it
 overrides all values with the declared primary key.

 An example:

 {{{
 class book():
     shelf  = IntegerField(primary_key=True)
     level  = IntegerField()
     index  = IntegerField()

     class Meta:
         unique_together = ('shelf', 'level', 'index')
     # don't mind the following line right now
     primary = ('shelf', 'level', 'index')
 }}}

 Currently you can only have one book per shelf with the current django
 code... (I actually use 0.96, but I don't think it has changed...)

 That was the problem, now to the solution! (Note, as I said, I'm a noob,
 and I just hacked an afternoon and came up with this)

 It needs to have a field called primary specified as shown in the above
 model.


 {{{
 import django.db.models.manipulators
 import django.db.models.manager
 from django.core import validators
 from django.core.exceptions import ObjectDoesNotExist
 from django.db.models.fields import AutoField, ImageField,
 FieldDoesNotExist
 from django.db.models.fields.related import OneToOneRel, ManyToOneRel
 from django.db.models.query import delete_objects
 from django.db.models.options import Options, AdminOptions
 from django.db import connection, backend, transaction, models
 from django.db.models import signals
 from django.db.models.loading import register_models, get_model
 from django.dispatch import dispatcher
 from django.utils.datastructures import SortedDict
 from django.utils.functional import curry
 from django.conf import settings
 from itertools import izip
 import types
 import sys
 import os

 class Model(models.Model):
     def save(self):
         dispatcher.send(signal=signals.pre_save, sender=self.__class__,
 instance=self)

         non_pks = [f for f in self._meta.fields if not (f.primary_key or
 (f.name in self.primary))]
         otpk = [f for f in self._meta.fields if ((not f.primary_key) and
 (f.name in self.primary))]
         where_and_clause = ''
         for f in otpk:
             where_and_clause = where_and_clause.join(' AND %s=%s ' % \
                 (backend.quote_name(f.attname),
                  str(getattr(self, f.attname))))
         # TODO: the last value in the above % thingy is probbably
 unsafe...

         cursor = connection.cursor()

         # First, try an UPDATE. If that doesn't update anything, do an
 INSERT.
         pk_val = self._get_pk_val()
         pk_set = bool(pk_val)
         record_exists = True
         if pk_set:
             # Determine whether a record with the primary key already
 exists.
             cursor.execute("SELECT 1 FROM %s WHERE %s=%%s %s LIMIT 1" % \
                 (backend.quote_name(self._meta.db_table),
                  backend.quote_name(self._meta.pk.column),
                  where_and_clause), [pk_val])
             # If it does already exist, do an UPDATE.
             if cursor.fetchone():
                 db_values = [f.get_db_prep_save(f.pre_save(self, False))
 for f in non_pks]
                 if db_values:
                     cursor.execute("UPDATE %s SET %s WHERE %s=%%s %s" % \
                         (backend.quote_name(self._meta.db_table),
                         ','.join(['%s=%%s' % backend.quote_name(f.column)
 for f in non_pks]),
                         backend.quote_name(self._meta.pk.column),
                         where_and_clause),
                         db_values + [pk_val])
             else:
                 record_exists = False
         if not pk_set or not record_exists:
             field_names = [backend.quote_name(f.column) for f in
 self._meta.fields if not isinstance(f, AutoField)]
             db_values = [f.get_db_prep_save(f.pre_save(self, True)) for f
 in self._meta.fields if not isinstance(f, AutoField)]
             # If the PK has been manually set, respect that.
             if pk_set:
                 field_names += [f.column for f in self._meta.fields if
 isinstance(f, AutoField)]
                 db_values += [f.get_db_prep_save(f.pre_save(self, True))
 for f in self._meta.fields if isinstance(f, AutoField)]
             placeholders = ['%s'] * len(field_names)
             if self._meta.order_with_respect_to:
                 field_names.append(backend.quote_name('_order'))
                 # TODO: This assumes the database supports subqueries.
                 placeholders.append('(SELECT COUNT(*) FROM %s WHERE %s =
 %%s %s)' % \
                     (backend.quote_name(self._meta.db_table),
 backend.quote_name(self._meta.order_with_respect_to.column),
 where_and_clause))
                 db_values.append(getattr(self,
 self._meta.order_with_respect_to.attname))
             if db_values:
                 cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % \
                     (backend.quote_name(self._meta.db_table),
 ','.join(field_names),
                     ','.join(placeholders)), db_values)
             else:
                 # Create a new record with defaults for everything.
                 cursor.execute("INSERT INTO %s (%s) VALUES (%s)" %
                     (backend.quote_name(self._meta.db_table),
                      backend.quote_name(self._meta.pk.column),
                      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()

         # Run any post-save hooks.
         dispatcher.send(signal=signals.post_save, sender=self.__class__,
 instance=self)

 }}}

 As some will see, this is a copy and paste of the original save function,
 but with additional constraints in the where clause everywhere.

 Just import and inherit from this class where you have multiple primary
 keys and everything will work.

 Also you need some sql like this: (for postgres 8.2)

 {{{
 ALTER TABLE app_book DROP CONSTRAINT app_book_pkey;
 ALTER TABLE app_book ADD CONSTRAINT app_book_pkey PRIMARY KEY ("shelf",
 "level", "index")
 }}}

 This is a fix that works now for the impatient (like me), but I would
 really have it work natively, without this hackish code, so keep the good
 work up dcramer!

 Happy programming wishes Niklas Ulvinge

-- 
Ticket URL: <http://code.djangoproject.com/ticket/373#comment:21>
Django Code <http://code.djangoproject.com/>
The web framework for perfectionists with deadlines
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to