Skip to content

Primary_keys

Chapter 8: Primary Keys & Generated Values

Section titled “Chapter 8: Primary Keys & Generated Values”

A primary key uniquely identifies each row in a table. TypeORM provides several ways to define primary keys.

Primary Key Types
================================================================================
Primary Key
|
+-------------------+-------------------+
| | | |
v v v v
+-------+ +----------+ +--------+ +-----------+
| Simple| | Auto-inc | | UUID | | Composite |
| Key | | (Serial) | | | | Key |
+-------+ +----------+ +--------+ +-----------+
| | | |
v v v v
Manual Automatic Globally Multiple
Assignment Generation Unique Columns
ID
================================================================================

Use @PrimaryColumn() when you want to manually assign primary key values.

import { Entity, PrimaryColumn, Column } from 'typeorm';
@Entity('countries')
export class Country {
// String primary key
@PrimaryColumn()
code: string;
// SQL: code VARCHAR(255) PRIMARY KEY
@Column()
name: string;
}
// Usage - you must provide the primary key
const country = new Country();
country.code = 'US'; // Must be set manually
country.name = 'United States';
await repository.save(country);
Use Cases for Manual Primary Keys
+------------------------------------------------------------------+
| |
| Scenario | Example |
| ---------------------|---------------------------------------- |
| Country codes | 'US', 'UK', 'IN' |
| Currency codes | 'USD', 'EUR', 'INR' |
| Status codes | 'ACTIVE', 'PENDING', 'INACTIVE' |
| External IDs | IDs from external systems |
| Natural keys | Email, username (with validation) |
| |
+------------------------------------------------------------------+

Use @PrimaryGeneratedColumn() for auto-generated primary keys.

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('users')
export class User {
// Auto-increment integer (1, 2, 3, ...)
@PrimaryGeneratedColumn()
id: number;
// PostgreSQL: id SERIAL PRIMARY KEY
// MySQL: id INT AUTO_INCREMENT PRIMARY KEY
@Column()
name: string;
}
// Usage - ID is auto-generated
const user = new User();
user.name = 'John Doe';
await repository.save(user);
console.log(user.id); // 1 (auto-assigned)
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('users')
export class User {
// UUID primary key
@PrimaryGeneratedColumn('uuid')
id: string;
// PostgreSQL: id UUID PRIMARY KEY DEFAULT uuid_generate_v4()
// MySQL: id CHAR(36) PRIMARY KEY
@Column()
name: string;
}
// Usage - UUID is auto-generated
const user = new User();
user.name = 'John Doe';
await repository.save(user);
console.log(user.id); // 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('users')
export class User {
// Identity column (PostgreSQL 10+)
@PrimaryGeneratedColumn('identity')
id: number;
// SQL: id GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
@Column()
name: string;
}
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('users')
export class User {
// Row ID (CockroachDB)
@PrimaryGeneratedColumn('rowid')
id: number;
@Column()
name: string;
}

UUID Generation Strategies
+------------------------------------------------------------------+
| |
| Strategy | Description |
| -------------------|------------------------------------------|
| uuid v4 | Random UUID (most common) |
| uuid v1 | Time-based UUID |
| uuid v5 | Namespace-based UUID |
| Database-generated | Generated by database function |
| |
+------------------------------------------------------------------+
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('users')
export class User {
// TypeORM generates UUID before insert
@PrimaryGeneratedColumn('uuid')
id: string;
// Uses uuid v4 by default
}
import { Entity, PrimaryColumn, Column } from 'typeorm';
@Entity('users')
export class User {
// Let database generate UUID
@PrimaryColumn({
type: 'uuid',
default: () => 'uuid_generate_v4()',
})
id: string;
@Column()
name: string;
}
import { Entity, PrimaryColumn, Column, BeforeInsert } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
@Entity('users')
export class User {
@PrimaryColumn({ type: 'uuid' })
id: string;
@Column()
name: string;
// Generate UUID before insert
@BeforeInsert()
generateId() {
if (!this.id) {
this.id = uuidv4();
}
}
}

Composite primary keys use multiple columns to uniquely identify a row.

Composite Primary Key
+------------------------------------------------------------------+
| |
| +---------------------------+ |
| | enrollments | |
| +---------------------------+ |
| | student_id (PK) | <-- Part of composite key |
| | course_id (PK) | <-- Part of composite key |
| | enrollment_date | |
| | grade | |
| +---------------------------+ |
| |
| Primary Key: (student_id, course_id) |
| Each combination is unique |
| |
+------------------------------------------------------------------+
import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm';
import { Student } from './student.entity';
import { Course } from './course.entity';
@Entity('enrollments')
export class Enrollment {
// First part of composite key
@PrimaryColumn()
studentId: number;
// Second part of composite key
@PrimaryColumn()
courseId: number;
@Column()
enrollmentDate: Date;
@Column()
grade: string;
// Relations
@ManyToOne(() => Student, student => student.enrollments)
student: Student;
@ManyToOne(() => Course, course => course.enrollments)
course: Course;
}
import { Entity, PrimaryColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
@Entity('order_items')
export class OrderItem {
@PrimaryColumn()
orderId: number;
@PrimaryColumn()
productId: number;
@Column()
quantity: number;
@Column({ type: 'decimal', precision: 10, scale: 2 })
unitPrice: number;
// Relation to Order
@ManyToOne(() => Order, order => order.items)
@JoinColumn({ name: 'orderId' })
order: Order;
// Relation to Product
@ManyToOne(() => Product)
@JoinColumn({ name: 'productId' })
product: Product;
}

TypeORM supports database-generated values beyond primary keys.

import { Entity, PrimaryGeneratedColumn, Column, Generated } from 'typeorm';
@Entity('orders')
export class Order {
@PrimaryGeneratedColumn()
id: number;
// Auto-generated UUID
@Generated('uuid')
@Column({ unique: true })
orderNumber: string;
// SQL: orderNumber VARCHAR UNIQUE DEFAULT uuid_generate_v4()
@Column()
customerName: string;
}
import { Entity, PrimaryGeneratedColumn, Column, Generated } from 'typeorm';
@Entity('invoices')
export class Invoice {
@PrimaryGeneratedColumn()
id: number;
// Auto-increment identity
@Generated('increment')
@Column({ unique: true })
invoiceNumber: number;
// SQL: invoiceNumber INT UNIQUE GENERATED BY DEFAULT AS IDENTITY
@Column()
customerName: string;
}

For databases that support sequences (PostgreSQL, Oracle).

import { Entity, PrimaryColumn, Column, Generated } from 'typeorm';
@Entity('invoices')
export class Invoice {
// Use sequence for primary key
@PrimaryColumn()
@Generated({
type: 'increment',
name: 'invoice_seq',
})
id: number;
// SQL:
// CREATE SEQUENCE invoice_seq;
// id INT PRIMARY KEY DEFAULT nextval('invoice_seq')
@Column()
customerName: string;
}

Primary Key Selection Guide
+------------------------------------------------------------------+
| |
| Use Case | Recommended Type |
| ----------------------------|---------------------------------|
| General purpose | Auto-increment integer |
| Distributed systems | UUID |
| External reference | UUID |
| Natural key exists | @PrimaryColumn (with caution) |
| Junction tables | Composite key |
| High insert volume | UUID (avoid contention) |
| Public-facing IDs | UUID (hide internal structure) |
| |
+------------------------------------------------------------------+
Auto-Increment vs UUID
+------------------------------------------------------------------+
| |
| Auto-Increment Integer |
| +------------------------+------------------------------------+ |
| | Pros | Cons | |
| +------------------------+------------------------------------+ |
| | Compact (4-8 bytes) | Predictable | |
| | Database efficient | Not globally unique | |
| | Human readable | Can cause contention | |
| | Good for sorting | Exposes table size | |
| +------------------------+------------------------------------+ |
| |
| UUID |
| +------------------------+------------------------------------+ |
| | Pros | Cons | |
| +------------------------+------------------------------------+ |
| | Globally unique | Large (36 chars or 16 bytes) | |
| | Non-predictable | Not human readable | |
| | No contention | Slower inserts (random I/O) | |
| | Good for distributed | Fragmented indexes | |
| +------------------------+------------------------------------+ |
| |
+------------------------------------------------------------------+

// Good
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
}
// Bad - no primary key
@Entity('users')
export class User {
@Column()
name: string;
}
// Good - surrogate key
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string; // Natural key as unique constraint
}
// Risky - natural key as primary key
@Entity('users')
export class User {
@PrimaryColumn()
email: string; // What if email changes?
}
// Good - simple primary key
@Entity('orders')
export class Order {
@PrimaryGeneratedColumn()
id: number;
@Column()
orderNumber: string; // Business identifier
}
// Bad - business logic in primary key
@Entity('orders')
export class Order {
@PrimaryColumn()
orderNumber: string; // Format: ORD-2024-0001
}
@Entity('users')
export class User {
// Internal use
@PrimaryGeneratedColumn()
id: number;
// External use (APIs, URLs)
@Generated('uuid')
@Column({ unique: true })
publicId: string;
}

import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm';
// User entity
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne(() => Profile, profile => profile.user)
profile: Profile;
}
// Profile entity - shares primary key with User
@Entity('profiles')
export class Profile {
// Primary key is also foreign key to User
@PrimaryColumn()
userId: number;
@Column()
bio: string;
@OneToOne(() => User, user => user.profile)
@JoinColumn({ name: 'userId' })
user: User;
}

Primary Key Decorators Summary
+------------------------------------------------------------------+
| |
| Decorator | Description |
| ------------------------|--------------------------------------|
| @PrimaryColumn() | Manual primary key |
| @PrimaryGeneratedColumn()| Auto-generated primary key |
| @PrimaryGeneratedColumn('increment')| Auto-increment integer |
| @PrimaryGeneratedColumn('uuid')| UUID primary key |
| @PrimaryGeneratedColumn('identity')| Identity column (PG 10+) |
| @Generated('uuid') | Auto-generated non-primary UUID |
| @Generated('increment')| Auto-generated non-primary number |
| |
+------------------------------------------------------------------+

Chapter 9: Entity Relationships Overview


Last Updated: February 2026