🗃️

TypeORM の Entity 定義:基本からリレーションまで

に公開

TypeORMのEntityとは

TypeORMのEntityは、データベーステーブルとTypeScriptクラスをマッピングするためのクラスです。

Entityの役割

  • データベーステーブルの構造を定義
  • カラムの型や制約を指定
  • テーブル間のリレーションを表現
  • オブジェクト指向のデータ操作を実現

基本的なEntity定義

最小構成のEntity

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  name: string

  @Column()
  email: string
}

主要なデコレータ

デコレータ 説明
@Entity() クラスがEntityであることを宣言
@PrimaryGeneratedColumn() 自動採番の主キー
@PrimaryColumn() 手動設定の主キー
@Column() 通常のカラム
@CreateDateColumn() 作成日時(自動設定)
@UpdateDateColumn() 更新日時(自動更新)
@DeleteDateColumn() 削除日時(ソフトデリート用)

カラム定義の詳細

カラムの型指定

@Entity()
export class Product {
  @PrimaryGeneratedColumn()
  id: number

  // 文字列型
  @Column('varchar', { length: 255 })
  name: string

  // 数値型(整数)
  @Column('int', { unsigned: true })
  price: number

  // 数値型(小数)
  @Column('decimal', { precision: 10, scale: 2 })
  weight: number

  // 真偽値
  @Column('boolean', { default: true })
  isActive: boolean

  // テキスト
  @Column('text')
  description: string

  // 日付
  @Column('date')
  publishedAt: Date

  // JSON
  @Column('json', { nullable: true })
  metadata: object
}

NULLを許容するカラム

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  name: string

  // NULLを許容
  @Column({ nullable: true })
  phoneNumber: string | null

  // デフォルト値を設定
  @Column({ default: 'user' })
  role: string

  // NULLの初期値を明示(TypeScript側の型安全性のため)
  @Column({ nullable: true })
  bio: string | null = null
}

タイムスタンプカラム

@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  title: string

  // 作成日時(INSERT時に自動設定)
  @CreateDateColumn()
  readonly createdAt: Date

  // 更新日時(UPDATE時に自動更新)
  @UpdateDateColumn()
  readonly updatedAt: Date

  // 削除日時(ソフトデリート用)
  @DeleteDateColumn()
  readonly deletedAt: Date | null
}

リレーションの定義

One-to-One(1対1)

// User Entity
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  name: string

  @OneToOne(() => Profile, (profile) => profile.user, {
    cascade: true, // Userを保存時にProfileも自動保存
  })
  @JoinColumn() // 外部キーを持つ側に付ける
  profile: Profile
}

// Profile Entity
@Entity()
export class Profile {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  bio: string

  @OneToOne(() => User, (user) => user.profile)
  user: User
}

One-to-Many / Many-to-One(1対多)

// User Entity(親)
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  name: string

  @OneToMany(() => Post, (post) => post.user, {
    cascade: true, // Userを保存時にPostも自動保存
  })
  posts: Post[]
}

// Post Entity(子)
@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  title: string

  @ManyToOne(() => User, (user) => user.posts, {
    onDelete: 'CASCADE', // Userが削除されたらPostも削除
  })
  @JoinColumn({ name: 'user_id' })
  user: User

  @Column()
  userId: number // 外部キーのカラム
}

Many-to-Many(多対多)

// Student Entity
@Entity()
export class Student {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  name: string

  @ManyToMany(() => Course, (course) => course.students)
  @JoinTable({ // 中間テーブルを作成する側に付ける
    name: 'student_courses',
    joinColumn: { name: 'student_id' },
    inverseJoinColumn: { name: 'course_id' },
  })
  courses: Course[]
}

// Course Entity
@Entity()
export class Course {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  title: string

  @ManyToMany(() => Student, (student) => student.courses)
  students: Student[]
}

リレーションオプションの詳細

cascade(カスケード)

親エンティティの操作を子エンティティに伝播させます。

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number

  @OneToMany(() => Post, (post) => post.user, {
    cascade: true, // insert, update, remove すべて
    // または個別に指定
    // cascade: ['insert', 'update']
  })
  posts: Post[]
}

// 使用例
const user = new User()
user.posts = [new Post(), new Post()]
await repository.save(user) // Userと一緒にPostsも保存される

eager / lazy(読み込み方式)

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number

  // Eager Loading: Userを取得時に自動でPostsも取得
  @OneToMany(() => Post, (post) => post.user, {
    eager: true,
  })
  posts: Post[]
}

// Lazy Loading
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number

  // Promise型にする
  @OneToMany(() => Post, (post) => post.user)
  posts: Promise<Post[]>
}

// 使用例
const user = await userRepository.findOne({ where: { id: 1 } })
const posts = await user.posts // Promiseをawaitで解決

onDelete / onUpdate(外部キー制約)

@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  id: number

  @ManyToOne(() => User, (user) => user.posts, {
    onDelete: 'CASCADE',    // Userが削除されたらPostも削除
    // onDelete: 'SET NULL', // Userが削除されたらuser_idをNULLに
    // onDelete: 'RESTRICT', // Postが存在する場合はUserを削除不可
    onUpdate: 'CASCADE',    // UserのIDが変更されたらuser_idも更新
  })
  @JoinColumn({ name: 'user_id' })
  user: User
}

パフォーマンス最適化

createForeignKeyConstraints

外部キー制約を作成しないことでパフォーマンスを向上させます。

@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  id: number

  @ManyToOne(() => User, (user) => user.posts, {
    createForeignKeyConstraints: false, // 外部キー制約を作成しない
  })
  @JoinColumn({ name: 'user_id' })
  user: User
}

注意点

  • データ整合性はアプリケーション側で保証する必要がある
  • 高速な書き込みが必要な場合に有効

persistence

保存時の余分なクエリを抑制します。

@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  id: number

  @ManyToOne(() => User, (user) => user.posts, {
    persistence: false, // 保存時にリレーションを永続化しない
  })
  @JoinColumn({ name: 'user_id' })
  user: User

  @Column()
  userId: number // 外部キーは直接操作
}

// 使用例
const post = new Post()
post.title = 'Hello'
post.userId = 1 // userオブジェクトではなく、直接IDを設定
await repository.save(post) // 余分なクエリが発生しない

Constructorの定義

オブジェクトの不変条件を満たすために、必須フィールドを受け取るConstructorを定義します。

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  readonly id: number

  @Column()
  name: string

  @Column()
  email: string

  @Column({ default: true })
  isActive: boolean = true

  @CreateDateColumn()
  readonly createdAt: Date

  constructor(name: string, email: string) {
    this.name = name
    this.email = email
  }
}

// 使用例
const user = new User('John Doe', 'john@example.com')
await repository.save(user)

MySQLの型対応表

TypeScript MySQL TypeORMの型
number TINYINT 'tinyint'
number SMALLINT 'smallint'
number INT 'int'
number BIGINT 'bigint'
number DECIMAL 'decimal'
number FLOAT 'float'
string VARCHAR 'varchar'
string TEXT 'text'
boolean TINYINT(1) 'boolean'
Date DATETIME 'datetime'
Date TIMESTAMP 'timestamp'
object JSON 'json'

ベストプラクティス

1. readonlyの活用

自動生成されるフィールドや変更されないフィールドにはreadonlyを付けます。

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  readonly id: number // 自動生成されるIDは変更不可

  @CreateDateColumn()
  readonly createdAt: Date // 作成日時は変更不可
}

2. NULLの明示的な初期化

TypeScriptの型安全性のため、NULL許容カラムには初期値を設定します。

@Column({ nullable: true })
phoneNumber: string | null = null // 明示的にnullで初期化

3. Commentの活用

データベースのカラムにコメントを付けることでドキュメント化できます。

@Column('varchar', { comment: 'ユーザーのメールアドレス' })
email: string

4. unsigned属性(MySQL)

負の値を扱わない場合はunsignedを指定して範囲を拡大します。

@Column('int', { unsigned: true }) // 0 〜 4,294,967,295
price: number

まとめ

TypeORMのEntityを定義する際のポイントは次の通りです。

  • デコレータ @Entity@Columnなどでカラムとリレーションを定義
  • 型指定 TypeScriptとデータベースの型を正しくマッピング
  • リレーション cascade、eager、onDeleteなどのオプションを理解
  • パフォーマンス 外部キー制約や永続化を制御
  • 型安全性 readonly、NULL初期化、Constructorで型安全なコードを実現

Entity定義により、型安全で保守性の高いデータベース操作が可能になります。

参考リンク

GitHubで編集を提案

Discussion