Django ORM 不内置“分表”,但可以通过动态 Model 实现。

核心思路:让同一个逻辑 Model 映射到不同物理表

1. ORM 是怎么确定表名的

Django 的查询链路大致是:

  • ModelManagerQuerySet → SQL
  • 表名由 Model.Meta.db_table 决定

示例:

class User(models.Model):
    user_id = models.IntegerField()
    user_name = models.CharField(max_length=256)
    password = models.CharField(max_length=256)

    class Meta:
        db_table = "user"

这意味着,当你写:

User.objects.filter(user_id=10).first()

实际上已经固定了访问的表名。

2. 方案一:穷举 Model(不推荐)

把同一逻辑表拆成多个 Model:

class User(models.Model):
    user_id = models.IntegerField(primary_key=True)
    user_name = models.CharField(max_length=256)
    password = models.CharField(max_length=256)

class User0(User):
    class Meta:
        db_table = "user_0"

class User1(User):
    class Meta:
        db_table = "user_1"

class User2(User):
    class Meta:
        db_table = "user_2"

再根据 user_id 路由:

user_model_map = {0: User0, 1: User1, 2: User2}
TABLE_NUM = 3

def get_user_db_model(user_id):
    return user_model_map[user_id % TABLE_NUM]

问题:分 1000 张表就要写 1000 个类,显然不现实。

3. 方案二:动态创建 Model(推荐)

核心是:运行时生成 Model + 缓存

from django.db import models

TABLE_NUM = 3

class User(models.Model):
    @classmethod
    def get_user_db_model(cls, user_id=None):
        suffix = user_id % TABLE_NUM
        table_name = "user_%s" % suffix
        if table_name in cls._user_model_dict:
            return cls._user_model_dict[table_name]

        class Meta:
            db_table = table_name

        attrs = {
            "__module__": cls.__module__,
            "Meta": Meta,
        }

        user_db_model = type(str("User_%s" % suffix), (cls,), attrs)
        cls._user_model_dict[table_name] = user_db_model
        return user_db_model

    _user_model_dict = {}
    user_id = models.IntegerField()
    user_name = models.CharField(max_length=256)
    password = models.CharField(max_length=256)

    class Meta:
        abstract = True

使用方式:

db_model = User.get_user_db_model(user_id)
user = db_model.objects.get(user_id=user_id)

这比复制粘贴强很多,但还是不够优雅。

4. 方案三:封装为通用基类

目标:让任意 Model 继承后即可分表。

from django.db import models

class Object:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

def _model_new(cls, *args, **kwargs):
    return cls(*args, **kwargs)

class ShardModel:
    """
    ShardModel support table horizontal partition.
    """

    _shard_db_models = {}

    def __new__(cls, *args, **kwargs):
        shard_key = kwargs.pop("shard_key", 0) % cls.Config.table_num
        model_name = f"{cls.__name__}_{shard_key}"

        model_class = cls._shard_db_models.get(model_name)
        if model_class is not None:
            return model_class

        attrs = dict(cls.__dict__)
        if "objects" in attrs:
            attrs["objects"] = attrs["objects"].__class__()

        meta = Object(**cls.Meta.__dict__)
        meta.db_table = meta.db_table % shard_key

        attrs["Meta"] = meta
        attrs["new"] = classmethod(_model_new)

        model_class = type(model_name, tuple([models.Model] + list(cls.__bases__[1:])), attrs)
        cls._shard_db_models[model_name] = model_class
        return model_class

class User(ShardModel):
    user_id = models.IntegerField()
    user_name = models.CharField(max_length=256)
    password = models.CharField(max_length=256)

    class Config:
        table_num = 3

    class Meta:
        app_label = "default"
        db_table = "user_%s"

调用:

user = User(shard_key=user_id).objects.get(user_id=user_id)

5. 注意事项

  • 迁移时要确保所有分表都存在。
  • 分表后,跨表统计会更复杂,建议先在业务侧聚合。
  • 分表不是银弹,优先评估缓存、索引与读写分离。

小结

从“穷举 Model”到“动态生成 + 缓存”,再到“通用基类”,分表的可维护性会大幅提升。