在Django模型中使用UUID作为主键(泛型关系影响)

117

出于多种原因^,我想在一些Django模型中使用UUID作为主键。如果我这样做,我还能够使用"contrib.comments"、"django-voting"或者"django-tagging"等外部应用程序吗?这些应用程序通过ContentType使用通用关系。

以"django-voting"为例,Vote模型如下:

class Vote(models.Model):
    user         = models.ForeignKey(User)
    content_type = models.ForeignKey(ContentType)
    object_id    = models.PositiveIntegerField()
    object       = generic.GenericForeignKey('content_type', 'object_id')
    vote         = models.SmallIntegerField(choices=SCORES)

这个应用程序似乎假定被投票模型的主键是一个整数。

不过内置的评论应用程序似乎能够处理非整数主键:

class BaseCommentAbstractModel(models.Model):
    content_type   = models.ForeignKey(ContentType,
            verbose_name=_('content type'),
            related_name="content_type_set_for_%(class)s")
    object_pk      = models.TextField(_('object ID'))
    content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_pk")

第三方应用程序中是否经常出现“整数主键假定”问题,从而使使用UUID变得麻烦?或者,可能是我误读了这种情况?

有没有一种方法可以在Django中使用UUID作为主键而不会引起太多麻烦?


^ 一些原因:隐藏对象计数,防止URL“ id爬行”,使用多个服务器创建不冲突的对象,...

6个回答

285

如官方文档所述,从 Django 1.8 版本开始,内置了 UUID 字段。使用 UUID 和整数字段相比性能差异可以忽略不计。

import uuid
from django.db import models

class MyUUIDModel(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

您也可以查看这个答案以获取更多信息。


@Keithhackbarth 我们如何设置 Django,在自动创建表格 ID 时始终使用这个? - fIwJlxSzApHEZIl
4
不是很清楚你所说的“every time”具体是什么意思。如果你想要在每个模型中使用UUID,可以创建自己的抽象基础模型并使用它,而不是django.models.Model。 - Назар Топольський
10
只有底层数据库支持UUID类型时,性能差异才是微不足道的。Django仍然在大多数数据库中使用CharField(PostgreSQL是唯一记录支持UUID字段的数据库)。 - NirIzr
4
我有点困惑,为什么这个回答这么受欢迎……问题是在问第三方包的难度。尽管Django本地支持UUID,但似乎仍然有许多第三方包没有考虑到UUIDs。根据我的经验,这很麻烦。 - ambe5960
请注意,这对于将现有模型的主键切换为UUID不起作用。 - infiniteloop

76

UUID作为主键不仅会对通用关系造成问题,而且在效率方面也会有很大的影响:每个外键都比机器字更昂贵——存储和连接速度都会受到影响。

然而,并不要求UUID是主键:只需将其作为辅助键,通过在模型中添加具有unique=True属性的uuid字段即可完成。使用隐式主键正常进行(内部系统),并使用UUID作为外部标识符。


20
无需如此麻烦,你可以将UUID生成函数作为字段的“default”值提供。 - Pi Delport
4
我使用django_extensions.db.fields.UUIDField在我的模型中创建UUID。非常简单,只需像这样定义字段: user_uuid = UUIDField() - mitchf
3
如果你像mitchf提到的那样使用django_extensions.db.fields.UUIDField,那么在Django-South迁移时就不会出现任何问题 - 他提到的字段已经内置支持South迁移。 - Tadeck
177
糟糕的回答。Postgres有本地支持(128位)UUID,在64位机器上只需2个字,因此与本地64位INT相比,成本并不会“显著更高”。 - postfuturist
9
考虑到Piet上有B树索引,针对特定查询会进行多少次比较?不会很多。此外,我相信大多数操作系统会对memcmp调用进行对齐和优化。基于问题的性质,我认为因为可能(很可能可以忽略不计的)性能差异而使用UUID是错误的优化。 - postfuturist
显示剩余8条评论

31

使用UUID作为主键的真正问题在于非数字标识符会带来磁盘碎片和插入降级。因为主键是一个聚集索引(除了PostgreSQL之外的几乎所有关系型数据库管理系统都是如此),当它不是自动递增时,您的数据库引擎将不得不重新排列物理驱动器,以便插入具有较低序数的id行,而用UUID进行id的情况会经常出现。当您的数据库中有大量数据时,可能需要花费许多秒甚至几分钟才能插入一条新记录。并且您的磁盘最终会变得碎片化,需要定期进行磁盘碎片整理。这些都非常糟糕。

为了解决这些问题,我最近想出了以下架构,觉得值得分享。

UUID伪主键

使用此方法可以利用UUID作为主键(使用唯一索引UUID),同时保持自动递增PK以解决具有非数字PK的碎片化和插入性能降级问题。

工作原理:

  1. 在DB模型上创建自动递增主键pkid
  2. 添加索引唯一的UUIDid字段,允许您通过UUID id搜索,而不是数字主键。
  3. 将ForeignKey指向UUID(使用to_field='id'),以便您的外键正确表示伪PK而不是数字ID。

实质上,您需要执行以下操作:

首先,创建一个抽象的Django基础模型。

class UUIDModel(models.Model):
    pkid = models.BigAutoField(primary_key=True, editable=False)
    id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

    class Meta:
        abstract = True

确保扩展基础模型而不是models.Model

class Site(UUIDModel):
    name = models.CharField(max_length=255)

同时确保您的外键指向 UUID 的 id 字段,而不是自动递增的 pkid 字段:

class Page(UUIDModel):
    site = models.ForeignKey(Site, to_field='id', on_delete=models.CASCADE)

如果您正在使用Django Rest Framework(DRF),请确保还创建一个基本的ViewSet类来设置默认搜索字段:

class UUIDModelViewSet(viewsets.ModelViewSet):
    lookup_field = 'id' 

可以扩展基于API视图的扩展模型ViewSet:

class SiteViewSet(UUIDModelViewSet):
    model = Site

class PageViewSet(UUIDModelViewSet):
    model = Page

在这篇文章中,对为什么和如何使用UUID作为Django Rest Framework的主键进行了更多的说明:https://www.stevenmoseley.com/blog/uuid-primary-keys-django-rest-framework-2-steps


6
这是不正确的。Postgres不会按主键在磁盘上对行进行排序。表被分块写入,当添加或更新行时,它被放置在最后一个块的末尾。 - Nicholas E.
1
你的博客文章出现了错误。你忘记了添加 class Meta: abstract=True,所以解决方案无法生效。@steven-moseley https://www.stevenmoseley.com/blog/tech/uuid-primary-keys-django-rest-framework-2-steps - Babak Bandpey
当正确遵循指示时,该解决方案运行良好。我已在许多应用程序中使用它。 - Steven Moseley
你提出的所有观点目前都是正确的,但是一旦https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122bis/发布并且uuid变体7成为一个选项时,这些观点可能会被取消。 - plunker

13

我遇到了类似的情况,并在Django官方文档中发现,object_id与相关模型的primary_key不必是相同类型的。例如,如果您希望通用关系对IntegerFieldCharField id都有效,则只需将您的object_id设置为CharField。由于整数可以转换为字符串,因此没问题。对于UUIDField也是一样。

示例:

class Vote(models.Model):
    user         = models.ForeignKey(User)
    content_type = models.ForeignKey(ContentType)
    object_id    = models.CharField(max_length=50) # <<-- This line was modified 
    object       = generic.GenericForeignKey('content_type', 'object_id')
    vote         = models.SmallIntegerField(choices=SCORES)

你的帖子中有个失效的链接,更新一下会很有意思。干杯! - jlandercy

7

您可以通过使用自定义基础抽象模型来完成此操作,按照以下步骤进行操作。

首先,在您的项目中创建一个名为“basemodel”的文件夹,然后添加一个abstractmodelbase.py文件,并添加以下内容:

from django.db import models
import uuid


class BaseAbstractModel(models.Model):

    """
     This model defines base models that implements common fields like:
     created_at
     updated_at
     is_deleted
    """
    id = models.UUIDField(primary_key=True, unique=True, default=uuid.uuid4, editable=False)
    created_at = models.DateTimeField(auto_now_add=True, editable=False)
    updated_at = models.DateTimeField(auto_now=True, editable=False)
    is_deleted = models.BooleanField(default=False)

    def soft_delete(self):
        """soft  delete a model instance"""
        self.is_deleted=True
        self.save()

    class Meta:
        abstract = True
        ordering = ['-created_at']

第二步:在每个应用程序的模型文件中执行以下操作:

from django.db import models
from basemodel import BaseAbstractModel
import uuid

# Create your models here.

class Incident(BaseAbstractModel):

    """ Incident model  """

    place = models.CharField(max_length=50, blank=False, null=False)
    personal_number = models.CharField(max_length=12, blank=False, null=False)
    description = models.TextField(max_length=500, blank=False, null=False)
    action = models.TextField(max_length=500, blank=True, null=True)
    image = models.ImageField(upload_to='images/', blank=True, null=True)
    incident_date = models.DateTimeField(blank=False, null=False) 

因此,上述模型事件继承了baseabstract模型中的所有字段。


-1
问题可以重新表述为“是否有一种方法让Django在所有表中使用UUID而不是自增整数作为所有数据库ID?”。
当然,我可以这样做:
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

在我的所有表中,我都可以找到一种方法来做到这一点:

  1. 第三方模块
  2. Django生成的ManyToMany表

因此,这似乎是一个缺失的Django功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接