75

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?

1

4 Answers 4

119

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

7
  • If I use unique_together, does it mean I don't have to have a field set as the primary key? Jan 15, 2014 at 19:18
  • 5
    If you dont specify an AutoField, django would add one for you. So in short, you dont.
    – karthikr
    Jan 15, 2014 at 19:23
  • 5
    Is this still up to date in 2018 with Django 2? Mar 27, 2018 at 1: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 5, 2018 at 8:04
  • 1
    There was an edit suggestion for Django 4.0. Was it OK or not? May 7, 2022 at 12:43
30

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.

1
  • THIS is very important information. The reason for having control over the primary key is to make it reproducible, e.g. that a partial database restore still points to the correct foreign keys. Mar 15 at 9:56
6

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'),)
0
3

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'),
        ]

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.