74

I have a model like this:

class Hop(models.Model):
    migration = models.ForeignKey('Migration')
    host = models.ForeignKey(User, related_name='host_set')

How can I have the primary key be the combination of migration and host?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Vahid Kharazi
  • 5,723
  • 17
  • 60
  • 103
  • 1
    possible duplicate http://stackoverflow.com/questions/4871966/make-primary-key-with-2-fields-in-django – Paulo Bu May 28 '13 at 20:11

4 Answers4

114

Update Django 4.0

Django 4.0 documentation recommends using UniqueConstraint with the constraints option instead of unique_together.

Use UniqueConstraint with the constraints option instead.

UniqueConstraint provides more functionality than unique_together. unique_together may be deprecated in the future.

class Hop(models.Model):
    migration = models.ForeignKey('Migration')
    host = models.ForeignKey(User, related_name='host_set')

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=['migration', 'host'], name='unique_migration_host_combination'
            )
        ]

Original Answer

I would implement this slightly differently.

I would use a default primary key (auto field), and use the meta class property, unique_together

class Hop(models.Model):
    migration = models.ForeignKey('Migration')
    host = models.ForeignKey(User, related_name='host_set')

    class Meta:
        unique_together = (("migration", "host"),)

It would act as a "surrogate" primary key column.

If you really want to create a multi-column primary key, look into this app

vlj614
  • 153
  • 2
  • 6
karthikr
  • 97,368
  • 26
  • 197
  • 188
  • If I use unique_together, does it mean I don't have to have a field set as the primary key? – Connor Pearson Jan 15 '14 at 19:18
  • 5
    If you dont specify an `AutoField`, django would add one for you. So in short, you dont. – karthikr Jan 15 '14 at 19:23
  • 5
    Is this still up to date in 2018 with Django 2? – NaturalBornCamper Mar 27 '18 at 01:28
  • 3
    You'd implement this differently, but you fail to mention that you'd do that because Django does not support the primary key being multiple fields – firelynx Oct 05 '18 at 08:04
  • If you do as above on legacy database (with `managed=False`), you should invoke save methods with `force_insert=True` in order to have them working properly. Otherwise Django will update one row instead of adding new record. – maciek Apr 08 '19 at 13:51
  • @NaturalBornCamper it is still open in Django 2. Discussion & ticket: https://code.djangoproject.com/wiki/MultipleColumnPrimaryKeys, https://code.djangoproject.com/ticket/373 – Kangur Jul 24 '19 at 10:48
  • 1
    There was [an edit suggestion](https://stackoverflow.com/review/suggested-edits/31705961) for Django 4.0. Was it OK or not? – Peter Mortensen May 07 '22 at 12:43
28

Currently, Django models only support a single-column primary key. If you don't specify primary_key = True for the field in your model, Django will automatically create a column id as a primary key.

The attribute unique_together in class Meta is only a constraint for your data.

Winston
  • 601
  • 1
  • 9
  • 29
Hùng Ng Vi
  • 1,251
  • 2
  • 14
  • 20
4

If you should use Django on a legacy database, you can't modify db_schema.

There is a workaround (ugly) method to fix this issue:

Override the models save or delete function:

# Use a raw SQL statement to save or delete the object

class BaseModel(models.Model):

    def get_max_length_unique_key(self):
        max_len_unique_key = []
        for unique_key in self._meta.unique_together:
            if len(unique_key) > len(max_len_unique_key):
                max_len_unique_key = unique_key
        return max_len_unique_key

    def get_db_conn(self):
        db_cnn = DbManage(db_ip, db_port, DATABASES_USER, DATABASES_PASSWORD, self._meta.db_table)
        db_cnn.connect()
        return db_cnn

    def save(self, *args, **kwargs):
        self.delete()
        cnn, databasename = self.get_db_conn()
        update_tables = self._meta.db_table
        key_list = ""
        values_list = ""
        for field in self._meta.fields:
            key_list += "%s," % field.name
            values_list += "\'%s\'," % str(getattr(self, field.name))

        key_list = key_list[:len(key_list) - 1]
        values_list = values_list[:len(values_list) - 1]

        sql = "insert into %s(%s) values(%s)" % (update_tables, key_list, values_list)
        logger.info("insert new record to %s" % databasename)
        cnn.excute_sql(sql)
        cnn.close()

    def delete(self, *args, **kwargs):
        cnn = self.get_db_conn()
        update_tables = self._meta.db_table
        sql = "delete from %s where " % update_tables
        for uk in self.get_max_length_unique_key():
            sql += "%s=\'%s\' and " % (uk, getattr(self, uk))
        sql = sql[:len(sql) - 4]

        logger.info("delete record from %s" % update_tables)
        cnn.excute_sql(sql)
        cnn.close()
        pass

    class Meta:
        abstract = True

class ImageList(BaseModel):

    field1 = models.CharField(primary_key=True, max_length=30)
    field2 = models.CharField(primary_key=True, max_length=30)
    field3 = models.CharField(primary_key=True, max_length=30)
    body = models.CharField(max_length=2000, blank=True, null=True)
    updated_on = models.DateTimeField(blank=True, null=True)

    class Meta:
        managed = True
        db_table = 'image_list'
        unique_together = (('field1', 'field2', 'field3'),)
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
pulord
  • 57
  • 4
2

Since the latest version of Django (4.0.4 at this time) suggests that the unique_together may be deprecated in the future, a more up-to-date solution would be to use the constraints option of the Meta class together with the UniqueConstraint class:

class Hop(models.Model):
    migration = models.ForeignKey('Migration')
    host = models.ForeignKey(User, related_name='host_set')

    class Meta:
        constraints = [
            UniqueConstraint(fields=['migration', 'host'], name='unique_host_migration'),
        ]
u09
  • 449
  • 1
  • 3
  • 11