Django ORM实现双关联(Double Join)与Sum聚合查询问题求助
问题:账户分组、下属账户及余额统计查询
我在Stack Overflow(SO)和谷歌上搜索了类似案例,但未找到可行解决方案。
问题简述
交易记录(Transaction)归属账户(Account),账户又归属账户分组(AccountsAggrupation)。我希望获取账户分组列表及其下属账户,同时统计每个账户的总余额(账户余额为该账户所有交易记录的金额之和)。
详细说明
我定义了以下模型(为完整展示,包含相关Mixin):
class UniqueNameMixin(models.Model): class Meta: abstract = True name = models.CharField(verbose_name=_('name'), max_length=100, unique=True) def __str__(self): return self.name class PercentageMixin(UniqueNameMixin): class Meta: abstract = True _validators = [MinValueValidator(0), MaxValueValidator(100)] current_percentage = models.DecimalField(max_digits=5, decimal_places=2, validators=_validators, null=True, blank=True) ideal_percentage = models.DecimalField(max_digits=5, decimal_places=2, validators=_validators, null=True, blank=True) class AccountsAggrupation(PercentageMixin): pass class Account(PercentageMixin): aggrupation = models.ForeignKey(AccountsAggrupation, models.PROTECT) class Transaction(models.Model): date = models.DateField() concept = models.ForeignKey(Concept, models.PROTECT, blank=True, null=True) amount = models.DecimalField(max_digits=10, decimal_places=2) account = models.ForeignKey(Account, models.PROTECT) detail = models.CharField(max_length=100, blank=True, null=True) def __str__(self): return '{} - {} - {} - {}'.format(self.date, self.concept, self.amount, self.account)
我希望通过Django ORM实现如下SQL查询的效果:
select ca.*, ca2.*, sum(ct.amount) from core_accountsaggrupation ca join core_account ca2 on ca2.aggrupation_id = ca.id join core_transaction ct on ct.account_id = ca2.id group by ca2.name order by ca.name;
解决方案
嘿,这个需求我之前做项目时也碰到过,用Django ORM完全能搞定,不用硬写原生SQL。下面给你两种实用的方法,看哪种更贴合你的业务场景:
方法一:从账户出发,聚合余额后关联分组
这种方式先计算每个账户的总余额,再关联到对应的账户分组,最后可以用Python简单整理成分组结构:
from django.db.models import Sum # 查询所有账户,带上余额总和,同时关联所属分组 accounts_queryset = Account.objects.annotate( total_balance=Sum('transaction__amount') ).select_related('aggrupation').order_by('aggrupation__name', 'name') # 把结果按分组整理成字典(方便后续遍历使用) grouped_data = {} for account in accounts_queryset: agg = account.aggrupation if agg not in grouped_data: grouped_data[agg] = [] # 处理无交易的账户,余额设为0 balance = account.total_balance or 0 grouped_data[agg].append({ 'account': account, 'total_balance': balance })
方法二:从分组出发,预取带余额的账户
如果你想直接从账户分组开始查询,并且一次性获取所有分组下的账户及余额,可以用Prefetch来实现精准预取,效率更高:
from django.db.models import Sum, Prefetch # 定义预取的账户查询集:每个账户带上余额总和 prefetched_accounts = Prefetch( 'account_set', # AccountsAggrupation关联Account的反向关联名 queryset=Account.objects.annotate(total_balance=Sum('transaction__amount')), to_attr='accounts_with_balance' # 自定义属性名,方便后续调用 ) # 查询所有分组,预取处理好的账户数据,按分组名称排序 aggrupations = AccountsAggrupation.objects.prefetch_related( prefetched_accounts ).order_by('name') # 遍历结果示例 for agg in aggrupations: print(f"分组名称:{agg.name}") for account in agg.accounts_with_balance: # 处理无交易的情况 total = account.total_balance if account.total_balance is not None else 0 print(f" 账户:{account.name},总余额:{total}")
为什么这两种方法能匹配你的SQL?
- 两种方式都实现了
join关联(ORM会自动处理外键关联),并且通过Sum('transaction__amount')完成了金额聚合 order_by('name')对应你SQL里的order by ca.name,保证分组按名称排序group by的逻辑由ORM的annotate自动处理,针对每个账户聚合交易金额,和你SQL里的group by ca2.name效果一致
另外提醒下:如果有账户没有任何交易记录,Sum会返回None,所以记得用or 0或者条件判断把它转成0,避免后续计算或展示出问题。
内容的提问来源于stack exchange,提问作者Alvaro Rodriguez Scelza
相关产品推荐
相关产品推荐

