Django ORM没有提供默认的分表功能,给访问分表的数据库带来的不变。那么Django分表怎么实现呢?
分析Django ORM
在实现具体的方案之前,我们先看看Django ORM是如何访问数据表的。
在Django中,数据库访问的逻辑基本上是在Queryset中完成的。假设我们有如下一个User Model:
1 2 3 4 5 6 7
| 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_id查询用户数据时,比如:User.objects.filter(user_id=10).first()。
其中的objects就是models.Manager,我们看看Manager的源码:
1 2
| class Manager(BaseManager.from_queryset(QuerySet)): pass
|
from_queryset的源码如下
1 2 3 4 5 6 7 8
| @classmethod def from_queryset(cls, queryset_class, class_name=None): if class_name is None: class_name = '%sFrom%s' % (cls.__name__, queryset_class.__name__) return type(class_name, (cls,), { '_queryset_class': queryset_class, **cls._get_queryset_methods(queryset_class), })
|
这里可以看出Manager是对QuerySet的一个包装。QuerySet是最终要转换为SQL的一个中间层,这其实就是ORM的核心功能,把Model操作转换为SQL语句的部分。
所以当我们调用User.objects()的时候,就已经决定了要访问的数据表了。
实际访问哪个表是由models.Model中的内部类class Meta中的db_table字段决定的。
简单方案-穷举Model类分表
有了上面背景知识,假如我们要根据user_id给user表分三个表。在Django models里如何支持呢?
最直接的思路就是声明三个User Model,然后在Meta类中,指定不同的db_table。在查询的时候,根据user_id返回具体的Model。
实现方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| 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_model_map = { 0: User0, 1: User1, 2: User2, }
table_num = 3
def get_user_db_model(user_id): key = user_id % table_num return user_model_map[key]
|
然后,在实际查询时,先获取user model,再根据user_id查询数据。
1 2 3 4
| def get_user_by_id(user_id): db_model = models.get_user_db_model(user_id) user = db_model.objects.get(user_id=user_id) return model_to_dict(user)
|
这样就可以支持访问分表的数据库了。
但是,聪明的大家都能看出来,要是user分100个表,甚至1000个表的时候怎么办呢?难道定义1000个User类?
Python这么牛逼的语言,怎么可以这么糟蹋呢?下面继续探究一下更好的方法吧。
改进方案-动态创建Model类
既然不通过复制粘贴来定义1000个类,那么就需要动态去创建这1000个类。
学过Java的同学都知道反射,Java的框架和ORM必然是用到反射的,需要动态处理一些类信息。
而Python作为一门动态语言,天生就支持Java 反射里各种黑科技的功能,对,我们不需要反射,原生Python已经自带了。
下面看看在Django里怎样去动态创建我们需要的1000个User类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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
|
嗯,看起来不错,我们已经不需要用复制粘贴来定义1000个类了,而且实现同样的功能代码短了不少。
依然可通过如下的方式获取数据:
1 2
| db_model = User.get_user_db_model(user_id) user = db_model.objects().get(user_id=user_id)
|
但是这里还有优化的空间,因为每次查询我们都创建了一个User Model对象,岂不是很浪费资源,那这些对象可以复用嘛?
改进-动态创建+本地缓存
怎么复用呢?自然是用字典把已经创建过的Model缓存下来,虽然这会消耗一部分本地内存,但为了提高性能还是值得的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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(primary_key=True) user_name = models.CharField(max_length=256) password = models.CharField(max_length=256) class Meta: abstract = True
|
我们定义了一个私有变量_user_model_dict,以table_name作为key,model对象作为值,把已经创建的Model类都缓存下来。
但到了这里还是有点麻烦。
- 这里获取
db_model,在访问数据库时,还是没有直接objects.get()来的方便。
1 2
| db_model = User.get_user_db_model(user_id) user = db_model.objects().get(user_id=user_id)
|
VS
1
| user = User.objects().get(user_id=user_id)
|
- 我们的分表只支持了User一个表,其他表分表还得再写一个
get_xx_db_model,可不可以做的更通用些呢?
改进-Hack Model类的创建
针对上面的问题,可以hack Django Model的创建,在动态创建生成Model类时修改一些属性,把分表需要的信息填充进去。
我们可以创建一个基类来实现动态创建的功能,不仅支持User Model分表,还能方便的让X Model,Y Model通过继承也支持分表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| 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(object): """ 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 = cls.__name__ model_name += '_%s' % shard_key
model_class = cls._shard_db_models.get(model_name) if model_class is not None: return model_class
attrs = dict() attrs.update(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'
|
通过继承ShardModel,User类就具备的自动分表的功能。
在views中可通过如下方法获取user info:
1 2 3 4
| def get_user_info(request): user_id = int(request.GET.get('user_id')) user = models.User(shard_key=user_id).objects.get(user_id=user_id) return HttpResponse(json.dumps(model_to_dict(user)))
|
配置好Django urls,在浏览器访问http://127.0.0.1:8000/user/get?user_id=32,得到下面的结果:
1 2 3 4 5 6
| { "user_name": "test32", "password": "test32", "user_id": 32, "id": 1 }
|
nice, it works!
如果我们有一个Book表也需要分表,这样只需要简单的继承就行了。
1 2 3 4
| class Book(ShardModel): book_id = models.IntegerField() book_name = models.CharField(max_length=256) book_author = models.CharField(max_length=256)
|
这样我们就通过层层深入,不断改进我们的方案,得到了一个比较优雅实现Django分表的方法。