mysql 数据库优化之 django分表方案

由来

知乎上的一个问题:Django 分表 怎么实现?

这个问题戳到了Django ORM的痛点,对于多数据库/分库的问题,Django提供了很好的支持,通过using和db router可以很好的完成多数据库的操作。但是说到分表的问题,就有点不那么友好了。但也不是那么难处理,只是处理起来不太优雅。

解析

在Django中,数据库访问的逻辑基本上是在Queryset中完成的,一个查询请求,比如:User.objects.filter(group_id=10)

其中的objects其实就是models.Manager,而Manager又是对QuerySet的一个包装。而QuerySet又是最终要转换为sql的一个中间层(就是ORM种,把Model操作转换为SQL语句的部分)。所以当我们写下User.objects的时候,就已经确定了要访问的是哪个表了,这是由class Meta中的db_table决定的。

 
 
  1. class User(models.Model):
  2. username = models.CharField(max_length=255)
  3. class Meta:
  4. db_table = 'user'

理论上讲,我们可以通过在运行时修改db_table来完成分表CRUD的逻辑,但是the5fire在看了又看源码之后,还是没找到如何下手。还是上面的问题,当执行到User.objects的时候,表已经确定了,当执行到User.objects.filter(group=10)的时候只不过是在已经生成好的sql语句中增加了一个where部分语句。所以并没有办法在执行filter的时候来动态设置db_table。

对于问题中说的get也是一样,因为get本身就是在执行完filter之后从_result_cache列表中获取的数据(_result_cache[0])。

方案一

根据the5fire上面的分析,要想在执行具体查询时修改db_table已经是不可能了(当然,如果你打算去重写Model中Meta部分的逻辑以及Queryset部分的逻辑,就当我没说,我只能表示佩服)。

所以只能从定义层面下手了。也就是我需要定义多个Model,同样的字段,不同的db_table。大概是这样。

 
 
  1. class User(models.Model):
  2. username = models.CharField(max_length=255)
  3. class Meta:
  4. abstract = True
  5. class User1(User):
  6. class Meta:
  7. db_table = 'user_1' # 默认情况下不设置db_table属性时,Django会使用``<app>_<model_name>``.lower()来作为表名
  8. class User2(User):
  9. class Meta:
  10. db_table = 'user_2'

这样在User.objects.get(id=3)的时候,如果按照模2计算,那就是User01.objects.get(id=3),笨点的方法就是写一个dict:

 
 
  1. user_sharding_map = {
  2. 1: User1,
  3. 2: User2
  4. }
  5. def get_sharding_model(id):
  6. key = id % 2 + 1
  7. return user_sharding_map[key]
  8. ShardingModel = get_sharding_model(3)
  9. ShardingModel.objects.get(id=3)

如果真的这么写那Python作为动态语言,还有啥用,你分128张表试试。我们应该动态创建出User01,User02,....UserN这样的表。

 
 
  1. class User(models.Model):
  2. @classmethod
  3. def get_sharding_model(cls, id=None):
  4. piece = id % 2 + 1
  5. class Meta:
  6. db_table = 'user_%s' % piece
  7. attrs = {
  8. '__module__': cls.__module__,
  9. 'Meta': Meta,
  10. }
  11. return type(str('User%s' % piece), (cls, ), attrs)
  12. username = models.CharField(max_length=255, verbose_name="the5fire blog username")
  13. class Meta:
  14. abstract = True
  15. ShardingUser = User.get_sharding_model(id=3)
  16. user = ShardingUser.objects.get(id=3)

嗯,这样看起来似乎好了一下,但是还有问题,id=3需要传两次,如果两次不一致,那就麻烦了。Model层要为上层提供统一的入口才行。

 
 
  1. class MyUser(models.Model):
  2. # 增加方法 BY the5fire
  3. @classmethod
  4. def sharding_get(cls, id=None, **kwargs):
  5. assert id, 'id is required!'
  6. Model = cls.get_sharding_model(id=id)
  7. return Model.objects.get(id=id, **kwargs)

对上层来书,只需要执行MyUser.sharding_get(id=10)即可。不过这改变了之前的调用习惯 objects.get 。

不管怎么说吧,这也是个方案,更完美的方法就不继续探究了,在Django的ORM中钻来钻去寻找可以hook的点实在憋屈。

我们来看方案二吧

方案二

ORM的过程是这样的,Model——> SQL ——> Model,在方案一中我们一直在处理Model——> SQL的部分。其实我们可以抛开这一步,直接使用raw sql。

QuerySet提供了raw这样的接口,用来让你忽略第一层转换,但是有可以使用从SQL到Model的转换。只针对SELECT的案例:

 
 
  1. class MyUser(models.Model):
  2. id = models.IntegerField(primary_key=True, verbose_name='ID')
  3. username = models.CharField(max_length=255)
  4. @classmethod
  5. def get_sharding_table(cls, id=None):
  6. piece = id % 2 + 1
  7. return cls._meta.db_table + str(piece)
  8. @classmethod
  9. def sharding_get(cls, id=None, **kwargs):
  10. assert isinstance(id, int), 'id must be integer!'
  11. table = cls.get_sharding_table(id)
  12. sql = "SELECT * FROM %s" % table
  13. kwargs['id'] = id
  14. condition = ' AND '.join([k + '=%s' for k in kwargs])
  15. params = [str(v) for v in kwargs.values()]
  16. where = " WHERE " + condition
  17. try:
  18. return cls.objects.raw(sql + where, params=params)[0] # the5fire:这里应该模仿Queryset中get的处理方式
  19. except IndexError:
  20. # the5fire:其实应该抛Django的那个DoesNotExist异常
  21. return None
  22. class Meta:
  23. db_table = 'user_'

大概这么个意思吧,代码可以再严谨些。

总结

单纯看方案一的话,可能会觉得这么大量数据的项目,就别用Django了。其实the5fire第一次尝试找一个优雅的方式hack db_table时,也是一头灰。但是,所有的项目都是由小到大的,随着数据/业务的变大,技术人员应该也会更加了解Django,等到一定阶段之后,可能发现,用其他更灵活的框架,跟直接定制Django成本差不多。


参考链接:

https://www.the5fire.com/django-sharding-model.html

猜你喜欢

转载自blog.csdn.net/u012762054/article/details/80920778
今日推荐