TypeORM是目前TypeScript生态中使用最广泛的ORM框架之一,它支持装饰器语法,可以把数据库中的表映射成TypeScript中的类,让开发者用面向对象的方式操作PostgreSQL,而不必手写大量SQL。这篇文章将从零开始搭建一个TypeScript加PostgreSQL的项目,覆盖数据源配置、实体定义、关系映射和常用查询写法,并分享一些实际项目中容易踩到的坑。

一、环境搭建与数据源配置
首先需要准备一个可用的PostgreSQL实例,可以是本地安装的,也可以使用Docker快速启动一个。接着初始化一个TypeScript项目并安装依赖,最常用的组合是typeorm、pg驱动以及reflect-metadata:
npm init -y npm install typeorm pg reflect-metadata npm install -D typescript ts-node @types/node npx tsc --init
TypeORM依赖装饰器元数据,所以必须在应用入口的最顶部引入reflect-metadata,并且在tsconfig.json中启用两个关键选项:experimentalDecorators和emitDecoratorMetadata。如果漏掉这一步,运行时会抛出类似“Experimental support for decorators is a feature that is subject to change”的错误,这是新手最常见的问题之一。
接下来配置数据源。TypeORM提供了DataSource类来管理数据库连接,推荐把它单独放在一个文件中导出,方便其他模块复用:
import "reflect-metadata";
import { DataSource } from "typeorm";
export const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "123456",
database: "test_db",
synchronize: false, // 生产环境务必关闭
logging: true,
entities: ["src/entity/**/*.ts"],
poolSize: 10,
});
// 初始化连接
AppDataSource.initialize()
.then(() => console.log("数据库连接成功"))
.catch((err) => console.error("连接失败:", err));这里有几个参数值得注意。synchronize设为true时,TypeORM会自动根据实体结构同步建表,开发阶段很方便,但生产环境千万不要开启,因为它的同步逻辑比较粗暴,可能造成数据丢失。生产环境建议使用migration迁移脚本管理表结构。另外poolSize控制连接池大小,默认是10,高并发场景下可以根据数据库的最大连接数合理调整,通常应用实例数乘以poolSize要小于PostgreSQL的max_connections配置。
二、实体定义与关系映射
实体是TypeORM的核心概念,一个类对应一张表,类的属性对应表的列。使用@Entity装饰器标记类,用@PrimaryGeneratedColumn、@Column等装饰器描述列:
import {
Entity, PrimaryGeneratedColumn, Column,
CreateDateColumn, UpdateDateColumn,
} from "typeorm";
@Entity("users")
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 50, unique: true })
username: string;
@Column({ type: "varchar", length: 100, select: false })
password: string;
@Column({ type: "int", default: 0 })
age: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}实体定义中有很多细节可以优化。select: false可以让该字段默认不出现在查询结果里,密码这类敏感字段建议加上。日期字段推荐使用@CreateDateColumn和@UpdateDateColumn,TypeORM会自动维护时间戳。PostgreSQL特有的JSON类型也很实用,直接声明type: "jsonb"就能存储结构化数据。
实际项目中表之间必然存在关联。TypeORM通过@OneToMany、@ManyToOne、@OneToOne和@ManyToMany四个装饰器处理关系。以用户和文章的一对多关系为例:
@Entity("articles")
export class Article {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 100 })
title: string;
@Column({ type: "text" })
content: string;
// 多篇文章属于一个作者
@ManyToOne(() => User, (user) => user.articles)
author: User;
@Column()
authorId: number;
}
// User实体中补充反向关系
// @OneToMany(() => Article, (article) => article.author)
// articles: Article[];写关系映射时建议总是把外键列(如authorId)显式声明出来,这样在创建关联数据时可以直接设置ID而不必先查询出完整的关联对象,减少一次数据库往返。查询时配合relations选项或QueryBuilder的leftJoinAndSelect即可一次性取出关联数据,避免N加1查询问题。
三、增删改查与事务处理
TypeORM提供两种主流查询方式:Repository模式和QueryBuilder模式。Repository封装了find、findOne、save、remove等便捷方法,适合大多数常规场景:
import { AppDataSource } from "./data-source";
import { User } from "./entity/User";
const userRepo = AppDataSource.getRepository(User);
// 新增
const user = await userRepo.save({
username: "zhangsan",
password: "hashed_pwd",
age: 25,
});
// 条件查询
const found = await userRepo.find({
where: { age: 25 },
order: { createdAt: "DESC" },
take: 10,
skip: 0,
});
// 更新
await userRepo.update({ id: 1 }, { age: 26 });
// 删除
await userRepo.delete(1);当查询逻辑复杂时,QueryBuilder更灵活,它支持多表连接、子查询、聚合函数和原生SQL片段。需要注意getOne返回可能为null,TypeScript中要做好空值判断:
const result = await AppDataSource
.createQueryBuilder(User, "u")
.leftJoinAndSelect("u.articles", "a")
.where("u.age > :age", { age: 18 })
.andWhere("a.title ILIKE :kw", { kw: "%typeorm%" })
.orderBy("u.id", "ASC")
.getMany();涉及多表写入的操作必须使用事务保证一致性。TypeORM的事务API有三种写法,推荐使用transaction方法的回调形式,出现异常时自动回滚:
await AppDataSource.transaction(async (manager) => {
const user = await manager.save(User, {
username: "lisi",
password: "hashed_pwd",
});
await manager.save(Article, {
title: "第一篇文章",
content: "正文内容",
authorId: user.id,
});
});最后提醒几个容易出错的地方:一是连接失败时优先检查PostgreSQL的pg_hba.conf认证配置;二是save方法带有主键时会执行更新,如果想强制插入请用insert;三是软删除可以配合@DeleteDateColumn使用,调用softDelete后数据不会物理删除,查询时默认被过滤。掌握这些要点后,用TypeORM开发PostgreSQL应用会顺畅很多。
TypeORMPostgreSQLTypeScript修改时间:2026-09-03 06:34:50