NestJS 入门(9):连上数据库,SQL 写在哪?
上一篇NestJS 入门8环境变量与配置 讲了DATABASE_URL、密钥不要写死。前面几篇把请求链路讲完了Module、注入、Guard、信封、Pipe、启动钩子、环境变量。还缺一块很多人学 Nest 时最先碰到的数据怎么进出 PostgreSQL / MySQL。中文教程里最常见的是TypeORM。另一条路上是Prisma用schema.prisma 生成客户端Service 里写prisma.user.findMany()。两者在 Nest 里的位置一样连库是基础设施模块查询写在 Service或 Repository不要写在 Controller。本篇以 TypeORM 为主把「连上」和「SQL 写哪」讲清楚文末用一张表对照 Prisma。1. 先分清三层连接、映射、查询层干什么TypeORM 里是谁连接用DATABASE_URL连上数据库TypeOrmModule.forRoot()映射一张表对应一个类Entity()实体查询增删改查Repository/QueryBuilder/ 少量裸 SQLController 仍然只负责接请求Get()findAll(){returnthis.usersService.findAll();}SELECT * FROM users不出现在这里。2. 安装与根模块连上数据库npmi nestjs/typeorm typeorm pg# MySQL 则换成 mysql2根模块注册一次连接全局数据源import{TypeOrmModule}fromnestjs/typeorm;Module({imports:[TypeOrmModule.forRoot({type:postgres,url:process.env.DATABASE_URL,autoLoadEntities:true,// 各业务模块 forFeature 登记的实体自动加载synchronize:false,// 生产务必 false改表用 migration}),UsersModule,],})exportclassAppModule{}和第八篇同一条纪律DATABASE_URL来自环境变量不要把账号密码写进仓库。更稳的写法是forRootAsync等ConfigService就绪再连TypeOrmModule.forRootAsync({inject:[ConfigService],useFactory:(config:ConfigService)({type:postgres,url:config.getOrThrowstring(DATABASE_URL),autoLoadEntities:true,synchronize:false,}),});synchronize: true会按实体自动改表本地玩玩可以生产会删列、丢数据。正经项目用 migration见第 7 节。连库发生在 Nest 创建 DataSource 时接近第七篇的「启动期 I/O」。连接失败时应用起不来这是好事。3. 实体表结构写在类上import{Column,Entity,PrimaryColumn}fromtypeorm;Entity({name:users})exportclassUser{PrimaryColumn()id!:string;Column({unique:true})email!:string;Column()password!:string;Column()name!:string;Column({name:created_at,type:timestamptz})createdAt!:Date;}这不是 SQL 文件但等价于在描述CREATETABLEusers(idTEXTPRIMARYKEY,emailTEXTUNIQUENOTNULL,passwordTEXTNOTNULL,nameTEXTNOTNULL,created_at TIMESTAMPTZNOTNULL);实体 表的 TypeScript 形态。真正建表、改表通常交给 migration而不是每次启动synchronize。4. 业务模块把「这张表」登记进盒子第六篇的边界在这里又出现一次根模块forRoot只负责连接哪张表给哪个模块用用forFeatureimport{TypeOrmModule}fromnestjs/typeorm;import{User}from./user.entity;import{UsersService}from./users.service;import{UsersController}from./users.controller;Module({imports:[TypeOrmModule.forFeature([User])],controllers:[UsersController],providers:[UsersService],exports:[UsersService],})exportclassUsersModule{}forFeature([User])的含义本模块可以注入RepositoryUser。别的模块要用用户表要么imports: [UsersModule]用导出的 Service要么自己forFeature——入门优先走 Service避免到处直接操同一张表。5. SQL 写在哪三种由浅到深5.1 Repository日常 CRUD推荐默认import{Injectable}fromnestjs/common;import{InjectRepository}fromnestjs/typeorm;import{Repository}fromtypeorm;import{User}from./user.entity;Injectable()exportclassUsersService{constructor(InjectRepository(User)privatereadonlyusers:RepositoryUser){}findAll(){returnthis.users.find();// 大致对应 SELECT * FROM users}findByEmail(email:string){returnthis.users.findOne({where:{email}});// SELECT * FROM users WHERE email $1 LIMIT 1}create(data:{email:string;password:string;name:string}){constrowthis.users.create(data);returnthis.users.save(row);// INSERT INTO users (...)}}你几乎看不到 SQL 字符串但每条 API 都对应一句 SQL。这就是入门阶段「SQL 写在哪」的答案写在 Service 里用 Repository API 表达。登录校验仍在 Service查库也在同一层asynclogin(email:string,password:string){constuserawaitthis.users.findByEmail(email);if(!user){thrownewUnauthorizedException(Invalid credentials);}// bcrypt.compare ...}5.2 QueryBuilder条件一复杂就用它多表 join、动态筛选、分页find({ where })会别扭。改用 QueryBuilderfindEditorsOfProject(projectId:string){returnthis.users.createQueryBuilder(u).innerJoin(project_members,m,m.user_id u.id).where(m.project_id :projectId,{projectId}).andWhere(m.role :role,{role:editor}).getMany();}生成的 SQL 接近SELECTu.*FROMusers uINNERJOINproject_members mONm.user_idu.idWHEREm.project_id$1ANDm.role$2仍然写在 Service或单独的 Repository 类里。参数用:projectId绑定不要把用户输入拼进字符串防 SQL 注入。5.3 裸 SQL报表、数据库方言、ORM 搞不定时asynccountActiveSessions():Promisenumber{constrowsawaitthis.users.query(SELECT COUNT(*)::int AS cnt FROM sessions WHERE expires_at NOW());returnrows[0].cnt;}或注入 DataSourceconstructor(privatereadonlydataSource:DataSource){}awaitthis.dataSource.query(SELECT 1);适用窗口函数、CTE、特定 PG 语法一次性数据修复ORM 生成的 SQL 明显更差不要把整站 CRUD 都写成字符串。能 Repository 就 Repository能 QueryBuilder 就不要上裸 SQL。6. 一张图请求怎么打到数据库GET /api/users → UsersController.findAll() → UsersService.findAll() → repository.find() → TypeORM 生成 SQL → 驱动发给 PostgreSQL对照本系列层职责ControllerHTTPService业务 调用 RepositoryTypeOrmModule.forFeature提供RepositoryEntityTypeOrmModule.forRoot连接、连接池不要在 Controller 里InjectRepository直接查库——测试和复用都会变差。复杂项目会再拆UsersRepository类把 QueryBuilder 从 Service 拿走入门一个 Service 足够。7. Migration 是啥和日常 SQL 不是一回事前面说的find()/save()是运行时查询请求进来了读一行、插一行。Migration迁移是另一类 SQL改表结构而且要可重复、可版本化。可以把它想成数据库的 Git代码有 git log谁在哪天改了什么 数据库有 migrations 文件夹谁在哪天加了哪张表、哪一列为什么不能靠synchronizesynchronize: true的意思是启动时看实体和库不一样就自动 ALTER。问题是你本地删了实体上的一个字段生产库对应列可能被直接丢掉同事 A、B 的库会被改成不同样子很难对齐没有「这一步改了什么」的记录出问题不好回滚所以生产关 synchronize改结构走 migration。一次 migration 长什么样就是一份带版本号的 SQL 文件例如「给 users 表加一列」-- 20260810120000_add_user_prefsALTERTABLEusersADDCOLUMNIFNOTEXISTSgeneration_preferences_jsonJSONB;第一次建库则是CREATE TABLE users (...)。这些文件提交进 Git。新同事或新服务器执行「跑 migration」库就会一步步追上代码。和 Service 里 SQL 的分工日常查询ServiceMigration何时跑每个 HTTP 请求都可能部署时跑一次或几份未执行的文件典型语句SELECT/INSERT/UPDATE一行业务数据CREATE TABLE/ALTER TABLE/CREATE INDEX写在哪UsersService的 Repository / QueryBuildermigrations/*.sql或 TypeORM 生成的 ts例子登录时按 email 查用户给 users 增加generation_preferences_json列一句话Service 改的是表里的数据migration 改的是表长什么样。TypeORM 常用typeorm migration:generate根据实体差异生成文件再migration:run打到库上。Prisma 则是改schema.prisma后prisma migrate效果相同多一份带时间戳的 SQL。入门先记住「有这么一层」不必把命令背熟。8. 和 Prisma 差在哪同一套 Nest 分层Prisma 没有Entity()表写在schema.prismadatasource db { provider postgresql url env(DATABASE_URL) } model User { id String id email String unique name String map(users) }Nest 里常包一层客户端启动时$connect()Injectable()exportclassPrismaServiceextendsPrismaClientimplementsOnModuleInit{asynconModuleInit(){awaitthis.$connect();}}Service 里查询变成this.prisma.user.findMany();this.prisma.user.findUnique({where:{email}});TypeORMPrisma表结构Entity()类schema.prisma连库TypeOrmModule.forRootPrismaClient.$connect多放onModuleInit日常查询Repository/ QueryBuilderprisma.user.findMany()裸 SQLrepository.query()/dataSource.query()prisma.$queryRaw改表结构TypeORM migrationprisma migrateSQL 写在哪都在 Service或 Repository不在 Controller同左换 ORM不换 Nest 分层。选 TypeORM 往往因为装饰器实体、和 Nest 官方模块集成熟选 Prisma 往往因为 schema 集中、类型生成舒服。入门先把「查询待在 Service」养成即可。9. 常见坑生产打开synchronize: true实体删字段库里的列可能被丢掉。Controller 里写repository.find和第一篇「Controller 薄、Service 厚」对着干。字符串拼接 SQLquery(... WHERE id id)有注入风险用参数绑定。实体随手exports给所有模块乱注入 Repository优先导出 Service表访问收口。把业务规则写成一段 80 行裸 SQL先 QueryBuilder实在表达不清再局部裸 SQL并加注释说明为什么。10. 小结连库TypeOrmModule.forRoot或forRootAsyncDATABASE_URL表Entity()模块内forFeature([User])才能注入RepositoryUser日常 SQL 写在 Servicefind/save复杂用 QueryBuilder方言/报表才裸 SQL改表结构用 migration不要生产开synchronizeController 不写 SQL也不直接碰 RepositoryPrisma 只是换了一套 API分层不变对照本系列请求谁接、业务谁做、模块怎么导出启动时谁连库TypeORM 根模块 / Prisma 的onModuleInit这句 SELECT/INSERT 落在哪一层有没有跑到 Controller 里系列导航上一篇NestJS 入门8环境变量与配置第七篇NestJS 入门7生命周期钩子第六篇NestJS 入门6Module 边界与导出第五篇NestJS 入门5Pipe 与 DTO 校验第四篇NestJS 入门4统一响应与异常处理第三篇NestJS 入门3Guard 如何挡住未登录请求第二篇NestJS 入门2依赖注入到底解决了什么问题第一篇NestJS 入门1先搞懂 Module、Controller、Service

相关新闻

Lynx-Stack路由管理:实现复杂应用的页面导航

Lynx-Stack路由管理:实现复杂应用的页面导航

Lynx-Stack路由管理:实现复杂应用的页面导航 【免费下载链接】lynx-stack The frontend framework and toolchain of Lynx 项目地址: https://gitcode.com/gh_mirrors/ly/lynx-stack Lynx-Stack作为现代前端框架和工具链,提供了强大的路由管理功能…

2026/10/13 16:17:34 阅读更多 →
从Lean URL到Neat URL:插件进化史与功能对比

从Lean URL到Neat URL:插件进化史与功能对比

从Lean URL到Neat URL:插件进化史与功能对比 【免费下载链接】Neat-URL Neat URL cleans URLs, removing parameters such as Google Analytics utm parameters. 项目地址: https://gitcode.com/gh_mirrors/ne/Neat-URL Neat URL是一款强大的URL清理工具&am…

2026/10/13 16:17:34 阅读更多 →
CartoCSS错误处理与调试:常见问题解决方案与最佳实践

CartoCSS错误处理与调试:常见问题解决方案与最佳实践

CartoCSS错误处理与调试:常见问题解决方案与最佳实践 【免费下载链接】carto fast CSS-like map stylesheets 项目地址: https://gitcode.com/gh_mirrors/ca/carto CartoCSS作为一种类CSS的地图样式表语言,在快速构建地图视觉效果时可能会遇到各种…

2026/10/4 2:06:51 阅读更多 →

最新新闻

MySQL 4.0 实战指南:老版本环境搭建、避坑与迁移要点

MySQL 4.0 实战指南:老版本环境搭建、避坑与迁移要点

简介:《MySQL 4.0.pdf》是一份聚焦MySQL 4.0版本的开源关系型数据库参考资料,适合数据库管理员、后端开发人员及系统学习数据库原理的读者,用于理解经典版本的功能体系与运维思路。资源为单个PDF文件,压缩包仅334KB,内…

2026/10/13 16:17:31 阅读更多 →
C++刷题指南:双指针反转、区间反转与原地扩容的字符串三题精讲

C++刷题指南:双指针反转、区间反转与原地扩容的字符串三题精讲

最近又刷了一轮力扣字符串题,把 344、541 和一道很经典的“替换数字”放到一起过了三遍。这三题单独看都不难,C 解法也都不超过 30 行,但把它们串在一起刷,你会发现它们正好覆盖了字符串操作中最容易被忽略的三类基本功&#xff1…

2026/10/13 16:17:31 阅读更多 →
Linux VFS核心架构深度拆解:从root device报错到数据落盘

Linux VFS核心架构深度拆解:从root device报错到数据落盘

前阵子帮朋友调一块嵌入式板子,内核起来后卡在了一行红字上:VFS: Cannot open root device "ram" or unknown-block(1,0): error -6折腾了一下午,最后发现只是内核配置没开 ramdisk 驱动,外加启动参数里 root 写得太想当…

2026/10/13 16:17:31 阅读更多 →
Cocos Creator房卡麻将源码拆解:状态机、洗牌算法与胡牌判断实战

Cocos Creator房卡麻将源码拆解:状态机、洗牌算法与胡牌判断实战

简介:一套基于CocosCreator引擎开发的房卡麻将棋牌游戏完整源码,面向Cocos Creator游戏开发者与棋牌类项目学习者。该项目围绕房卡模式展开,覆盖玩家创建私密房间、邀请好友开局、房卡购买与发放等核心链路,可作为从零搭建棋牌游戏…

2026/10/13 16:17:30 阅读更多 →
Linux VFS虚拟文件系统:从启动报错到架构与实战剖析

Linux VFS虚拟文件系统:从启动报错到架构与实战剖析

1. VFS到底是什么:从一次启动报错说起如果你玩过Linux内核开发或者自己折腾过嵌入式系统,大概率遇到过这么一行让人血压升高的日志:VFS: Cannot open root device "ram" or unknown-block(1,0): error -6 Please append a correct …

2026/10/13 16:17:30 阅读更多 →
基于SpringBoot+Vue的西安旅游系统管理平台设计与实现

基于SpringBoot+Vue的西安旅游系统管理平台设计与实现

每年到这个时间点,总有不少同学私信问我同一个问题:毕设选题到底选什么,既能赶紧把东西做出来,又不至于在答辩的时候被问得哑口无言。如果你正在发愁这件事,那这套基于SpringBootVue的西安旅游系统管理平台&#xff0c…

2026/10/13 16:16:29 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →