typeorm
TypeScriptおよびJavaScript向けの多機能ORMであるTypeORMを使った開発ガイドラインを提供します。複数のデータベースに対応しており、エンティティ定義やリレーション設計、クエリビルダーの活用など、実践的な実装パターンをサポートします。
description の原文を見る
Guidelines for developing with TypeORM, a full-featured ORM for TypeScript and JavaScript supporting multiple databases
SKILL.md 本文
TypeORM 開発ガイドライン
TypeORM、TypeScript、データベース設計の専門家として活動し、Data Mapper パターンとエンタープライズアプリケーションアーキテクチャに焦点を当てます。
コア原則
- TypeORM は Active Record パターンと Data Mapper パターンの両方をサポート
- TypeScript デコレータを使用してエンティティとカラム定義を実装
- MySQL、PostgreSQL、MariaDB、SQLite、MS SQL Server、Oracle など複数のデータベースをサポート
- Node.js、Browser、Ionic、Cordova、React Native、NativeScript、Expo、Electron で動作
- データベースマイグレーションのファーストクラスサポート
TypeScript の設定
tsconfig.json で必須の設定:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true,
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node"
}
}
エンティティ定義
基本的なエンティティ
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
@Entity("users")
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: "varchar", length: 255, unique: true })
email: string;
@Column({ type: "varchar", length: 255, nullable: true })
name: string | null;
@Column({ type: "boolean", default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
主キーのオプション
// 自動インクリメント
@PrimaryGeneratedColumn()
id: number;
// UUID
@PrimaryGeneratedColumn("uuid")
id: string;
// カスタム主キー
@PrimaryColumn()
id: string;
// 複合主キー
@Entity()
export class OrderItem {
@PrimaryColumn()
orderId: number;
@PrimaryColumn()
productId: number;
}
カラムデコレータ
@Entity()
export class Product {
@PrimaryGeneratedColumn()
id: number;
// 文字列カラム
@Column({ type: "varchar", length: 255 })
name: string;
@Column({ type: "text", nullable: true })
description: string | null;
// 数値カラム
@Column({ type: "decimal", precision: 10, scale: 2 })
price: number;
@Column({ type: "int", default: 0 })
stock: number;
// ブール値
@Column({ type: "boolean", default: true })
isAvailable: boolean;
// JSON
@Column({ type: "jsonb", nullable: true })
metadata: Record<string, any> | null;
// 列挙型
@Column({
type: "enum",
enum: ["active", "inactive", "pending"],
default: "pending",
})
status: "active" | "inactive" | "pending";
// タイムスタンプ
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@DeleteDateColumn()
deletedAt: Date | null; // ソフトデリート用
// オプティミスティックロック用のバージョンカラム
@VersionColumn()
version: number;
}
リレーションシップ
一対一
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@OneToOne(() => Profile, (profile) => profile.user, { cascade: true })
@JoinColumn()
profile: Profile;
}
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
bio: string;
@OneToOne(() => User, (user) => user.profile)
user: User;
}
一対多 / 多対一
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
}
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@ManyToOne(() => User, (user) => user.posts, { onDelete: "CASCADE" })
@JoinColumn({ name: "author_id" })
author: User;
@Column()
authorId: number; // 明示的な外部キーカラム
}
多対多
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@ManyToMany(() => Tag, (tag) => tag.posts)
@JoinTable({
name: "post_tags",
joinColumn: { name: "post_id" },
inverseJoinColumn: { name: "tag_id" },
})
tags: Tag[];
}
@Entity()
export class Tag {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
name: string;
@ManyToMany(() => Post, (post) => post.tags)
posts: Post[];
}
リポジトリパターン
基本的なリポジトリの使用
import { AppDataSource } from "./data-source";
import { User } from "./entities/User";
const userRepository = AppDataSource.getRepository(User);
// すべて検索
const users = await userRepository.find();
// 条件付きで検索
const activeUsers = await userRepository.find({
where: { isActive: true },
});
// 1 件を検索
const user = await userRepository.findOne({
where: { id: 1 },
});
// 検索または失敗
const user = await userRepository.findOneOrFail({
where: { id: 1 },
});
// 保存
const newUser = userRepository.create({
email: "user@example.com",
name: "John Doe",
});
await userRepository.save(newUser);
// 更新
await userRepository.update({ id: 1 }, { name: "Jane Doe" });
// 削除
await userRepository.delete({ id: 1 });
// ソフトデリート (@DeleteDateColumn が必須)
await userRepository.softDelete({ id: 1 });
カスタムリポジトリ
import { Repository, DataSource } from "typeorm";
import { User } from "./entities/User";
export class UserRepository extends Repository<User> {
constructor(private dataSource: DataSource) {
super(User, dataSource.createEntityManager());
}
async findByEmail(email: string): Promise<User | null> {
return this.findOne({ where: { email } });
}
async findActiveUsers(): Promise<User[]> {
return this.find({
where: { isActive: true },
order: { createdAt: "DESC" },
});
}
async findWithPosts(userId: number): Promise<User | null> {
return this.findOne({
where: { id: userId },
relations: ["posts"],
});
}
}
Query Builder
const users = await userRepository
.createQueryBuilder("user")
.leftJoinAndSelect("user.posts", "post")
.where("user.isActive = :isActive", { isActive: true })
.andWhere("post.publishedAt IS NOT NULL")
.orderBy("user.createdAt", "DESC")
.skip(0)
.take(10)
.getMany();
// 生の結果を使用
const result = await userRepository
.createQueryBuilder("user")
.select("COUNT(*)", "count")
.where("user.isActive = :isActive", { isActive: true })
.getRawOne();
// Query builder でのインサート
await userRepository
.createQueryBuilder()
.insert()
.into(User)
.values([
{ email: "user1@example.com", name: "User 1" },
{ email: "user2@example.com", name: "User 2" },
])
.execute();
Data Source の設定
// data-source.ts
import { DataSource } from "typeorm";
import { User } from "./entities/User";
import { Post } from "./entities/Post";
export const AppDataSource = new DataSource({
type: "postgres",
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "5432"),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
// エンティティ設定
entities: [User, Post],
// または glob パターン: entities: ["src/entities/**/*.ts"]
// マイグレーション
migrations: ["src/migrations/**/*.ts"],
// 同期 - 本番環境では絶対に使用しないこと
synchronize: false,
// ロギング
logging: process.env.NODE_ENV === "development",
// コネクションプール
poolSize: 10,
// SSL (本番環境用)
ssl: process.env.NODE_ENV === "production" ? { rejectUnauthorized: false } : false,
});
// コネクション初期化
AppDataSource.initialize()
.then(() => console.log("Data Source initialized"))
.catch((error) => console.error("Error initializing Data Source:", error));
マイグレーション
マイグレーションの作成
# エンティティの変更からマイグレーションを生成
npx typeorm migration:generate src/migrations/CreateUsers -d src/data-source.ts
# 空のマイグレーションを作成
npx typeorm migration:create src/migrations/SeedUsers
# マイグレーションを実行
npx typeorm migration:run -d src/data-source.ts
# 最後のマイグレーションを戻す
npx typeorm migration:revert -d src/data-source.ts
マイグレーションファイルの構造
import { MigrationInterface, QueryRunner, Table, TableIndex } from "typeorm";
export class CreateUsers1234567890 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: "users",
columns: [
{
name: "id",
type: "int",
isPrimary: true,
isGenerated: true,
generationStrategy: "increment",
},
{
name: "email",
type: "varchar",
length: "255",
isUnique: true,
},
{
name: "name",
type: "varchar",
length: "255",
isNullable: true,
},
{
name: "is_active",
type: "boolean",
default: true,
},
{
name: "created_at",
type: "timestamp",
default: "CURRENT_TIMESTAMP",
},
{
name: "updated_at",
type: "timestamp",
default: "CURRENT_TIMESTAMP",
onUpdate: "CURRENT_TIMESTAMP",
},
],
}),
true
);
await queryRunner.createIndex(
"users",
new TableIndex({
name: "IDX_USERS_EMAIL",
columnNames: ["email"],
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropIndex("users", "IDX_USERS_EMAIL");
await queryRunner.dropTable("users");
}
}
トランザクション
// QueryRunner を使用
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const user = queryRunner.manager.create(User, {
email: "user@example.com",
name: "User",
});
await queryRunner.manager.save(user);
const post = queryRunner.manager.create(Post, {
title: "First Post",
author: user,
});
await queryRunner.manager.save(post);
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
// transaction メソッドを使用
await AppDataSource.transaction(async (manager) => {
const user = manager.create(User, {
email: "user@example.com",
name: "User",
});
await manager.save(user);
const post = manager.create(Post, {
title: "First Post",
author: user,
});
await manager.save(post);
});
NestJS の統合
// app.module.ts
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { User } from "./entities/user.entity";
import { UsersModule } from "./users/users.module";
@Module({
imports: [
TypeOrmModule.forRoot({
type: "postgres",
host: "localhost",
port: 5432,
username: "user",
password: "password",
database: "db",
entities: [User],
synchronize: false,
}),
UsersModule,
],
})
export class AppModule {}
// users/users.module.ts
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
// users/users.service.ts
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
findAll(): Promise<User[]> {
return this.usersRepository.find();
}
findOne(id: number): Promise<User | null> {
return this.usersRepository.findOneBy({ id });
}
}
ベストプラクティス
本番環境ではマイグレーションを使用
本番環境では synchronize: true を絶対に使用しないこと。常にマイグレーションを使用:
// 開発環境: 同期ではなくマイグレーションを使用
synchronize: false,
Eager ロード vs Lazy ロード
// Eager ロード - リレーションを自動的にロード
@OneToMany(() => Post, (post) => post.author, { eager: true })
posts: Post[];
// Lazy ロード - アクセス時にリレーションをロード
@OneToMany(() => Post, (post) => post.author)
posts: Promise<Post[]>;
// 明示的なロード (推奨)
const user = await userRepository.findOne({
where: { id: 1 },
relations: ["posts"],
});
N+1 クエリを回避
// 悪い例: N+1 クエリ
const users = await userRepository.find();
for (const user of users) {
console.log(user.posts); // 各ユーザーごとに個別のクエリ
}
// 良い例: リレーションを Eager ロード
const users = await userRepository.find({
relations: ["posts"],
});
インデックスを使用
@Entity()
@Index(["email"])
@Index(["firstName", "lastName"])
export class User {
@Column()
@Index()
email: string;
@Column()
firstName: string;
@Column()
lastName: string;
}
カスケード操作
@OneToMany(() => Post, (post) => post.author, {
cascade: true, // 関連する投稿を保存/削除
onDelete: "CASCADE", // データベースレベルのカスケード
})
posts: Post[];
ネーミング戦略
TypeScript とデータベース間の一貫したネーミングのため:
import { DefaultNamingStrategy, NamingStrategyInterface } from "typeorm";
import { snakeCase } from "typeorm/util/StringUtils";
export class SnakeNamingStrategy extends DefaultNamingStrategy implements NamingStrategyInterface {
tableName(targetName: string, userSpecifiedName: string | undefined): string {
return userSpecifiedName ? userSpecifiedName : snakeCase(targetName);
}
columnName(propertyName: string, customName: string, embeddedPrefixes: string[]): string {
return snakeCase(embeddedPrefixes.join("_")) + (customName ? customName : snakeCase(propertyName));
}
}
// Data Source 設定で使用
namingStrategy: new SnakeNamingStrategy(),
ライセンス: Apache-2.0(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- mindrally
- リポジトリ
- mindrally/skills
- ライセンス
- Apache-2.0
- 最終更新
- 不明
Source: https://github.com/mindrally/skills / ライセンス: Apache-2.0
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。