本文和大家分享的主要是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
的技术博客