Bug!Bug?Bug.....
又遇到了灵异事件。。Django的ORM。orz.
昨天对Blog的程序进行了优化。见前一篇文章,因于文章分类允许为Null导致查询文章列表时(列表需要显示分类名)Select_related不起作用引发了1+N问题,所以我把Null=True去掉。减却了多余的N条查询。但是奇怪的事情发生了,我从早上开始发觉,首页列表的作者变成了Blog的Title。但代码明明是{{entry.author.name}}!我改成其他属性试下,依然是Print出Blog的其他属性。我回想昨天更新做过的改动,撒销均无效。最后想起会不会是因为改了Model的属性引起的,于是我把分类的Null=True加上。果然!显示正常了。这是为什么呢?不解!缓存?没可能吧?
我是不可能再把分类的Null=true保留的,因为实践证明这样对性能损耗太大。但不加上又出现属性值错乱的情况。怎么办?最后我作了个尝试,我把Model里面的属性调换了一下位置,原来Author在Catelog下方:
catelog = models.ForeignKey(Catelog,verbose_name='分类')
author = models.ForeignKey(Account,verbose_name='作者')
现在改回来,Author写在Catelog上方。显示正常。My god!
author = models.ForeignKey(Account,verbose_name='作者')
catelog = models.ForeignKey(Catelog,verbose_name='分类')
这是我的程序的Bug?还是Django的Bug?还是我的Bug?我想这个解决的办法不是好办法。
我拿到三种情况的Sql。一是分类为Null的查询,二是分类为NotNull的查询,三是分类为NotNull且Author属性排在Catelog前面的查询。结果是第一和第三种情况blog_account_name所在的列位置是一样的。这是否说明Django的确是记住查询结果的位置并且缓存起来了?但缓存到哪里了呢?如果刷新呢?
Django也会智能Lazyload?
今天在本地很快实现了最新评论的功能。在右手边的最新评论点击连接时会去到文章的页面,并定位到评论所在的锚点上,也就是说,我需要在页面上取得评论所在文章的ID,也就是需要这样:{{comment.comment_to.id}}。这时候,我仅仅需要获得文章的ID,但不知道Django是否会到数据库去把相应的文章查出来(我没有使用Select_relate)之后再拿ID呢?不过Django好现没有可以设置打印执行Sql语句的开关,并不能通过配置来看到运行时Django查了多少次数据库。而这样的功能,我使用Java的Hibernate时就有的。
我Google了一下,发现Django本身是没有提供日志和Sql打印功能的。而有人做了些工作,如这位仁兄写了个中间件,可以在页面上打印出该次请求一共执行了多少条语句,每条语句的执行时间。而这里有个叫Django-logging的项目,为Django提供Logging,包括Sql的打印。等会就试验一下。Django天生就是Lazyload的,需要用到数据的时候才去加载,我希望,我的担心是多余的。一会尝试使用Log来看看测试结果吧。
结果一:Select_related对允许为空的外键不起作用。
我的查询语句是这样:entries = blog.entry_set.filter(status=1).select_related(depth=1).order_by('-id') 查询一个Blog下面所有的Entry,并把有关联的数据一并抓取出来(根据ForeignKey)。也就是说,在查询Entry的同时查出Blog,作者,分类等信息。
但是情况是,我在页面找印这些文章的时候,需要把文章的分类也打显示出来,这时候,Django却跑到数据库执行了N次查询分类的语句。暴汗。这是经典的Hibernate的延迟加载用得不恰当的情况啊!我一看打印出来的查询语句,有查相关的作者、Blog,但偏偏没有查分类。最后我回到Model里比较了一下,我的分类属性上面设置了null=true,原意是让文章可以没有分类。最后我尝试着去掉Null=true,一运行。那N条查询不见了。靠。。原来Select_related在外键允许空的情况下是行不通D。
结果二:Django不会智能LazyLoad。要手动去Select_related。
回到对Django有疑问的情景,recent_comments = Comment.objects.filter(blog=blog).order_by('-comment_time')[:10] 查出本BLog最新的十条评论。我的假设是,Comment本身保存有Entry的ID,所以在仅仅是获取Entry的ID的时候不需要去数据据里找,Hibernate的LazyLoad就是这样做的,使用动态代理的解决方案。我想在Python这样的动态语言的解决方案更佳,但事实证明我错了。上面的语句又产生了经典的1+N的问题。我在页面根据评论取EntryID的时候{{comment.entry.id}}还是去加载了一次数据,搞笑的是,这句查询需要得到的数据对我有用的是ID,但是查询条件就是ID。我只好把查询语句改成:
recent_comments = Comment.objects.select_related(depth=1).filter(blog=blog).order_by('-comment_time')[:10]。显示去Select_related。宁愿直接去Join相关的表,也不要做1+ N的查询。显然这很浪费,仅仅为了一个ID,要Join好几个表。