欢迎加入QQ讨论群258996829
麦子学院 头像
苹果6袋
6
麦子学院

Django分表的两个方案

发布时间:2017-05-16 21:02  回复:0  查看:2494   最后回复:2017-05-16 21:02  
本文和大家分享的主要是django 分表相关内容,一起来看看吧,希望对大家 学习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 决定的。
  class User(models.Model):
  username = models.CharField(max_length=255)
  class Meta:
  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 。大概是这样。
  class User(models.Model):
  username = models.CharField(max_length=255)
  class Meta:
  abstract = True
  class User1(User):
  class Meta:
  db_table = 'user_1'  #  默认情况下不设置 db_table 属性时, Django 会使用 ``_``.lower() 来作为表名
  class User2(User):
  class Meta:
  db_table = 'user_2'
  这样在 User.objects.get(id=3)  的时候,如果按照模2 计算,那就是 User01.objects.get(id=3)  ,笨点的方法就是写一个dict:
  user_sharding_map = {
  1: User1,
  2: User2
  }
  def get_sharding_model(id):
  key = id % 2 + 1
  return user_sharding_map[key]
  ShardingModel = get_sharding_model(3)
  ShardingModel.objects.get(id=3)
  如果真的这么写那Python 作为动态语言,还有啥用,你分 128 张表试试。我们应该动态创建出 User01,User02,....UserN 这样的表。
  class User(models.Model):
  @classmethod
  def get_sharding_model(cls, id=None):
  piece = id % 2 + 1
  class Meta:
  db_table = 'user_%s' % piece
  attrs = {
  '__module__': cls.__module__,
  'Meta': Meta,
  }
  return type(str('User%s' % piece), (cls, ), attrs)
  username = models.CharField(max_length=255, verbose_name="the5fire blog username")
  class Meta:
  abstract = True
  ShardingUser = User.get_sharding_model(id=3)
  user = ShardingUser.objects.get(id=3)
  嗯,这样看起来似乎好了一下,但是还有问题,id=3 需要传两次,如果两次不一致,那就麻烦了。 Model 层要为上层提供统一的入口才行。
  class MyUser(models.Model):
  增加方法  BY the5fire    @classmethod
  def sharding_get(cls, id=None, **kwargs):
  assert id, 'id is required!'
  Model = cls.get_sharding_model(id=id)
  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 的案例 :
  class MyUser(models.Model):
  id = models.IntegerField(primary_key=True, verbose_name='ID')
  username = models.CharField(max_length=255)
  @classmethod
  def get_sharding_table(cls, id=None):
  piece = id % 2 + 1
  return cls._meta.db_table + str(piece)
  @classmethod
  def sharding_get(cls, id=None, **kwargs):
  assert isinstance(id, int), 'id must be integer!'
  table = cls.get_sharding_table(id)
  sql = "SELECT * FROM %s" % table
  kwargs['id'] = id
  condition = ' AND '.join([k + '=%s' for k in kwargs])
  params = [str(v) for v in kwargs.values()]
  where = " WHERE " + condition
  try:
  return cls.objects.raw(sql + where, params=params)[0]  # the5fire: 这里应该模仿 Queryset get 的处理方式
  except IndexError:
  # the5fire: 其实应该抛 Django 的那个 DoesNotExist 异常
  return None
  class Meta:
  db_table = 'user_'
  大概这么个意思吧,代码可以再严谨些。
   总结
  单纯看方案一的话,可能会觉得这么大量数据的项目,就别用Django 了。其实 the5fire 第一次尝试找一个优雅的方式 hack db_table 时,也是一头灰。但是,所有的项目都是由小到大的,随着数据 / 业务的变大,技术人员应该也会更加了解 Django ,等到一定阶段之后,可能发现,用其他更灵活的框架,跟直接定制 Django 成本差不多。

来源:the5fire 的技术博客
您还未登录,请先登录

热门帖子

最新帖子