【发布时间】:2015-07-16 20:56:31
【问题描述】:
考虑一个通用应用程序,每次用户做好事时都会给他们积分
我有模型
class GoodDeed(models.Model):
user = models.ForgeinKey(CustomUser)
points = models.IntegerField(default=0, blank=True)
class CustomUser(models.Model):
points = models.IntegerField(default=0, blank=True)
rank = models.IntegerField(blank=True, null=True)
现在,每次添加新契约时,都会重新计算该用户的总积分,并且每次重新计算用户的积分时,也会重新计算所有用户(相对于其他用户)的排名。
这太可怕了,我想汇总所有排名更新请求并每隔几分钟执行一次。有没有可以做这种事情的python库?
cron 作业是一个选项,但它会每隔几分钟更新一次所有用户的排名,无论是否需要更新。
编辑:根据@The Django Ninja 的建议,我编写了这个装饰器并分享它
用法:
class CustomUser(models.Model):
@cached_model_property
def points(self):
del self.rank # removed cached rank value
return sum(self.good_deeds.values_list("points", flat=True))
@cached_model_property(readonly=False)
def rank(self):
user_list = []
for user in User.onjects.all().only("points")
user_list.push((user.points, user))
# sort the list
user_list.sort(reverse=True)
# update all ranking
rank = 0
my_rank = None
for points, user in user_list:
rank += 1
if user.pk == self.pk:
# Don't update this instance's rank yet, as it will be
# updated when the function return
my_rank = rank
continue
# Save the new rank in the cache
user.rank = rank
# If you forget to return something (not None) caching will always assumed to be invalid
return my_rank
效果
>>> user.points
# call points() method, cache the result, return it
>>> user.points
# return cached result without calling points() method
>>> del user.points
>>> user.points
# call points() method, cache the result, return it
>>> user.rank
# 1. Calculate and save (in cache) rank of all users except current user
# 2. Cache current user's rank and return it
>>> user.rank
# return cached user's rank
>>> del user.points
>>> user.rank
# Recalculate user's points and all ranking values
>>> user.points = 9
# Readonly property exception
id 为 20 的用户的缓存排名值将存储在名为 'User.20.rank'
装饰器代码
def cached_model_property(f=None, **kwargs):
"""
cached_model_property is a decorator for model functions that takes no arguments
The function is converted into a property that support caching out of the box
Sample usage:
class Team(models.Model):
@cached_model_property
def points(self):
# Do complex DB queries
return result
@cached_model_property(readonly=False)
def editable_points(self):
# get result
return result
Now try
team = Team.objects.first()
team.points <-- complex DB queries will happen, result will be returned
team.points <-- this time result is returned from cache (points function is not called at all!
del team.points <-- points value has been removed from cache
team.points <-- complex DB queries will happen, result will be returned
set readlonly parameter False to make the property writeable
team.editable_points = 88
in this case the assigned value will replace the value stored in the cache
team.editable_points
returns 88
"""
readonly = kwargs.get("readonly", True)
def func(f):
def _get_cache_key(obj):
model_name = obj.__class__.__name__
method_name = f.__name__
return "%s.%s.%s" % (model_name, obj.pk, method_name)
def getX(obj):
"""
This decorator can convert any function that **doesn't** take any arguments into a cached property
:param obj: model object instance (python provide it by default to class members)
:return: the cached value if present otherwise call the actual method
Note:
This decorator doesn't work if the model function return None.
The function will be called as long as it return None, no caching!
"""
# Try to get the cache key for that method
cache_key = _get_cache_key(obj)
result = cache.get(cache_key)
# If not cached, call the actual method and cache the result
if result is None:
result = f(obj)
cache.set(cache_key, result)
return result
def delX(obj):
"""
Remove that property from the cache
:param obj:
:return: None
"""
cache_key = _get_cache_key(obj)
# Remove that key from the cache
cache.delete(cache_key)
def setX(obj, value):
"""
Set the cache value of that property
:param obj:
:return: None
"""
cache_key = _get_cache_key(obj)
# Remove that key from the cache
cache.set(cache_key, value)
if readonly:
return property(fget=getX, fdel=delX)
else:
return property(fget=getX, fset=setX, fdel=delX)
# f (= class method) is passed when using @cached_model_property
if f:
return func(f)
# f is not passed when using @cached_model_property(readonly=True) or even @cached_model_property()
return f
【问题讨论】:
-
“现在每次添加新契约时,都会重新计算该用户的总积分,并且每次重新计算用户的积分时,也会重新计算所有用户(相对于其他用户)的排名。” - 好吧,为什么?! 为什么它们不是
CustomUser的属性,根据需要计算,而不是每当GoodDeed被添加了吗?我不会将points或rank设为属性。 -
Celery 是一个很好的异步任务python库docs.celeryproject.org/en/latest/index.html
-
@jonrsharpe,要计算用户的排名,您需要计算每个用户的总积分(=他的善行积分的总和)。你能想象每次用户打开他的主页时执行这些查询吗?
-
@MarkGalloway Celery 很棒,但如果 10 个用户在一分钟内获得新积分,排名重新计算任务将执行 10 次。我试图避免这种情况
-
@Ramast 是的,我可以;我认为这不会花那么长时间!