拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门