前兩天整理完 PostgreSQL,今天再繼續整理更進階的 TypeORM,簡而言之,PostgreSQL 是存資料的地方,TypeORM 是幫你「用 TypeScript 操作 PostgreSQL」的工具。
TypeORM 是 TypeScript 和 JavaScript 的 ORM(Object-Relational Mapping)框架,讓你可以用物件導向的方式操作資料庫。
不用 ORM(原生 SQL):
const result = await client.query(
'SELECT * FROM users WHERE email = $1',
['alice@example.com']
);
const user = result.rows[0];
使用 TypeORM:
const user = await userRepository.findOne({
where: { email: 'alice@example.com' }
});
優點:
缺點:
npm install typeorm reflect-metadata pg
npm install --save-dev @types/node
套件說明:
typeorm: ORM 核心reflect-metadata: 裝飾器需要pg: PostgreSQL 驅動src/
├── config/
│ └── database.ts # 資料庫設定
├── entities/
│ ├── User.ts # User Entity
│ ├── Post.ts # Post Entity
│ └── Tag.ts # Tag Entity
├── migrations/ # 資料庫遷移檔案
└── index.ts # 主程式
// src/config/database.ts
import { DataSource } from 'typeorm';
import { User } from '../entities/User';
import { Post } from '../entities/Post';
import { Tag } from '../entities/Tag';
export const AppDataSource = new DataSource({
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
username: process.env.DB_USER || 'myuser',
password: process.env.DB_PASSWORD || 'mysecretpassword',
database: process.env.DB_NAME || 'mydb',
// 開發環境自動同步(生產環境請設為 false)
synchronize: process.env.NODE_ENV === 'development',
// 顯示 SQL 語句(除錯用)
logging: process.env.NODE_ENV === 'development',
// Entity 檔案位置
entities: [User, Post, Tag],
// Migration 檔案位置
migrations: ['src/migrations/*.ts'],
});
# .env
DB_HOST=localhost
DB_PORT=5432
DB_USER=myuser
DB_PASSWORD=mysecretpassword
DB_NAME=mydb
NODE_ENV=development
Entity 就是資料表的物件表示。
// src/entities/User.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('users') // 對應資料表名稱
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column()
name: string;
@Column({ nullable: true })
age: number;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn() // 自動設定建立時間
createdAt: Date;
@UpdateDateColumn() // 自動更新時間
updatedAt: Date;
}
@Column() // 一般欄位
@Column({ nullable: true }) // 可為 null
@Column({ unique: true }) // 唯一值
@Column({ default: 'active' }) // 預設值
@Column({ length: 100 }) // 字串長度
@Column('text') // 指定型別
@Column({ type: 'decimal', precision: 10, scale: 2 }) // 小數
@PrimaryGeneratedColumn() // 自動遞增主鍵
@PrimaryGeneratedColumn('uuid') // UUID 主鍵
@CreateDateColumn() // 建立時間(自動)
@UpdateDateColumn() // 更新時間(自動)
// src/index.ts
import 'reflect-metadata';
import { AppDataSource } from './config/database';
const main = async () => {
try {
// 初始化資料庫連線
await AppDataSource.initialize();
console.log('資料庫連線成功');
// 你的程式邏輯...
} catch (error) {
console.error('資料庫連線失敗:', error);
}
};
main();
import { AppDataSource } from './config/database';
import { User } from './entities/User';
const userRepository = AppDataSource.getRepository(User);
// 方式 1:create + save
const user = userRepository.create({
email: 'alice@example.com',
name: 'Alice',
age: 25,
});
await userRepository.save(user);
// 方式 2:直接 save
await userRepository.save({
email: 'bob@example.com',
name: 'Bob',
age: 30,
});
// 方式 3:insert(效能較好,但不會觸發事件)
await userRepository.insert({
email: 'charlie@example.com',
name: 'Charlie',
age: 28,
});
// 批次新增
await userRepository.save([
{ email: 'user1@example.com', name: 'User1' },
{ email: 'user2@example.com', name: 'User2' },
]);
// 查詢所有
const users = await userRepository.find();
// 查詢單筆(找不到回傳 null)
const user = await userRepository.findOne({
where: { email: 'alice@example.com' }
});
// 查詢單筆(找不到拋出錯誤)
const user = await userRepository.findOneOrFail({
where: { id: '123' }
});
// 條件查詢
const users = await userRepository.find({
where: { isActive: true }
});
// 多重條件(AND)
const users = await userRepository.find({
where: {
isActive: true,
age: 25
}
});
// 多重條件(OR)
import { Or } from 'typeorm';
const users = await userRepository.find({
where: [
{ age: 25 },
{ age: 30 }
]
});
// 大於、小於
import { MoreThan, LessThan } from 'typeorm';
const users = await userRepository.find({
where: { age: MoreThan(25) }
});
// 模糊搜尋
import { Like } from 'typeorm';
const users = await userRepository.find({
where: { name: Like('%Alice%') }
});
// 排序
const users = await userRepository.find({
order: { createdAt: 'DESC' }
});
// 限制筆數
const users = await userRepository.find({
take: 10, // LIMIT 10
skip: 20, // OFFSET 20
});
// 選擇欄位
const users = await userRepository.find({
select: ['id', 'name', 'email']
});
// 計數
const count = await userRepository.count({
where: { isActive: true }
});
// 方式 1:先查詢再更新
const user = await userRepository.findOne({
where: { email: 'alice@example.com' }
});
if (user) {
user.name = 'Alice Smith';
user.age = 26;
await userRepository.save(user);
}
// 方式 2:直接更新(效能較好)
await userRepository.update(
{ email: 'alice@example.com' }, // 條件
{ name: 'Alice Smith', age: 26 } // 更新內容
);
// 方式 3:使用 Query Builder
await userRepository
.createQueryBuilder()
.update(User)
.set({ age: 27 })
.where('email = :email', { email: 'alice@example.com' })
.execute();
// 批次更新
await userRepository.update(
{ isActive: false },
{ isActive: true }
);
// 方式 1:先查詢再刪除
const user = await userRepository.findOne({
where: { email: 'alice@example.com' }
});
if (user) {
await userRepository.remove(user);
}
// 方式 2:直接刪除(效能較好)
await userRepository.delete({
email: 'alice@example.com'
});
// 方式 3:軟刪除(只標記,不真的刪除)
// 先在 Entity 加上 @DeleteDateColumn()
await userRepository.softDelete({
email: 'alice@example.com'
});
// 恢復軟刪除的資料
await userRepository.restore({
email: 'alice@example.com'
});
// 批次刪除
await userRepository.delete({
isActive: false
});
一個使用者有多篇文章:
// src/entities/User.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { Post } from './Post';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column({ unique: true })
email: string;
// 一個 user 有多個 posts
@OneToMany(() => Post, (post) => post.user)
posts: Post[];
}
// src/entities/Post.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { User } from './User';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@Column('text')
content: string;
// 多個 posts 屬於一個 user
@ManyToOne(() => User, (user) => user.posts, {
onDelete: 'CASCADE' // 刪除 user 時連帶刪除 posts
})
user: User;
@Column()
userId: string; // 外鍵欄位
}
使用範例:
// 新增文章
const user = await userRepository.findOne({
where: { email: 'alice@example.com' }
});
const post = postRepository.create({
title: '我的第一篇文章',
content: '內容...',
user: user, // 或 userId: user.id
});
await postRepository.save(post);
// 查詢使用者及其所有文章
const user = await userRepository.findOne({
where: { email: 'alice@example.com' },
relations: ['posts'] // 載入關聯資料
});
console.log(user.posts); // 使用者的所有文章
// 查詢文章及其作者
const post = await postRepository.findOne({
where: { id: '123' },
relations: ['user']
});
console.log(post.user.name); // 作者名稱
一篇文章有多個標籤,一個標籤屬於多篇文章:
// src/entities/Post.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Tag } from './Tag';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@Column('text')
content: string;
// 多對多關係
@ManyToMany(() => Tag, (tag) => tag.posts)
@JoinTable({
name: 'post_tags', // 關聯表名稱
joinColumn: { name: 'post_id' },
inverseJoinColumn: { name: 'tag_id' }
})
tags: Tag[];
}
// src/entities/Tag.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
import { Post } from './Post';
@Entity('tags')
export class Tag {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
name: string;
@ManyToMany(() => Post, (post) => post.tags)
posts: Post[];
}
使用範例:
// 新增標籤
const tag1 = tagRepository.create({ name: 'PostgreSQL' });
const tag2 = tagRepository.create({ name: 'TypeScript' });
await tagRepository.save([tag1, tag2]);
// 為文章加上標籤
const post = await postRepository.findOne({
where: { id: '123' },
relations: ['tags']
});
post.tags = [tag1, tag2];
await postRepository.save(post);
// 查詢文章及其標籤
const posts = await postRepository.find({
relations: ['tags']
});
posts.forEach(post => {
console.log(post.title, post.tags.map(t => t.name));
});
// 查詢特定標籤的所有文章
const tag = await tagRepository.findOne({
where: { name: 'PostgreSQL' },
relations: ['posts']
});
console.log(tag.posts);
當關聯查詢較複雜時,使用 Query Builder:
// 複雜的 JOIN 查詢
const posts = await postRepository
.createQueryBuilder('post')
.leftJoinAndSelect('post.user', 'user')
.leftJoinAndSelect('post.tags', 'tag')
.where('user.email = :email', { email: 'alice@example.com' })
.andWhere('post.createdAt > :date', { date: new Date('2024-01-01') })
.orderBy('post.createdAt', 'DESC')
.take(10)
.getMany();
// 聚合查詢
const result = await postRepository
.createQueryBuilder('post')
.select('user.name', 'author')
.addSelect('COUNT(post.id)', 'postCount')
.leftJoin('post.user', 'user')
.groupBy('user.name')
.getRawMany();
// 子查詢
const posts = await postRepository
.createQueryBuilder('post')
.where((qb) => {
const subQuery = qb
.subQuery()
.select('user.id')
.from(User, 'user')
.where('user.isActive = :active', { active: true })
.getQuery();
return 'post.userId IN ' + subQuery;
})
.getMany();
Migration 用來管理資料庫結構變更,類似版本控制。
// src/config/database.ts
export const AppDataSource = new DataSource({
// ...其他設定
// 生產環境必須關閉 synchronize
synchronize: false,
// Migration 設定
migrations: ['src/migrations/*.ts'],
migrationsTableName: 'migrations',
});
# 根據 Entity 變更自動產生 Migration
npx typeorm migration:generate src/migrations/CreateUsers -d src/config/database.ts
# 手動建立空白 Migration
npx typeorm migration:create src/migrations/AddEmailIndex
// src/migrations/1234567890-CreateUsers.ts
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateUsers1234567890 implements MigrationInterface {
// 執行遷移
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
"email" varchar(255) UNIQUE NOT NULL,
"name" varchar(100) NOT NULL,
"created_at" TIMESTAMP DEFAULT now()
)
`);
// 建立索引
await queryRunner.query(`
CREATE INDEX "idx_users_email" ON "users" ("email")
`);
}
// 回滾遷移
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "idx_users_email"`);
await queryRunner.query(`DROP TABLE "users"`);
}
}
# 執行所有待執行的 Migration
npx typeorm migration:run -d src/config/database.ts
# 回滾最後一個 Migration
npx typeorm migration:revert -d src/config/database.ts
# 查看 Migration 狀態
npx typeorm migration:show -d src/config/database.ts
// 好的做法:小步前進
// Migration 1: 新增欄位
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ADD "age" integer`);
}
// Migration 2: 設定預設值
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`UPDATE "users" SET "age" = 0 WHERE "age" IS NULL`);
}
// Migration 3: 設定 NOT NULL
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "age" SET NOT NULL`);
}
// 不好的做法:一次做太多事
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "users"
ADD "age" integer NOT NULL DEFAULT 0
`);
}
// 錯誤:直接 import 會造成循環依賴
import { User } from './User';
@ManyToOne(User, (user) => user.posts)
user: User;
// 正確:使用箭頭函式延遲載入
import { User } from './User';
@ManyToOne(() => User, (user) => user.posts)
user: User;
// N+1 查詢問題
const posts = await postRepository.find();
for (const post of posts) {
// 若 relation 沒被 preload,這裡對每個 post 觸發額外查詢
console.log(post.user.name);
}
// 使用 relations 一次載入
const posts = await postRepository.find({
relations: ['user']
});
for (const post of posts) {
console.log(post.user.name);
}
// 錯誤:直接修改不會儲存
const user = await userRepository.findOne({ where: { id: '123' } });
user.name = 'New Name'; // 只改變記憶體物件
// 沒呼叫 save → DB 不會更新
// 正確:要呼叫 save
const user = await userRepository.findOne({ where: { id: '123' } });
if (user) {
user.name = 'New Name';
await userRepository.save(user); // 把變更寫回 DB
}