Opened 18 years ago

Last modified 3 months ago

#373 assigned New feature

Add support for multiple-column primary keys

Reported by: Jacob Owned by: Clouds
Component: Database layer (models, ORM) Version: dev
Severity: Normal Keywords: database
Cc: onelson@…, stava@…, erik.engbrecht@…, knyght+django@…, Marinho Brandão, Maciej Bliziński, David Larlet, artagnon, Tobu, tristan@…, blyth, vvinet, Joey Wilhelm, stef@…, wprins, jfishman, alexis_m, Wonlay, joejasinski, hector@…, Martin Paquette, vinilios, broderboy, chouquette, spaceriqui, Marco Bazzani, alexlewin, timbo, Mikhail Korobov, lau@…, rupa108, ludo@…, scott.hebert@…, rogelio.dominguez@…, drdee, davidhalter88@…, Chris Streeter, gezuru@…, andrewford55139@…, garen.p@…, Sergiy Kuzmenko, pawel.suwala@…, cuboci, Ben Finney, kitsunde@…, kkumler, thisgenericname@…, lars@…, trbs@…, cmawebsite@…, Natt Piyapramote, toracle@…, zerks0@…, sky@…, David Sanders, Solomon Ucko, Golan Bar, Michael Schmidt, Damien, Alexandr Artemyev, Charlie Denton, Peter Thomassen, raydeal, אורי, elonzh, Lily Foote Triage Stage: Accepted
Has patch: yes Needs documentation: yes
Needs tests: no Patch needs improvement: yes
Easy pickings: no UI/UX: no

Description

In the interest of being able to use Django as an admin interface for any db schema, it really should support multiple column primary keys.

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.

This is likely to be very difficult.

Attachments (1)

test-m2m (827 bytes) - added by anonymous 12 years ago.
related field in composite

Download all attachments as: .zip

Change History (157)

comment:1 Changed 18 years ago by Adrian Holovaty

Component: Core frameworkMetasystem

comment:2 Changed 17 years ago by Adrian Holovaty

Resolution: wontfix
Status: newclosed

comment:3 Changed 17 years ago by Jacob

Resolution: wontfix
Status: closedreopened

This is something that gets asked about a lot, so I'm going to reopen this ticket as a place to keep track of it.

Personally I don't have time to work on this, but if the LazyWeb can work up a patch, it would be quite nice to have. From an email I wrote a while back, here are the issues (that I'm aware of) that would need to be solved to make this work:

There's three basic problems in dealing with composite primary keys in Django.

The first is that a number of APIs use "obj._meta.pk" to access the primary key field (for example, to do "pk=whatever" lookups). A composite PK implementation would need to emulate this in some way to avoid breaking everything.

Second, a number of things use (content_type_id, object_pk) tuples to refer to some object -- look at the comment framework, or the admin log API. Again, a composite PK system would need to somehow not break this.

Finally, there's the issue of admin URLs; they're of the form "/app_label/module_name/pk/"; there would need to be a way to map URLs to objects in the absence of a primary key.

comment:4 Changed 17 years ago by brantley

It seems to me that multiple primary keys could be represented simply as tuples. The admin could function as: "/app_label/module_name/key1,key2/"

comment:5 Changed 17 years ago by Adrian Holovaty

Brantley: In your URL example, what happens when the key value contains a comma?

comment:6 Changed 17 years ago by anonymous

Adrian: what if in the current setup, a primary key has a "/" in it?

comment:7 Changed 17 years ago by Adrian Holovaty

The current setup works with primary keys with slashes in 'em, because the admin-site regular expression is greedy. Try it yourself -- and if there's a problem, please do let us know.

comment:8 Changed 17 years ago by brantley

I think that might be a fringe case that could be overcome in many ways.

comment:9 Changed 17 years ago by anonymous

It also seems like the admin can't recognize any unique_together foreign key pair.

comment:10 Changed 17 years ago by C8E

What about /app_label/module_name/key1/key2/? That way we can think to access to partial key query /app_label/module_name/key1/, like in the date based generic views.

comment:11 Changed 17 years ago by anonymous

in regards to: "/app_label/module_name/key1,key2/". though not as efficient, primary keys can be text. what happens if they contain a comma, where pk1 = "i do, sometimes" and pk2 = "value of key 2"? /app_label/module_name/i do, sometimes,value of key 2/. or would this not happen?

comment:12 Changed 17 years ago by Adrian Holovaty

Summary: Django lacks support for multiple-column primary keysAdd support for multiple-column primary keys

comment:13 Changed 16 years ago by Gary Wilson <gary.wilson@…>

Triage Stage: UnreviewedDesign decision needed

Seems like the details for this still need to be decided.

comment:14 Changed 16 years ago by buriy <yuri@…>

so why not do escaping for such keys?
Add class ComplexKey...
add some str method or something like this for getting escaped representation of the key.
and what's the best escape sequence for ','... please discuss that without me:)

comment:15 in reply to:  14 Changed 16 years ago by anonymous

Replying to buriy <yuri@buriy.com>:

and what's the best escape sequence for ','

A second comma?

from django.db.vapourware import urlencode_pk
assert urlencode_pk(("ab","c") == "ab,f"
assert urlencode_pk(("a,b","c") == "a,,b,f"
assert urlencode_pk(("a,,b","c") == "a,,,,b,f"
assert urlencode_pk(("i do, sometimes","value of key 2") == "i do,, sometimes,value of key 2"

...although I'm trying to think of a simple RE that would code this, and I can't think of anything that would work.

comment:16 Changed 16 years ago by ilatypov

Does the issue of encoding a composite key into URL prevent from solving this ticket?

To address the URL encoding issue, may I suggest to consider comma an unsafe character when encoding parts of the composite primary key? Then an unprotected comma can serve a clear mark of part separator. According to the URI syntax at W3C web site,

http://www.w3.org/Addressing/URL/5_URI_BNF.html ,

the comma character is allowed to appear unprotected in URIs.

So encoding a composite primary key should be as simple as

pk = [url_encode(pk_part, more_unsafe=",") for pk_part in pk_list]
url = ",".join(pklist)

The major roadblock is still the issue of supporting composite primary keys in the models.

comment:17 Changed 16 years ago by ilatypov

I meant ",".join(pk).

comment:18 Changed 16 years ago by Jacob

Triage Stage: Design decision neededAccepted

comment:19 Changed 16 years ago by David Cramer

Owner: changed from nobody to David Cramer
Status: reopenednew

I'm currently working up some support for this -- the work will depend and need to wait on the queryset refactoring code.

The currently plan is to maintain backwards compatibility on instance._meta.pk, but it will throw an exception if accessed when there are multiple pks, and it should be deprecated. There will be a new attributes, pks on the meta class, which will return a tuple of the primary key fields.

comment:20 Changed 16 years ago by David Cramer

Status: newassigned

comment:21 in reply to:  description Changed 15 years ago by ulvinge@…

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

comment:22 Changed 15 years ago by anonymous

Thanks, exactly what i was looking for.

Chris, B.
Phim Online

comment:23 Changed 15 years ago by Tom Carrick <knyght@…>

Cc: knyght+django@… added

comment:24 in reply to:  description Changed 15 years ago by Ravi Gidwani <Ravi.Gidwani@…>

Replying to jacob:

In the interest of being able to use Django as an admin interface for any db schema, it really should support multiple column primary keys.

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.

This is likely to be very difficult.

################################################################################################
# Classes for models that need a composite key. Django as of 0.96 does not support composite keys
# To have composite keys effect/workarround in your model follows the steps:
# Step1: Paste the below classes in your models.py (before your composite models)
# Step2: Extend you class from CompositeKeyModel
# Step3: Define 'unique_together=(("field1","field2","foreignkey__field"),) in your models Meta class
# Step4: additionally you can define unique index on the database
#
# Thats it! The save() on the instances of these models should have the composite key effect.
#
# Send feedback to: Ravi.Gidwani@gmail.com
##################################################################################################

class CompositeKeyModel(models.Model): pass

class IntermediateModelBase(ModelBase):
  def __new__(cls, name, bases, attrs):
    if CompositeKeyModel in bases:
      if bases == (CompositeKeyModel,):
        # create the class via the super method
        newclass = ModelBase.__new__(ModelBase, name, (models.Model,), attrs)
        # but then make it inherit from our model class
        newclass.__bases__ = (CompositeKeyModel,)
        return newclass
      else: raise Exception, "IntermediateModelBase does not support more than one base"
    else:
      return type.__new__(cls, name, bases, attrs)


class CompositeKeyModel(models.Model):
    __metaclass__ = IntermediateModelBase
    def save(self):
         filter = {}
         #contruct model filter on the fly for the fields that listed in the
         # 'unique_together' meta attribute
         for field_name_list in self._meta.unique_together:
             for field in field_name_list:
                 filter[field]='%s' % (self.getFieldValue(field))     

         #use the generated filter to check if the object already exist
         # if so fetch it
         fetched = self.__class__.objects.complex_filter(filter)
         #if fetched, then get its primary key and set it into the object instance
         #that is being saved.
         if(len(fetched) > 0):
             pk = self.getPrimaryKey()
             self.__setattr__(pk.name,fetched[0].__getattribute__(pk.name))
    
         #finally call the super class i.e Model save() method   
         models.Model.save(self)
         
    def getFieldValue(self,fieldName):
        separator = fieldName.find('__')
        if(separator > -1): 
            tokens = fieldName.partition('__')
            foriegnObj = getattr(self, tokens[0])
            return getattr(foriegnObj,tokens[2])
        else:
            return getattr(self, fieldName)

    def getPrimaryKey(self):                                
        for field in self._meta.fields:
            if (field.primary_key):
                return field
        
        raise Exception('No primary key')
         
#################################################################################################         

comment:25 Changed 15 years ago by David Cramer

Neat hack and all, but we should stick to the topic on trac.

comment:26 Changed 15 years ago by Ravi Gidwani <Ravi.Gidwani@…>

I agree, but I am sure it can be useful to someone,like me, until this bug is fixed. What say ?

comment:27 Changed 15 years ago by mrts

milestone: post-1.0

Non-essential for 1.0.

comment:28 Changed 15 years ago by David Cramer

Status: assignednew

I don't currently have time to reimplement this in qs-rf so if someone else wants to volunteer go for it.

comment:29 Changed 15 years ago by David Cramer

Status: newassigned

I'm going to be working up a patch in the next few days, please see http://groups.google.com/group/django-developers/browse_thread/thread/4b2370a0652d9135 for discussion.

comment:30 Changed 15 years ago by Marinho Brandão

Cc: Marinho Brandão added

comment:31 Changed 15 years ago by Adrian Holovaty

Component: MetasystemDatabase wrapper

comment:32 Changed 14 years ago by Dan Crosta

Cc: Dan Crosta added

Is the intention to add support only for composite primary keys? What about composite, non-unique indices?

#5805 was closed as a duplicate of this ticket, but I think the intention there was different. If the fix to this ticket will not add non-unique, composite indexing to Django's model API, I suggest re-opening #5805.

comment:33 Changed 14 years ago by David Cramer

I don't want this to go back and forth as a patch, simply due to how large the changeset is, but I've forked the "unofficial django git repo" and put my patch there: http://github.com/dcramer/django-compositepks/tree/master#

So far it allows declaration and creation of primary key models. You declare them in class Meta: primary_key = ('field', 'field', 'field').

There is no ForeignKey/Relational handlers as of right now, but the admin is mostly working.

comment:34 Changed 14 years ago by Maciej Bliziński

Cc: Maciej Bliziński added

comment:35 Changed 14 years ago by David Larlet

Cc: David Larlet added

comment:36 Changed 14 years ago by (none)

milestone: post-1.0

Milestone post-1.0 deleted

comment:37 Changed 14 years ago by artagnon

Cc: artagnon added

comment:38 Changed 14 years ago by Tobu

Cc: Tobu added

comment:39 Changed 14 years ago by anonymous

Cc: tristan@… added

comment:40 Changed 14 years ago by blyth

Cc: blyth added

comment:41 in reply to:  32 Changed 14 years ago by fongandrew

Replying to dcrosta:
Second that. This appears to add primary-key support only. It would be nice to have non-unique composite-key indexing. It would be nice for things like the generic relations in the contenttypes app for instance.

Suggested syntax:

class MyModel(models.Model):
    field_a = models.IntegerField()
    field_b = models.IntegerField()

    class Meta:
        index_together = (('field_a', 'field_b'),)

comment:42 Changed 14 years ago by bohney

Correct me if I'm wrong, but this seems to be a serious issue for those of us that are using a pre-existing database managed by a DBA that won't change the schema for the table(s) with a Composite Primary Key.

comment:43 Changed 14 years ago by eengbrec

Cc: erik.engbrecht@… added

comment:44 Changed 14 years ago by vvinet

Cc: vvinet added

comment:45 Changed 14 years ago by Joey Wilhelm

Cc: Joey Wilhelm added

+1 on the generic multi-column field. To add to the complexity of the issue (or perhaps this is a different issue?) I have a DBA who has begun implementing multi-part foreign keys, and I would love to see these implemented as well.

Of course, the multi-part primary key is by far a higher priority for me, as I have quite a large number of tables using these, and this renders the Django admin essentially unusable.

comment:46 Changed 14 years ago by anonymous

Cc: stava@… added

comment:47 Changed 13 years ago by Stef Walter <stef@…>

Cc: stef@… added

comment:48 Changed 13 years ago by wprins

Cc: wprins added

comment:49 Changed 13 years ago by jfishman

Cc: jfishman added

+1

comment:50 Changed 13 years ago by anonymous

Cc: alexis_m added

comment:51 Changed 13 years ago by Wonlay

Cc: Wonlay added

comment:52 Changed 13 years ago by joejasinski

Cc: joejasinski added

comment:53 Changed 13 years ago by Hector Lecuanda

Cc: hector@… added

comment:54 Changed 13 years ago by Martin Paquette

Cc: Martin Paquette added

comment:55 Changed 13 years ago by anonymous

Cc: vinilios added

comment:56 Changed 13 years ago by Oroku Saki

Sorry to be a rail bird (ie, I'm not skilled enough to help), but what is the rough ETA for this solution?

comment:57 Changed 13 years ago by broderboy

Cc: broderboy added

comment:58 Changed 13 years ago by chouquette

Cc: chouquette added

comment:59 Changed 13 years ago by spaceriqui

Cc: spaceriqui added

comment:60 Changed 13 years ago by Marco Bazzani

Cc: Marco Bazzani added

comment:61 Changed 13 years ago by unho

I was very excited about using Django to develop a new web app, but this particular issue makes impossible to use Django for developing it.

What we need is a way to define a primary key for a model which consist on two foreign keys, one from another model and a second from another different model than the first two I mentioned. Of course we will be referring to the first model (the one with a multi-part primary key made of two foreign keys) from a fourth model, in other words, the fourth model has a foreign key pointing to the first model.

I hope you can fix this for other projects since we won't use Django for developing our app.

comment:62 Changed 13 years ago by unho

And of course it is also needed for several to several relationships which have extra attributes, because they also have compound primary keys that also are foreign keys.

comment:63 Changed 13 years ago by alexlewin

Cc: alexlewin added

comment:64 Changed 13 years ago by timbo

Cc: timbo added

comment:65 Changed 13 years ago by mikmani

Lack of support for composite primary key fields is a major drawback of Django and should be dealt with as a priority. Most applications use databases with tables having composite primary keys. Such applications cannot be migrated to Django which reduces Django's usability.

comment:66 Changed 12 years ago by anonymous

Cc: onelson@… added

comment:67 Changed 12 years ago by anonymous

Cc: Mikhail Korobov added

comment:68 Changed 12 years ago by pjroberts

I'd like to add my voice to those that want this. If you need a linking table between two tables in a many-to-many relationship, it seems absurd to have to add a redundant integer primary key to an already-unique row.

comment:69 Changed 12 years ago by Mike Fogel

Cc: Mike Fogel added

comment:70 Changed 12 years ago by anonymous

Cc: lau@… added

comment:71 Changed 12 years ago by Thomas Güttler

Cc: hv@… added

comment:72 Changed 12 years ago by rupa108

Cc: rupa108 added

comment:73 Changed 12 years ago by Ludovico Magnocavallo

Cc: ludo@… added

comment:74 Changed 12 years ago by Erik Allik

Cc: eallik@… added

comment:75 Changed 12 years ago by Łukasz Rekucki

Severity: normalNormal
Type: defectNew feature

comment:76 Changed 12 years ago by slaptijack

Cc: scott.hebert@… added

comment:77 Changed 12 years ago by Antti Kaihola

Easy pickings: unset

Adding support for composite fields and primary keys into the Django model layer seems to be part of Google's Summer of Code 2011 (see http://www.djangoproject.com/weblog/2011/apr/25/gsoc/ announcement on the Django weblog).

comment:78 Changed 12 years ago by akseli.palen@…

I really hope this feature will be implemented as soon as possible..

comment:79 Changed 12 years ago by rogelio.dominguez@…

Cc: rogelio.dominguez@… added

comment:80 Changed 12 years ago by Michal Petrucha

Owner: changed from David Cramer to Michal Petrucha
Status: assignednew

I'll try to post progress updates here from time to time.

I just kicked off a thread on django-developers@ to discuss the final API (https://groups.google.com/d/topic/django-developers/rF79c8z65cQ/discussion). I'll appreciate comments from anyone interested. (I'm posting a note in here because I realize not everyone reads the mailing list.)

comment:81 Changed 12 years ago by Michal Petrucha

Status: newassigned

comment:82 Changed 12 years ago by Erik Allik

Cc: eallik@… removed

comment:83 Changed 12 years ago by drdee

Cc: drdee added

comment:84 Changed 12 years ago by davidhalter88@…

Cc: davidhalter88@… added
UI/UX: unset

comment:85 Changed 12 years ago by Michal Petrucha

Okay, a progress update:
there's a branch on github: https://github.com/koniiiik/django/

Currently, you can define a composite field, set it as the primary key and basically that's it. No support in ForeignKey or any other relationship fields yet, that's what I'll work on starting with the next week. Model validation is missing as well, i. e. CompositeFields are not yet checked for correctness.

The admin should just work, try to explore its functionality and please let me know if you find something broken. I tried to test everything I saw but of course, I may have forgotten about a few details.

All in all, feel free to clone the repo and try it out, any feedback I get will push me forward.

comment:86 Changed 12 years ago by Chris Streeter

Cc: Chris Streeter added

comment:87 Changed 12 years ago by gezuru@…

Cc: gezuru@… added

Changed 12 years ago by anonymous

Attachment: test-m2m added

related field in composite

comment:88 Changed 12 years ago by anonymous

I've tried using this when one of the component fields is a relationship field, but it doesn't seem to work. I might be doing it wrong. I've attached a small example that can be applied on top of koniiiik's existing tests.

comment:89 Changed 12 years ago by Dan Crosta

Cc: Dan Crosta removed

comment:90 Changed 12 years ago by anonymous

Ignore test-m2m for the time being, I didn't mean the FK target to have a composite PK. I'll try to get a useful test case out of my real model tomorrow.

comment:92 Changed 12 years ago by Andrew J Ford

Cc: andrewford55139@… added

comment:93 Changed 12 years ago by garen.p@…

Cc: garen.p@… added

comment:94 Changed 12 years ago by Erik Allik

Why am I getting notifications about this issue even though I'm not in the CC? Or at least Trac only allows me to Add myself, not Remove.

comment:95 Changed 12 years ago by Erik Allik

Cc: eallik@… added

comment:96 Changed 12 years ago by Erik Allik

Cc: eallik@… removed

comment:97 Changed 11 years ago by Sergiy Kuzmenko

Cc: Sergiy Kuzmenko added

comment:98 Changed 11 years ago by Erik Allik

From now on I'm reporting code.djangoproject.com e-mails as spam as removing myself from CC has no effect whatsoever.

comment:99 Changed 11 years ago by Aymeric Augustin

(off-topic for this ticket)

RaceCondition, given the comment you left on ticket 16763, it seems that you're aware of the situation. Whenever you contribute to a ticket, you will be notified of further updates by email.

We don't intend to change this behavior because many contributors rely on it. However, we have a manual procedure to remove people who no longer want to receive these emails. Just follow the instructions!.

And of course, don't post while logged-in, or your email will be added to the CC list again. As an alternative, I suggest posting without logging in and signing your messages.

comment:100 Changed 11 years ago by Erik Allik

So, um, what is then the purpose of the CC list if you get messages anyway?
Basically, still, code.djangoproject.com is sending out spam; it's irrelevant if it's a feature for some people.

comment:101 Changed 11 years ago by pawel.suwala@…

Cc: pawel.suwala@… added

comment:102 Changed 11 years ago by Lau Bech Lauritzen

We're still a bunch of hopeful people holding our breaths for this feature to enter django. What's the status? Looks like there's lots of activity on github, is the code at a state where it's reasonably stable? In which case I'd be happy to start testing on some real-life data.

comment:103 Changed 11 years ago by Michal Petrucha

Well, the current status is, the soc2011/composite-fields branch is stable and I'm just keeping it up-to-date with master. There is some documentation, though I'm afraid it is not complete. Anyway, it only works with non-relationship fields.

As for relationship fields and the auxiliary_fk_fields branch, it is still only in the refactoring phase. I had to modify ForeignKey extensively and it's still not there but I'm trying to fix the bugs one by one. Once the whole test suite succeeds (except for CompositeField tests) I can resume working on composite fields themselves.

Note that this is quite a tedious process since most of the remaining failing test cases require individual handling, therefore I'll appreciate any help with this. Feel free to fork me on github and send pull requests. I don't always find enough free time to work on this myself, I've still got some exams to do and also a Bc. thesis to work on...

comment:104 Changed 11 years ago by Pawel Suwala <pawel.suwala@…>

I have bugfixed and ported the CompositeKey base class posted by Ravi (4 years ago!) to work with current django version.

Here comes the snippet for people who can't wait for real-ck support in django.

################################################################################################
# Base Class for weak-emulation of composite primary key. 
# To have composite keys workaround in your model, follow these steps: 
# Step1: Paste this snippet before your models in models.py
# Step2: Extend you class from CompositeKeyModel
# Step3: Define 'unique_together=(("field1","field2","field3"),) in your models Meta class
# Step4: Create db-table by hand.
#
# That's it! The save() on the instances of these models should have the composite key effect.
#
# Send feedback to: pawel.suwala@fsfe.org
##################################################################################################
from django.db import models
from django.db.models.base import ModelBase

class CompositeKeyModel(models.Model): pass

class IntermediateModelBase(ModelBase):
  def __new__(cls, name, bases, attrs):
    if CompositeKeyModel in bases:
      if bases == (CompositeKeyModel,):
        # create the class via the super method
        newclass = ModelBase.__new__(ModelBase, name, (models.Model,), attrs)
        # but then make it inherit from our model class
        newclass.__bases__ = (CompositeKeyModel,)
        return newclass
      else: raise Exception, "IntermediateModelBase does not support more than one base"
    else:
      return type.__new__(cls, name, bases, attrs)


class CompositeKeyModel(models.Model):
    __metaclass__ = IntermediateModelBase
    def save(self, *args, **kwargs):
         filter = {}
         #contruct model filter on the fly for the fields that listed in the
         # 'unique_together' meta attribute

         if not self._meta.unique_together:
             raise ValueError('Specify Meta.unique_together to emulate Composite pk')

         for field_name_list in self._meta.unique_together:
             for field in field_name_list:
                 filter[field]='%s' % (self.get_field_value(field))

         #use the generated filter to check if the object already exist
         # if so fetch it
         fetched = self.__class__.objects.complex_filter(filter)
         #if fetched, then get its primary key and set it into the object instance
         #that is being saved.
         if(len(fetched) > 0):
             pk = self.get_primary_key()
             self.__setattr__(pk.name,fetched[0].__getattribute__(pk.name))

         #finally call the super class i.e Model save() method
         models.Model.save(self, *args, **kwargs)

    def _is_foreign_key(self, field): # there must be a better way to do this
        meta_class = getattr(field.__class__, '__metaclass__', None)
        return meta_class == ModelBase

    def get_field_value(self, fieldName):
        field_value = getattr(self, fieldName)
        if self._is_foreign_key(field_value):
            return field_value.pk
        return field_value

    def get_primary_key(self):
        for field in self._meta.fields:
            if (field.primary_key):
                return field

        raise Exception('Your model must have a dummy primary key (id)')
#################################################################################################
## example:
#class MyModel(CompositeKeyModel):
#    id                 = models.AutoField(primary_key=True) # dummy-for-django
#    user               = models.ForeignKey(User)
#    mess               = models.ForeignKey(Message)
#    atta               = models.ForeignKey(Attachment)
#
#    class Meta:
#        unique_together = (('user', 'mess', 'atta'),)
#
# -- You will need to make this table by hand. For postgresql:
#CREATE TABLE app_mymodel (
#    id integer NOT NULL, -- dummy, NO PrimaryKey CONSTRAINT
#    user_id integer NOT NULL, -- Foreign key to some table
#    mess_id integer NOT NULL, -- Foreign key to some table
#    atta_id integer NOT NULL  -- Foreign key to some table
#);

# -- create sequence for ID field (like MySQL auto-increment, but without pk)
# CREATE SEQUENCE app_mymodel_id_seq
#    START WITH 1
#    INCREMENT BY 1
#    NO MINVALUE
#    NO MAXVALUE
#    CACHE 1;
# ALTER SEQUENCE app_mymodel_id_seq OWNED BY app_mymodel.id;

# -- Add real composite Primary Key
# ALTER TABLE ONLY app_mymodel
# ADD CONSTRAINT comp_key PRIMARY KEY (user_id, mess_id, atta_id);


>>> MyModel.objects.create(user_id=1, mess_id=5, atta_id=7)
<MyModel: 1_5_7>
>>> MyModel.objects.create(user_id=1, mess_id=5, atta_id=7)
IntegrityError: duplicate key value violates unique constraint
DETAIL:  Key (user_id, mess_id, atta_id)=(1, 8383, 1321) already exists.

comment:105 Changed 11 years ago by cuboci

Cc: cuboci added

comment:106 Changed 11 years ago by Ben Finney

Cc: Ben Finney added

comment:107 Changed 11 years ago by Kit Sunde

Cc: kitsunde@… added

comment:108 Changed 10 years ago by kkumler

Cc: kkumler added

comment:109 Changed 10 years ago by thisgenericname@…

Cc: thisgenericname@… added

comment:110 Changed 10 years ago by thisgenericname@…

From skimming over the above comments, it seems that one of the biggest issues is the representation of composite primary keys in URLs and in references to model.pk

How feasible would it be to have a set of model methods that:

1) Given a set of primary key fields, return a string representation of the composite key with the requirement that this mapping must be 1:1

2) Given a string representation of a primary key, return some sort of structure that identifies what fields should be at which values... Essentially the inverse of the above.

1 allows the generation of URLs, and allows things that examine model.pk to continue to function.

2 creates the framework necessary to support queries that dereference the pk, including lookups and possibly foreign keys.

The one requirement is that the composite-key-to-string bit must produce output that has only one possible interpretation... e.g does escaping of commas/slashes/whatever as needed. In the vast majority of cases using integer ids, composites can probably be in the form of 1.5 or 1-5 or such and escaping is a non-issue.

comment:111 Changed 10 years ago by Michal Petrucha

Not at all, this is an issue that has been brought up years ago and it simply ignited the biggest discussions because this is one of the easiest things about this project and practically anyone can give their opinions on that. Personally I spent about a few hours thinking about the string representation, most of which was spent reading the comments on this ticket and then concluding that this is not really an issue.

More precisely, to implement a working string representation suitable for the admin, this is just about everything you need: https://github.com/koniiiik/django/blob/soc2011/composite-fields/django/db/models/fields/composite.py#L151 and https://github.com/koniiiik/django/blob/soc2011/composite-fields/django/db/models/fields/composite.py#L135

There are much more difficult aspects. The work is currently stalled on a refactor of ForeignKey and friends. I haven't had much time to work on this lately because it is the end of semester here and school's been keeping me busy but I still intend to work on this when I get a bit of free time. Unless someone wants to take over in which case I would be glad to assist in any way I can.

However, one more thing about the string representation, there is one nontrivial issue that hasn't been discussed in this ticket at all – GenericForeignKey. Using GenericForeignKey you can create SQL joins between the model containing a GFK and an arbitrary model. The value of a GFK pointing to a composite primary key should probably be a string, at least I don't have any better ideas. However, to be able to create a SQL join between the GFK and the composite field, there needs to be a way to reproduce the string encoding of the composite value in SQL. Something like

blog_posts.objects.annotate(Max('comments__date'))

should generate roughly this SQL query:

SELECT blog_posts.date, blog_posts.title, blog_posts.content, MAX(comments.date)
FROM blog_posts INNER JOIN comments ON (comments.object_id = PERFORM_ESCAPE(blog_posts.date, blog_posts.title))
WHERE comments.content_type = '''the content type of blog_posts'''
GROUP BY (blog_posts.date, blog_posts.title)

The important bit is PERFORM_ESCAPE(blog_posts.date, blog_posts.title) which should probably be a rather complex SQL expression that takes all the constituents of a composite field, turns them into strings, escapes them appropriately and generates one output string out of them. This expression will necessarily be strongly backend-dependent but the encoding should also be consistent across backends.

The bad thing about this is that we need to get this right on the first try. Once we release this feature with a certain encoding, we'll have to stick to that because of backwards compatibility, we can't require all users of this feature to migrate their data each time we change the encoding. With admin URLs this is not an issue as changing the string representation there would just result in slightly different URLs but the admin would still work just fine.

Actually, the last sentence is not entirely true with my current implementation because the admin also uses parts of the contenttypes framework to store the log of admin activity and currently I put the same value in there as the one used in the URL, which means that if I changed the representation, all of my logged admin activity would point to non-existing objects.

comment:112 Changed 10 years ago by jeremyt

Where does this issue stand on the Django timeline? Have we settled on a solution (I like the approach of the composite field koniiiik has done). I would like to see a primary_key option being placed in the Meta class as well.

comment:113 Changed 10 years ago by Michal Petrucha

Adding a primary_key option to Meta would be redundant, both the current API for single-column PKs and the proposed API for composite PKs use the primary_key boolean flag on a Field instance. And as far as I understand, attempts to add a new Meta option are usually turned down by the core team (similar to project-level settings). However, if you have good arguments, feel free to state them and this may still be considered.

As far as the progress goes, there is no timeline. The work is stalled once again for lack of time (school and other projects requiring immediate attention) but if the core team agrees, I'd like to take this to the final stages this summer as part of GSoC.

comment:114 Changed 10 years ago by jeremyt

I think the primary_key option in the Meta makes since from the ease of use/flow prospective. I'm not saying that we should remove the option from the field either but it should only be specified in one space. Just like a CREATE TABLE statement really. If I have a primary key of a single column, I can specify it on that one column directly but if I have multiple, I place the primary key explanation as a constraint and not a column/field itself.

In the end, the model would be the same except the user doesn't have to manually declare the composite field(they still can if they want to though), but system would generate the composite field for them and make it the pk. But if the primary_key is missing everywhere, it continues to create the AutoField pk just as it does today.

Does this disagree with the current schema?

comment:115 Changed 10 years ago by Michal Petrucha

Well… The Meta.primary_key option was the first proposed API and the outcome of this discussion was that the BDFL disliked this option and preferred an explicit CompositeField. The arguments for either option were mostly a matter of personal preference, therefore we settled on the one chosen by Jacob.

Personally, I don't really care. I can deliver the code for either one, there's not that much of a difference as far as the implementation is concerned. I'll start a thread on django-developers@ in the upcoming weeks regarding the status of this project and my plans, that could be a good opportunity for you to bring this up. Just keep in mind that this feature has a history of attracting lots of bikeshedding discussions while the more serious architectural issues are mostly being ignored. To me, this smells more like the former. So, whatever the final decision on django-developers@ is, I'll just accept it and write it that way.

comment:116 Changed 10 years ago by jeremyt

Yeah.. the ideas on this have been plentiful as shown by that thread. The optional primary_key Meta option definitely goes with the DRY principle that Django is based upon but as long as the scenario is enabled, then I'm ok with spelling it out every time.

This patch plus the work I've done on multi-column joins https://code.djangoproject.com/ticket/19385 should open up some major use cases. I lend a helping hand to this issue, as well, to handle any concerns.

comment:117 Changed 10 years ago by Andrew J Ford

Jeremy makes a good point about placing primary_key as a constraint within Meta. However, for the interest of time IMO, sticking with the current plan is the best path. Adding primary_key to Meta later as an afterthought (assuming the rest of the community finds a need for it) should be simple enough without breaking (future) existing code.

What is the difference between the auxillary_fk_fields and soc2011/composite-fields branches? How often is master merged? Obviously, this isn't a simple change. Maybe the community could help out a little if they had some status of what they could do to help.

comment:118 Changed 10 years ago by Michal Petrucha

I apologize for the lack of timely response.

#19385 looks like it implements the last big piece that is missing from my work. I'll need to have a more thorough look at it though.

The current status of my work is kinda sad. The repo is badly out of date because it has become a real pain to keep it in sync with master. I won't get into much detail about why here, but I think it isn't worth the effort to try to sync it up. Basically, the problem is that I first implemented CompositeField (in branch soc2011/composite-fields) without ForeignKey support and then I started to refactor ForeignKey on top of that (auxiliary_fk_fields). This has become a real nightmare to maintain. I managed to get the branches through the initial Py3k changes but that required carefully merging each upstream commit one by one, after that there were numerous cleanups in the ORM which I didn't get around to merging.

However, that doesn't mean I'm giving up, I've got other plans. The idea is to do it the other way around, first refactor ForeignKey and only after it works with simple fields, port CompositeField on top of it. This should still make it possible to reuse most of the code I wrote throughout the past two years and make the changes easier to follow.

I'd like to apply for the upcoming Google Summer of Code with this to finally put this to rest, so keep an eye out on django-developers@ for an email in the near future.

comment:119 Changed 9 years ago by lars@…

Cc: lars@… added

comment:120 Changed 9 years ago by trbs

Cc: trbs@… added

comment:121 Changed 8 years ago by Collin Anderson

Cc: cmawebsite@… added

comment:122 Changed 8 years ago by Aron Podrigal

Cc: Aron Podrigal added

comment:123 Changed 8 years ago by Natt Piyapramote

Cc: Natt Piyapramote added

comment:124 Changed 8 years ago by Jeongsoo, Park

Cc: toracle@… added

comment:125 Changed 8 years ago by Asif Saifuddin Auvi

these are the draft dep to implement composite primary key on django orm

https://github.com/django/deps/blob/master/draft/0191-composite-fields.rst

and

https://github.com/django/deps/blob/master/draft/0192-standalone-composite-fields.rst

anyone interested to completed this feature could have some look on them.

comment:126 Changed 7 years ago by Asif Saifuddin Auvi

comment:127 Changed 7 years ago by Asif Saifuddin Auvi

Version: master

comment:128 Changed 7 years ago by Thomas Güttler

Cc: hv@… removed

comment:129 Changed 6 years ago by Asif Saifuddin Auvi

As per conversation in IRC the existing Deps should also be revised based on other works and also https://gist.github.com/koniiiik/5408673

comment:130 Changed 6 years ago by Asif Saifuddin Auvi

#16508 could be related

Last edited 6 years ago by Tim Graham (previous) (diff)

comment:131 Changed 6 years ago by Fernando Gutiérrez

Cc: zerks0@… added

comment:132 Changed 6 years ago by Schuyler Duveen

Cc: sky@… added

comment:133 Changed 6 years ago by David Sanders

Cc: David Sanders added

comment:134 Changed 4 years ago by Solomon Ucko

Cc: Solomon Ucko added

It's been about 13 years since this issue was created and about 8 years since work on it has begun, and it still hasn't been implemented! Does the basic functionality at least work? FWIW, AFAIK the main motivation for this feature is "relationship tables" (i.e. ones that link 2 or more tables together), which is relatively common. Also, a better API might be allowing primary_key to be set on multiple fields. For my use case, I don't need to reference the set of columns directly from the database, just in the view logic. (The item specified by the URL and the current user are my two primary keys.) I could just use unique_together, but I'd rather not unnecessarily add an extra column. It's not that much of a problem, but I'd prefer not to need to.

comment:135 Changed 4 years ago by Golan Bar

Cc: Golan Bar added

comment:136 Changed 4 years ago by Asif Saifuddin Auvi

Owner: Michal Petrucha deleted
Status: assignednew

comment:137 Changed 4 years ago by Michael Schmidt

Cc: Michael Schmidt added
Owner: set to Michael Schmidt
Status: newassigned

comment:138 Changed 2 years ago by Damien

Cc: Damien added

comment:139 Changed 2 years ago by Thomas van Gulick

Cc: Thomas van Gulick added

comment:142 Changed 21 months ago by Alexandr Artemyev

Cc: Alexandr Artemyev added

comment:143 Changed 20 months ago by Charlie Denton

Cc: Charlie Denton added

comment:144 Changed 20 months ago by Thomas van Gulick

Cc: Thomas van Gulick removed

comment:145 Changed 20 months ago by Mike Fogel

Cc: Mike Fogel removed

comment:146 Changed 20 months ago by Aron Podrigal

Cc: Aron Podrigal removed

comment:147 Changed 18 months ago by Mariusz Felisiak

Owner: Michael Schmidt deleted

comment:148 Changed 18 months ago by Mariusz Felisiak

Status: assignednew

comment:149 Changed 16 months ago by Peter Thomassen

Cc: Peter Thomassen added

comment:150 Changed 15 months ago by raydeal

Cc: raydeal added
Owner: set to raydeal
Status: newassigned

I would like to dive into this issue and see how can I help.

comment:151 Changed 15 months ago by אורי

Cc: אורי added

comment:152 Changed 15 months ago by raydeal

I am digging in Dj ORM code and trying understand how FK works underneath.

comment:153 Changed 14 months ago by raydeal

Owner: raydeal deleted
Status: assignednew

comment:154 Changed 14 months ago by elonzh

Cc: elonzh added

comment:156 Changed 6 months ago by Mariusz Felisiak

Needs documentation: set
Owner: set to Clouds
Patch needs improvement: set
Status: newassigned

comment:157 Changed 6 months ago by Clouds

Hello, the main work of CompoisteField has been done and I'm now working on primary_together/PrimaryKeyConstraint.
While I've got several questions:

  1. How to write test for migration? (or witch existing test as example)
    1. test if the model generating correct models.State
    2. test if from OldModel to NewModel would success migrated
  2. Shall we have PrimaryKeyConstraint and CompositeField in one PR?
    1. primary (without multicolumn) first, then composite
    2. composite (cannot set as PK) first, then primary

The main blocker split it into 2 PR might be hard to add meaningful tests in first PR.

comment:158 Changed 3 months ago by Lily Foote

Cc: Lily Foote added
Note: See TracTickets for help on using tickets.
Back to Top