最 近为公司的文章加上了permalink,由于之前已经有满大的数据量了,所以写了一个小脚本将之前的标题自动转换成url友好的slug字段。不过由于考虑不周,等全部弄好了才想起来忘了给slug字段加上unique属性,转换时也没考虑到这点,结果所有同标题的文章在自动转换后也就都有了相同的slug,这在Django里使用 Article.objects.get() 的时候自然就会报错了。
最快的应急措施是立刻把重复的slug改成具有唯一属性的id值,要做成这件事在SQL里面是满简单的,只要用以下语句就可以达到目的:
UPDATE article SET slug = id WHERE slug IN ( SELECT slug FROM article GROUP BY slug HAVING (COUNT(slug) > 1));
不过既然框架都用Django了,我们就来看看用Django怎么来做这件事吧。Django在1.1时为queryset加入了 annotate 这个方法,可以为查询结果添加聚合支持,那让我们打开ipython来试一试吧。
from django.db.models import Count print Article.objects.values('slug')\ .annotate(count = Count('slug'))\ .filter(count__gt=1)
第一次尝试从经验上来说一般是不成功的,果不其然,打印的结果是一个空数组,这是为啥呢?
想起以前写的 Django数据库聚合bug ,自然会认为这也是那个bug的问题了。不过咱还是得验证一下的,以下是Django对这次查询生成的SQL:
SELECT "article"."slug", COUNT("article"."slug") AS "count" FROM "article" GROUP BY "article"."slug", "article"."created" HAVING COUNT("article"."slug") > 1 ORDER BY "article"."created" DESC;
慢着,SQL里面怎么莫名其妙地多出来了一个created列?这时候突然一个激灵,Article的Meta类中确实是定义了 ordering 字段来默认使用发布时间倒序排列的。提到排序,便依稀想起Django文档中好像确实有讲到过关于这个的一些注意点了。立刻翻开文档,果然,在 distinct() 下的Note里找到了这么一段:
if you use a values() query to restrict the columns selected, the columns used in any order_by() (or default model ordering) will still be involved and may affect uniqueness of the results.
这下就清楚了,对于只取部分字段的查询来说,需要再调用一次 order_by 来把那些默认的排序字段的影响清楚。所以把上面的语句稍作修改:
from django.db.models import Count print Article.objects.values('slug')\ .order_by('created')\ .annotate(count = Count('slug'))\ .filter(count__gt=1)
这下便终于得到了重复的slug了。既然已经可以找到重复的slug,接下来就好办了:
from django.db.models import Count, F Article.objects.filter( slug__in = Article.objects.values('slug')\ .order_by('created')\ .annotate(count = Count('slug'))\ .filter(count__gt=1)\ .values_list('slug', flat = True))\ .update(slug = F('id'))
OK,大功告成,收工。
Note
我不是DBA,也许上面的做法并不高效,各位童鞋建议点更好的办法来完成这种任务吧 :)

