You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于Django ContentType模型的用户多类型贡献内容关联查询实现方案咨询

Django ORM vs 原生SQL:实现用户贡献内容聚合查询

当然可以用Django ORM实现这个需求,不需要非得依赖原生SQL!不过两种方式各有适用场景,我给你详细拆解一下:


一、使用Django ORM实现

ORM的优势是可读性强、符合Django的开发习惯,而且能自动处理数据库兼容性问题。这里的核心是利用ContentType和预取查询(Prefetch)来避免N+1查询,同时整理出你需要的结构。

步骤1:导入依赖并定义ContentType

首先我们需要获取目标模型的ContentType,用来筛选对应的贡献记录:

from django.contrib.contenttypes.models import ContentType
from django.db.models import Prefetch
from .models import User, UserContribution, Tip, Example, Struggle

# 获取各模型对应的ContentType
tip_ct = ContentType.objects.get_for_model(Tip)
example_ct = ContentType.objects.get_for_model(Example)
struggle_ct = ContentType.objects.get_for_model(Struggle)

步骤2:创建预取规则

Prefetch对象分别预取每个用户的三类贡献,这样可以批量查询,避免多次数据库请求:

# 预取用户的Tip贡献
prefetch_tips = Prefetch(
    'usercontribution_set',
    queryset=UserContribution.objects.filter(content_type=tip_ct),
    to_attr='tip_contributions'
)

# 预取用户的Example贡献
prefetch_examples = Prefetch(
    'usercontribution_set',
    queryset=UserContribution.objects.filter(content_type=example_ct),
    to_attr='example_contributions'
)

# 预取用户的Struggle贡献
prefetch_struggles = Prefetch(
    'usercontribution_set',
    queryset=UserContribution.objects.filter(content_type=struggle_ct),
    to_attr='struggle_contributions'
)

步骤3:获取用户并整理数据

先筛选目标用户,然后预取所有贡献,最后转换成你需要的JSON结构。这里我们用批量查询来避免N+1问题:

# 选择目标用户(这里示例选择id为1、2的用户)
selected_users = User.objects.filter(id__in=[1, 2]).prefetch_related(
    prefetch_tips,
    prefetch_examples,
    prefetch_struggles
)

# 批量获取所有需要的模型实例,减少查询次数
# 收集所有Tip的ID
all_tip_ids = []
for user in selected_users:
    all_tip_ids.extend([contrib.object_id for contrib in user.tip_contributions])
tip_map = {tip.id: tip for tip in Tip.objects.filter(id__in=all_tip_ids)}

# 收集所有Example的ID
all_example_ids = []
for user in selected_users:
    all_example_ids.extend([contrib.object_id for contrib in user.example_contributions])
example_map = {ex.id: ex for ex in Example.objects.filter(id__in=all_example_ids)}

# 收集所有Struggle的ID
all_struggle_ids = []
for user in selected_users:
    all_struggle_ids.extend([contrib.object_id for contrib in user.struggle_contributions])
struggle_map = {struggle.id: struggle for struggle in Struggle.objects.filter(id__in=all_struggle_ids)}

# 整理成目标格式
result = []
for user in selected_users:
    user_data = {
        'user_id': user.id,
        'first_name': user.first_name,
        'last_name': user.last_name,
        'tips': [],
        'examples': [],
        'struggles': []
    }

    # 填充Tip数据
    for contrib in user.tip_contributions:
        tip = tip_map[contrib.object_id]
        user_data['tips'].append({
            'id': tip.id,
            'title': tip.title,
            'content': tip.content,
            'contributed_at': contrib.contributed_at.strftime('%Y-%m-%d')
        })

    # 填充Example数据
    for contrib in user.example_contributions:
        example = example_map[contrib.object_id]
        user_data['examples'].append({
            'id': example.id,
            'headline': example.headline,
            'content': example.content,
            'contributed_at': contrib.contributed_at.strftime('%Y-%m-%d')
        })

    # 填充Struggle数据
    for contrib in user.struggle_contributions:
        struggle = struggle_map[contrib.object_id]
        user_data['struggles'].append({
            'id': struggle.id,
            'headline': struggle.headline,
            'content': struggle.content,
            'contributed_at': contrib.contributed_at.strftime('%Y-%m-%d')
        })

    result.append(user_data)

二、原生SQL的高效实现方式

如果你的用户量和贡献量很大,原生SQL会更高效——它可以通过一次查询完成所有关联和聚合,避免ORM中多次查询和内存拼接的开销。这里我们用PostgreSQL的JSON函数来直接生成你需要的结构(如果用其他数据库,语法会略有不同,但思路一致)。

原生SQL查询语句

SELECT
    u.id AS user_id,
    u.first_name,
    u.last_name,
    -- 聚合Tip数据,过滤掉非Tip的记录
    JSON_AGG(
        JSON_BUILD_OBJECT(
            'id', t.id,
            'title', t.title,
            'content', t.content,
            'contributed_at', uc.contributed_at::DATE
        )
    ) FILTER (WHERE ct.model = 'tip') AS tips,
    -- 聚合Example数据
    JSON_AGG(
        JSON_BUILD_OBJECT(
            'id', e.id,
            'headline', e.headline,
            'content', e.content,
            'contributed_at', uc.contributed_at::DATE
        )
    ) FILTER (WHERE ct.model = 'example') AS examples,
    -- 聚合Struggle数据
    JSON_AGG(
        JSON_BUILD_OBJECT(
            'id', s.id,
            'headline', s.headline,
            'content', s.content,
            'contributed_at', uc.contributed_at::DATE
        )
    ) FILTER (WHERE ct.model = 'struggle') AS struggles
FROM
    auth_user u
JOIN
    user_contribution uc ON u.id = uc.contributed_by_id
JOIN
    django_content_type ct ON uc.content_type_id = ct.id
LEFT JOIN
    tip t ON ct.id = (SELECT id FROM django_content_type WHERE model = 'tip') AND uc.object_id = t.id
LEFT JOIN
    example e ON ct.id = (SELECT id FROM django_content_type WHERE model = 'example') AND uc.object_id = e.id
LEFT JOIN
    struggle s ON ct.id = (SELECT id FROM django_content_type WHERE model = 'struggle') AND uc.object_id = s.id
WHERE
    u.id IN %s
GROUP BY
    u.id, u.first_name, u.last_name;

在Django中执行原生SQL

from django.db import connection

def get_user_contributions(selected_user_ids):
    with connection.cursor() as cursor:
        cursor.execute("""
            SELECT
                u.id AS user_id,
                u.first_name,
                u.last_name,
                JSON_AGG(
                    JSON_BUILD_OBJECT(
                        'id', t.id,
                        'title', t.title,
                        'content', t.content,
                        'contributed_at', uc.contributed_at::DATE
                    )
                ) FILTER (WHERE ct.model = 'tip') AS tips,
                JSON_AGG(
                    JSON_BUILD_OBJECT(
                        'id', e.id,
                        'headline', e.headline,
                        'content', e.content,
                        'contributed_at', uc.contributed_at::DATE
                    )
                ) FILTER (WHERE ct.model = 'example') AS examples,
                JSON_AGG(
                    JSON_BUILD_OBJECT(
                        'id', s.id,
                        'headline', s.headline,
                        'content', s.content,
                        'contributed_at', uc.contributed_at::DATE
                    )
                ) FILTER (WHERE ct.model = 'struggle') AS struggles
            FROM
                auth_user u
            JOIN
                user_contribution uc ON u.id = uc.contributed_by_id
            JOIN
                django_content_type ct ON uc.content_type_id = ct.id
            LEFT JOIN
                tip t ON ct.id = (SELECT id FROM django_content_type WHERE model = 'tip') AND uc.object_id = t.id
            LEFT JOIN
                example e ON ct.id = (SELECT id FROM django_content_type WHERE model = 'example') AND uc.object_id = e.id
            LEFT JOIN
                struggle s ON ct.id = (SELECT id FROM django_content_type WHERE model = 'struggle') AND uc.object_id = s.id
            WHERE
                u.id IN %s
            GROUP BY
                u.id, u.first_name, u.last_name;
        """, (tuple(selected_user_ids),))
        rows = cursor.fetchall()
        
        # 转换为目标格式
        result = []
        for row in rows:
            user_id, first_name, last_name, tips, examples, struggles = row
            result.append({
                'user_id': user_id,
                'first_name': first_name,
                'last_name': last_name,
                'tips': tips if tips else [],
                'examples': examples if examples else [],
                'struggles': struggles if struggles else []
            })
        return result

总结

  • ORM方式:适合中小数据量,代码易维护、可读性强,符合Django生态。
  • 原生SQL方式:适合大数据量场景,查询效率更高,直接在数据库层面完成聚合和格式转换。

内容的提问来源于stack exchange,提问作者Simon Vu

火山引擎 最新活动