资讯动态

Django ORM单表操作全解析:从模型定义到查询集实战

发布时间:2026/10/9 16:19:16 来源:尧图企业网站定制
Django ORM的单表实例其实是很多人入门Django时最先接触、但往往又没真正吃透的一块内容。网上讲CRUD的文章一抓一大把但大部分都是把代码贴一遍告诉你“这样写就能跑”至于为什么这么写、底层发生了什么、遇到问题怎么排查基本是一笔带过。这篇文章我想换个角度直接从单表操作入手把ORM的模型定义、增删改查、查询集的懒加载机制、常见坑点都揉碎了讲一遍。这也算是Django的一站式教程里最基础也最关键的一环适合刚学完Python基础、准备上手Django做web应用开发实战的新手也适合写过一些Django代码但总觉得ORM“差点意思”的开发者。1. 先搞清楚ORM到底在替你做什么很多新手学Django ORM上来就背语法objects.all()查全部、objects.get()查单条、save()保存、delete()删除。语法背得滚瓜烂熟但一旦换个场景就懵了比如“我要按条件更新怎么办”“查出来的数据为什么改了不生效”“为什么明明删了记录get()还是能查到”这些问题归根结底都是因为没有理解ORM的本质。1.1 ORM不是“把数据库变没了”而是“给你一个翻译官”ORM全称是对象关系映射听起来很高大上其实干的事特别朴素把数据库表映射成Python类把表里的行映射成类的实例对象把字段映射成对象的属性。你操作对象就等于操作数据库里的记录。打个比方数据库是一间仓库SQL是仓库管理员使用的语言而Python是你的语言。ORM就是一个翻译官你说“我要把编号为1的那箱货搬出来”翻译官就转译成管理员能听懂的指令去仓库里执行再把结果翻译回你熟悉的格式递给你。在Django里这个翻译官的核心就是Model基类和Manager管理器。Model负责定义“仓库长什么样”——也就是表结构Manager默认叫objects负责执行“搬货指令”——也就是查询和操作。from django.db import models class Article(models.Model): title models.CharField(max_length200) content models.TextField() created_at models.DateTimeField(auto_now_addTrue)这段代码定义了Article表包含标题、正文、创建时间三个字段。你没写任何建表SQL但Django会在你执行makemigrations和migrate时自动生成并执行建表语句。这就是ORM最直接的价值——屏蔽了数据库方言差异。1.2 为什么Django选ORM而不是直接写SQL有人会说直接写SQL不是更灵活、性能更好吗这话没错但对大部分web应用开发来说维护成本才是最大的敌人。跨数据库迁移你开发时用SQLite上线用MySQL如果手写SQL至少得检查一遍语法兼容性。Django ORM生成的SQL会自动适配当前数据库引擎你几乎不用关心底层是哪种数据库。防注入ORM的查询参数都会经过参数化处理从根本上规避了SQL注入风险。手写SQL时一旦忘记转义或者用了字符串拼接就是给攻击者留后门。对象与数据联动你修改一个对象的属性再调用save()Django会自动生成UPDATE语句而且只更新被修改的字段在特定条件下。这种“对象即数据”的体验写业务代码时效率极高。当然ORM也不是万能的。复杂报表查询、海量数据批量操作该上原生SQL还是得上。但单表场景下ORM的效率和安全性远超手写SQL这也是为什么Django官方文档把ORM作为核心卖点之一。2. 单表模型的建模与准备工作项目标题是“单表实例”那我们就从一张最简单的表开始。我这次用的例子是博客文章的Article表理由很简单字段类型覆盖了CharField标题、TextField正文、DateTimeField时间、BooleanField是否发布足够演示ORM的常见操作又不至于复杂到分散注意力。2.1 模型字段选型不只是“选个类型”那么简单很多初学者在建表时字段类型随手选觉得“反正能存数据就行”。其实字段类型直接影响数据库层面的存储方式、索引策略、校验规则甚至影响后续的查询性能。以Article为例我推荐的字段设计是from django.db import models class Article(models.Model): title models.CharField(max_length200, verbose_name标题) content models.TextField(verbose_name正文) status models.BooleanField(defaultFalse, verbose_name是否发布) views models.IntegerField(default0, verbose_name浏览量) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) updated_at models.DateTimeField(auto_nowTrue, verbose_name更新时间) class Meta: db_table article ordering [-created_at] def __str__(self): return self.title这里有几个容易忽略的点第一CharField必须指定max_length而且这个长度会影响数据库索引。MySQL里InnoDB的索引最大长度有限制如果max_length设得太大后面想给这个字段建索引会失败或需要前缀索引。第二auto_now_add和auto_now的区别。auto_now_addTrue只在记录创建时写入当前时间之后不再变动适合created_atauto_nowTrue每次调用save()都会刷新为当前时间适合updated_at。这个设计比手动写timezone.now()干净得多也不会出现“忘了更新更新时间”的问题。第三db_table可以自定义表名。如果不设置Django默认表名是“app名_模型名”比如blog_article。设置成article更简洁但要注意团队规范别每个模型都随便起名后面维护起来会想骂人。第四Meta.ordering影响默认查询顺序。设置了ordering [-created_at]之后所有不带显式order_by的查询结果都会按创建时间倒序排列。这个设计的好处是列表页直接Article.objects.all()就是最新的在前坏处是如果你某次查询想按其他字段排序必须显式order_by覆盖否则会困惑“为什么这个查询结果是这个顺序”。2.2 迁移让模型变成真正的数据库表模型定义好之后需要两步操作才能让数据库里真的出现这张表python manage.py makemigrations python manage.py migratemakemigrations会根据模型变化生成迁移文件放在app的migrations目录下。这个文件是给Django用的“变更记录”记录了从上一个状态到当前状态需要执行哪些数据库操作。migrate才是真正执行把变更应用到数据库。这里分享两个实战经验迁移文件要提交到版本库不要ignore。团队协作时别人拉代码后执行migrate就能同步数据库结构不需要手动去数据库执行SQL。而且迁移文件是数据库结构变更的历史记录出问题可以回溯。改模型后不要手动改数据库一定要重新生成迁移。我见过有同事为了省事直接在数据库管理工具里加了个字段结果模型里没同步代码一跑直接报column does not exist。更要命的是后来执行makemigrations时Django会尝试把模型状态和数据库实际状态做对比然后产生一堆莫名其妙的迁移。2.3 shell环境快速验证模型的“试验台”模型建好了不要急着写视图先在Django shell里把CRUD撸一遍。shell是Django提供的交互式环境可以让你直接操作ORM非常适合验证逻辑、调试代码。python manage.py shell进入shell之后你可以一行一行地执行Python代码实时看到结果。这一步对新手特别友好因为你可以立刻确认“这个查询返回到底是什么”“filter和exclude的结果对不对”不用等整个项目跑起来。3. 单表核心操作增删改查的落地实现这一节是全文的干货重心。我会按“创建、查询、修改、删除”四个维度逐一展开每个操作都讲清楚“怎么写”和“底层发生了什么”。3.1 创建对象两种方式适用不同场景创建记录有两种主流写法方式一实例化对象后调用save()article Article(titleDjango ORM入门, content这是一篇教程) article.save()方式二使用objects.create()article Article.objects.create(titleDjango ORM进阶, content这是第二篇)两者最终都会生成INSERT语句区别在于方式一允许你在save()之前对对象做更多处理比如根据某个字段动态计算另一个字段的值方式二则是典型的“一步到位”适合创建参数已经齐全的场景。还有一种经常被忽略的创建方式get_or_create。它先尝试按条件查询如果找不到则创建返回一个元组(对象, 是否新建的布尔值)。article, created Article.objects.get_or_create( titleDjango ORM实战, defaults{content: 默认正文} )这个方法的经典应用场景是“初始化数据”“幂等写入接口”。比如用户第一次登录时创建个人资料重复请求不会产生重复记录。注意get_or_create查询条件的字段和defaults里给的字段是分开的。查询字段用于匹配已有数据defaults里的字段只在创建时写入。如果你把title放在查询条件里又想在defaults里重复指定Django会直接报错。3.2 查询操作all、get、filter的区别和正确用法单表查询是Django ORM使用频率最高的操作也是新手最容易混淆的地方。all()返回所有记录返回类型是QuerySetall_articles Article.objects.all()get()返回单个对象如果查询不到或查到多条都会抛异常article Article.objects.get(id1)get()有两个经典的坑DoesNotExist查不到时抛出和MultipleObjectsReturned查到多条时抛出。所以在用get之前你要先确认查询条件是唯一的比如主键id、唯一字段。如果条件不唯一更稳妥的做法是用filter加.first()。filter()按条件过滤返回的是QuerySet一条或多条都可能published Article.objects.filter(statusTrue)从使用场景来说all()用于列表页get()用于详情页按主键取单条filter()用于各种带条件的列表和统计。filter的参数写法是字段名值但条件不止“等于”一种Django提供了双下划线扩展语法写法作用示例fieldvalue等于titleDjangofield__gtvalue大于views__gt100field__gtevalue大于等于views__gte100field__ltvalue小于views__lt10field__ltevalue小于等于views__lte10field__containsvalue包含title__containsORMfield__icontainsvalue包含忽略大小写title__icontainsormfield__invalue在列表内status__in[True, False]field__isnullTrue为空content__isnullTrue以日期查询为例实际开发中经常用到的created_at__date、created_at__year、created_at__month语法也是双下划线扩展的一部分。# 查询2024年创建的已发布文章 Article.objects.filter(created_at__year2024, statusTrue) # 查询今天创建的文章 from django.utils import timezone today timezone.now().date() Article.objects.filter(created_at__datetoday)这里有个理解难点双下划线并不是Python语法而是Django ORM约定的“字段路径分隔符”。created_at__year表示“created_at字段的年份属性”Django会把它翻译成SQL里的EXTRACT(YEAR FROM created_at)或者YEAR(created_at)具体取决于数据库引擎。3.3 修改更新两种策略性能和语义差别很大更新操作主要分两类对象级更新和查询集级更新。对象级更新先查后改语义清晰但多一次查询article Article.objects.get(id1) article.title 修改后的标题 article.save()这种方式先执行一次SELECT把对象加载到内存修改属性后再执行一次UPDATE。优点是你可以同时修改多个字段还可以在save()之前对值做校验或加工。缺点是多一次查询如果是一批记录都要改性能开销明显。查询集级更新直接更新高效但“看不见改动后的对象”# 所有已发布文章浏览量加1 Article.objects.filter(statusTrue).update(viewsmodels.F(views) 1)这段代码只执行一次UPDATE不会有SELECT。F表达式让数据库在SQL层面完成“当前值1”避免了先把值读到Python内存再写回去的竞争问题。这在并发场景下尤为重要因为如果用“先查再加再存”的方式两个请求同时读到的views都是100分别加1后写回最终结果是101而不是102数据就丢了。使用QuerySet.update()有一点要记住它不会触发模型的save()方法也就不会触发auto_now对updated_at的更新实际上Django 2.0会自动处理updated_at但自定义逻辑不会跑。如果模型里有些字段依赖save()里的逻辑就必须用对象级更新。另一个实用场景是按条件批量修改不同值。比如批量发布一组文章ids [1, 2, 3] Article.objects.filter(id__inids).update(statusTrue)这种写法比for循环逐个save()快得多也是典型的高性能更新方式。3.4 删除对象单条删除与批量删除的边界删除操作同样分两种单条删除先查到对象然后调用delete()article Article.objects.get(id1) article.delete()批量删除直接用QuerySet的delete()方法Article.objects.filter(statusFalse).delete()这里有几个重点提醒批量delete()同样不会触发单个对象的delete()方法里的自定义逻辑只执行SQL级的DELETE。如果有外键关联Django默认会执行级联删除on_deletemodels.CASCADE也就是删掉父表记录时子表里的关联记录也会一并删除。这在单表场景不存在但在实际项目中一定要留意误删一片是灾难。有些“删除”其实是“软删除”。比如给Article加一个is_deleted字段删除时只是把is_deleted置为True。这种做法在需要保留审计数据的业务里很常用代价是每次查询都要带上is_deletedFalse条件。要不要引入django-safedelete这类第三方库取决于项目对数据保留的要求个人建议前期别引入等真正需要时再重构。4. 查询的艺术让单表查询也能玩出花样单个表查询听起来没什么难度但实际开发中仅仅一个Article表就能玩出很多需求分页、去重、统计、条件组合。这些需求如果依赖Python内存里处理数据量一大就崩如果依赖ORM正确的查询集操作效率和代码可读性都能兼顾。4.1 QuerySet是懒加载的什么时候真正执行SQL这是Django ORM最核心的机制之一理解它你写查询时就会少踩一半的坑。qs Article.objects.filter(statusTrue) # 此刻没有执行SQL qs qs.filter(views__gt100) # 还是没有执行SQL print(qs) # 此刻才执行SQLfilter()只是构建一个查询条件对象真正的SQL在“求值”的那一刻才执行。所谓求值就是遍历QuerySet、调用len()、用list()转换、访问索引下标、判断布尔值等操作。这个设计的价值有两个链式filter可以不断累加条件灵活组合最后统一生成一条SQL不会每条filter都单独发一次请求。可以延迟到真正需要数据时才访问数据库比如先拼好条件再决定要不要执行。懒加载也带来一个常见坑如果你在代码里先创建了一个QuerySet然后修改了模型实例的字段值再去遍历这个QuerySet得到的结果可能和预期不一致。因为普通QuerySet不缓存结果每次求值都会重新查询数据库。如果你需要多次使用同一批数据建议强制求值一次并转成列表articles list(Article.objects.filter(statusTrue))4.2 聚合查询count、annotate的基础用法单表查询里count()是最高频的统计操作。它返回的是整数而不是QuerySet所以可以单独赋值给变量或作为接口返回值。total Article.objects.count() published_count Article.objects.filter(statusTrue).count()这里有个性能细节count()在SQL层面执行SELECT COUNT(*)不会加载记录内容。而如果你先all()再len()Django会把所有记录加载到内存数据量大时内存直接飙升。所以永远用count()而不是len(all())。annotate是分组统计的利器。比如统计每天发布的文章数from django.db.models.functions import TruncDate from django.db.models import Count daily_counts Article.objects.annotate( dayTruncDate(created_at) ).values(day).annotate( countCount(id) ).order_by(day)这里TruncDate(created_at)把created_at截断到日期精度values(day)按天分组然后annotate(countCount(id))统计每组的记录数。这类查询在后台管理页、运营报表里非常常见。annotate还有一个重要用途给查询结果附加计算字段。比如给每篇文章算一个“字数”字段from django.db.models.functions import Length articles_with_length Article.objects.annotate( title_lengthLength(title) ) # 此后每篇文章对象都有title_length属性 for article in articles_with_length: print(article.title, article.title_length)注意annotate添加的字段只存在于查询结果中不会改到数据库。4.3 Q对象让OR条件不再别扭filter()默认是AND关系多个条件并列就是“且”。但实际需求经常要“或”。比如“标题包含Django或者正文包含ORM的文章都要”。from django.db.models import Q articles Article.objects.filter( Q(title__containsDjango) | Q(content__containsORM) )Q对象用|表示OR、用表示AND还可以用~表示NOT组合出非常复杂的查询条件。一个经典场景是“搜索功能”keyword Django articles Article.objects.filter( Q(title__icontainskeyword) | Q(content__icontainskeyword), statusTrue )注意这里statusTrue和前面的Q表达式用逗号分隔仍然是AND关系。如果你想让整个条件变成“状态为真且标题或正文包含关键词”这个写法是对的。如果想调整逻辑可以把statusTrue也放进Q里组合articles Article.objects.filter( Q(statusTrue) (Q(title__icontainskeyword) | Q(content__icontainskeyword)) )Q对象还可以动态拼接。比如根据用户选择的筛选条件循环构建Q再组合写法上比一长串filter链式调用更清晰。4.4 排序与去重order_by、distinct的注意点order_by基本是列表查询的标配# 按浏览量降序浏览量相同时按创建时间升序 articles Article.objects.all().order_by(-views, created_at)这里-views的负号表示降序多个排序字段按优先级从左到右。要注意的是order_by如果模型Meta里定义了ordering默认会带上但你显式order_by后可以覆盖。另外order_by对annotate出的字段同样生效比如按统计的count排序。distinct()用于去重。单表场景下如果查询没有joindistinct()基本用不上但一旦涉及多表或某些数据库的values()分组结果不加distinct()可能会出现重复行。新手阶段需要记住distinct()是对查询结果去重但并不是所有数据库都支持所有字段的去重使用前最好先看生成的SQL。单表去重还有一种常见场景查询所有出现过的不重复状态值。Article.objects.values(status).distinct()返回的是字典列表每个字典里包含一个状态值这在做筛选下拉框时很好用。5. 实际开发中最容易踩的坑与排查思路写单表ORM语法不难难的是排查问题。我整理了几个自己踩过、也看同事踩过的坑每一个都有明确的现象和排查方法。5.1 字段名与Python保留字、字段名冲突模型字段名如果跟Python的关键字或Django的内部属性冲突会导致一些很诡异的问题。比如object这种字段名虽然语法上不报错但调用Article.objects时可能会被Python解释成实例属性访问逻辑混乱。保险起见字段名要避开objects、pk、save、delete这类Django内部使用的名称也不要直接用Python关键字比如class、import这种。5.2get()报错DoesNotExist和MultipleObjectsReturned这是单表查询最高频的报错。get()的语义是“查询唯一一条记录”但数据一旦不满足唯一性异常就来了。DoesNotExist你查不到Django抛出这个异常。注意这个异常是Article.DoesNotExist是Django在模型上动态生成的。捕获时要写Article.DoesNotExist不要写成DoesNotExist否则可能捕获不到或者捕获到其他模型的同名异常。MultipleObjectsReturned查询条件匹配了多条。很多新手改了一通发现数据库里确实有重复数据但代码逻辑没问题。这时候要回到数据本身清理重复记录或者在代码层面用filter().first()代替get()。推荐的稳妥写法article Article.objects.filter(pk1).first() if article is None: # 处理不存在的情况 pass else: # 正常使用 passfirst()的好处是即使匹配到多条也只会返回第一条不会抛异常。代价是如果逻辑上应该唯一它可能会悄悄掩盖重复数据需要根据业务场景权衡。5.3 更新字段后没有保存对象级修改的经典坑新手最容易犯的错误从数据库查了一个对象改了属性但忘了调save()。结果页面刷新数据还是原来的。这种错误往往出现在“先查询-再赋值-然后还有其他逻辑”的流程里。写代码时建议修改字段后立刻save()不要拖到后面可以用print(article.title)之类的调试手段确认是否已在内存中修改更稳妥的方式是使用QuerySet.update()一步到位完成数据库更新。5.4 时区问题created_at和updated_at差8小时Django的时区设置是新手绕不开的坑。settings.py里的USE_TZTrue时Django会以UTC时间存储DateTimeField展示时再转换到TIME_ZONE指定的时区。如果你在中国TIME_ZONE建议设成Asia/Shanghai。如果不设置很可能出现“数据库存的是UTC时间页面上打出来比北京时间少8小时”的情况。排查顺序确认USE_TZ和TIME_ZONE的配置确认写入时用的是timezone.now()而不是datetime.now()确认前端展示时有没有做时区转换。Django模板里默认是开启时区转换的但序列化输出时如果直接调.isoformat()会带上00:00或Z后缀前端如果不处理显示的时间还是不对。规范做法是后端返回UTC时间带时区标识前端负责转成用户本地时间或者后端统一转成Asia/Shanghai的字符串再返回。5.5 不是所有“查询”都该用ORM什么时候回到原生SQL单表查询里ORM游刃有余但一旦涉及复杂报表、递归查询、跨表子查询ORM生成的SQL可能又臭又长还慢。这时候不要硬扛直接使用RawSQL、extra或者connection.cursor()执行原生SQL。from django.db import connection with connection.cursor() as cursor: cursor.execute(SELECT COUNT(*) FROM article WHERE status %s, [True]) row cursor.fetchone()注意原生SQL的调试成本更高而且会有SQL注入风险使用时要保证参数化查询不要拼接字符串。我的习惯是视图和常规业务逻辑用ORM复杂的统计报表用原生SQL或数据库视图View两者结合才是平衡之道。6. 从单表走向多表这个基础决定你的天花板文章写到这里其实都在讲单表。但很多人真正卡住的地方是“单表还没吃透就急着上外键、上多表联查”结果一遇到select_related、prefetch_related完全晕头转向。我特别想强调单表ORM是Django数据层的全部基础。你在单表阶段建立的每一个查询习惯——用filter而不是用get、用update而不是循环save、用count而不是len、用Q组合条件而不是一堆if——都会直接复用到多表场景。举例来说假设文章表加了一个外键author你查询文章时带出作者姓名articles Article.objects.select_related(author).all()select_related利用SQL的JOIN一次性拿到关联对象避免每篇都单独查一次作者这就是著名的N1查询问题。这个概念如果你单表阶段就理解了“ORM惰性求值”“查询要尽量减少数据库交互”就很容易接受。反过来如果你单表阶段就没搞明白QuerySet和对象列表的区别多表阶段面对related_name、prefetch_related带来的各种“有列表有对象”的组合只会更乱。所以我建议每一位学Django的开发者都不要急着跳过单表实例把这一章练透再上一层楼。7. 调试ORM的必备小工具前面讲了理论和实操最后分享几个我平时调试ORM的“独门武器”可以显著提升排错效率。7.1 查看SQLquery属性和connection.queries每当你怀疑“ORM到底生成了什么SQL”时直接打印QuerySet.queryqs Article.objects.filter(statusTrue) print(qs.query)输出类似SELECT article.id, article.title, ... FROM article WHERE article.status true这样你就知道ORM实际翻译成了什么方便和手写SQL做对比。如果想看整个请求过程中执行了哪些SQL、耗时多少在settings.DEBUGTrue时使用from django.db import connection print(connection.queries)输出是一个列表每个元素包含sql和time字段非常适合排查N1查询和慢查询。7.2 使用django-debug-toolbar查看页面SQL开发环境下装上django-debug-toolbar页面右侧会显示一个调试面板里面包含当前请求执行的所有SQL、耗时、重复次数。这个工具是Django调试的“神器”级别存在强烈建议新手从第一天就装上养成“看SQL”的习惯。7.3shell是用来试验的不要客气我几乎每个项目都会在shell里做大量的“试验性查询”确认返回类型、确认SQL、试各种filter组合再写进视图或服务层。这比边写边跑服务、断点调试的效率高得多。8. 结语Django ORM的单表实例说到底是“用对象的方式操作数据”这一思想的小缩影。你学会的filter、get、update、delete底层全是SQL你掌握的懒加载、查询集、聚合、F表达式迁移到多表场景也完全适用。我个人在实际操作中的体会是遇到了ORM“行为奇怪”时不要急着怀疑是框架的bug先打印一下SQL看看数据很多问题就清楚了。把单表操作练熟、把调试工具用熟、把常见的坑记熟这三件事做到位Django的数据层就算真正入门了。后面写多表关联、复杂业务逻辑时你会发现这些基础功给你省下了无数时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑