全书目录

第二十四篇:TypeORM 完全指南 —— 数据映射施工队宇宙

第十章:一个完整示例 —— 从蓝图到班组到事务到改造单

1 分钟 261 字 第 442 / 962 个阅读单元

下面给一个完整示例,把 TypeORM 的主线一次串起来。

#1. data-source.ts
ts
import "reflect-metadata";
import { DataSource } from "typeorm";
import { AuditLog } from "./entities/AuditLog";
import { Post } from "./entities/Post";
import { User } from "./entities/User";

export const AppDataSource = new DataSource({
  type: "postgres",
  host: "localhost",
  port: 5432,
  username: "city",
  password: "city",
  database: "city_blog",
  synchronize: false,
  logging: false,
  entities: [User, Post, AuditLog],
  migrations: ["src/migrations/*.{ts,js}"],
});
#2. entities/User.ts
ts
import {
  Column,
  CreateDateColumn,
  Entity,
  OneToMany,
  PrimaryGeneratedColumn,
} from "typeorm";
import { Post } from "./Post";

@Entity({ name: "users" })
export class User {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column({ unique: true })
  email!: string;

  @Column({ length: 50 })
  nickname!: string;

  @Column({ default: true })
  isActive!: boolean;

  @OneToMany(() => Post, (post) => post.author)
  posts!: Post[];

  @CreateDateColumn()
  createdAt!: Date;
}
#3. entities/Post.ts
ts
import {
  Column,
  CreateDateColumn,
  Entity,
  ManyToOne,
  PrimaryGeneratedColumn,
} from "typeorm";
import { User } from "./User";

@Entity({ name: "posts" })
export class Post {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  title!: string;

  @Column({ type: "text" })
  content!: string;

  @Column({ default: false })
  published!: boolean;

  @ManyToOne(() => User, (user) => user.posts, {
    nullable: false,
    onDelete: "CASCADE",
  })
  author!: User;

  @CreateDateColumn()
  createdAt!: Date;
}
#4. entities/AuditLog.ts
ts
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity({ name: "audit_logs" })
export class AuditLog {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  action!: string;

  @Column()
  targetType!: string;

  @Column()
  targetId!: number;

  @CreateDateColumn()
  createdAt!: Date;
}
#5. 业务代码:创建作者、发布文章、记录日志
ts
import { AppDataSource } from "./data-source";
import { AuditLog } from "./entities/AuditLog";
import { Post } from "./entities/Post";
import { User } from "./entities/User";

async function main() {
  await AppDataSource.initialize();

  const userRepository = AppDataSource.getRepository(User);
  const postRepository = AppDataSource.getRepository(Post);

  let author = await userRepository.findOneBy({
    email: "lin@city.dev",
  });

  if (!author) {
    author = userRepository.create({
      email: "lin@city.dev",
      nickname: "Lin",
    });

    await userRepository.save(author);
  }

  const draft = postRepository.create({
    title: "TypeORM 是施工队,不是魔法棒",
    content: "先把实体和关系建清楚,再谈查询与迁移。",
    author,
  });

  await postRepository.save(draft);

  const publishedPost = await AppDataSource.transaction(async (manager) => {
    draft.published = true;
    await manager.save(draft);

    const log = manager.create(AuditLog, {
      action: "PUBLISH_POST",
      targetType: "Post",
      targetId: draft.id,
    });

    await manager.save(log);

    return manager
      .getRepository(Post)
      .createQueryBuilder("post")
      .leftJoinAndSelect("post.author", "author")
      .where("post.id = :id", { id: draft.id })
      .getOneOrFail();
  });

  console.log("发布后的文章:", publishedPost);

  const latestPosts = await postRepository
    .createQueryBuilder("post")
    .leftJoinAndSelect("post.author", "author")
    .where("post.published = :published", { published: true })
    .andWhere("author.isActive = :isActive", { isActive: true })
    .orderBy("post.createdAt", "DESC")
    .take(5)
    .getMany();

  console.log("最近 5 篇已发布文章:", latestPosts);

  await AppDataSource.destroy();
}

main().catch(async (error) => {
  console.error(error);
  if (AppDataSource.isInitialized) {
    await AppDataSource.destroy();
  }
  process.exit(1);
});

这个示例里已经串起了:

  • DataSource
  • Entity
  • Column
  • Relation
  • Repository
  • QueryBuilder
  • Transaction

还差最后一块:Migration

#6. 一份配套 migration
ts
import { MigrationInterface, QueryRunner } from "typeorm";

export class InitBlogTables1710000000001 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      CREATE TABLE "users" (
        "id" SERIAL PRIMARY KEY,
        "email" VARCHAR NOT NULL UNIQUE,
        "nickname" VARCHAR(50) NOT NULL,
        "isActive" BOOLEAN NOT NULL DEFAULT true,
        "createdAt" TIMESTAMP NOT NULL DEFAULT now()
      )
    `);

    await queryRunner.query(`
      CREATE TABLE "posts" (
        "id" SERIAL PRIMARY KEY,
        "title" VARCHAR NOT NULL,
        "content" TEXT NOT NULL,
        "published" BOOLEAN NOT NULL DEFAULT false,
        "createdAt" TIMESTAMP NOT NULL DEFAULT now(),
        "authorId" INTEGER NOT NULL REFERENCES "users"("id") ON DELETE CASCADE
      )
    `);

    await queryRunner.query(`
      CREATE TABLE "audit_logs" (
        "id" SERIAL PRIMARY KEY,
        "action" VARCHAR NOT NULL,
        "targetType" VARCHAR NOT NULL,
        "targetId" INTEGER NOT NULL,
        "createdAt" TIMESTAMP NOT NULL DEFAULT now()
      )
    `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE "audit_logs"`);
    await queryRunner.query(`DROP TABLE "posts"`);
    await queryRunner.query(`DROP TABLE "users"`);
  }
}

这就是完整施工链:

text
实体蓝图
  ▼
DataSource 接入工地
  ▼
Repository / QueryBuilder 干活
  ▼
Transaction 保证联动施工
  ▼
Migration 管理结构改造